I want to tell you about my small library Dependency Injection on Scala. The problem that I wanted to solve: the ability to test the dependency graph before their actual construction and fall as soon as possible if something went wrong, and also to see what exactly the error was. This is exactly what is missing in the wonderful Scaldi DI library. At the same time, we wanted to preserve the external transparency of the syntax and get the most out of the means of the language, and not complicate and get into macros.
I also want to immediately note that I concentrate on DI through the constructor, as in the simplest and most idiomatic way that does not require changes to the implementation of the classes.
You can transfer the constructor as a function to Scala using a partial call, for example:
class A(p0: Int, p1: Int) Module().bind(new A(_: Int, _: Int))
Writing is quite cumbersome, it may be better to use predefined functions that call the constructor, which can be passed explicitly:
class A(p0: Int, p1: Int) object A { def getInstance(p0: Int, p1: Int) = new A(p0, p1) } Module().bind(A.getInstance)
The readability of this style is much better, so in the examples below I will try to use it.
build.sbt:
libraryDependencies += "io.ics" %% "disciple" % "1.2.1"
Imports:
import io.ics.disciple._
Suppose we have a certain set of classes whose instances need to be injected into each other:
Domain entity "User"
case class User(name: String)
A service that takes an instance of an admin user as a parameter and has one method with a primitive implementation.
class UserService(val admin: User) { def getUser(name: String) = User(name) }
We want to have an instance of this service in the form of a singleton - in order to make sure that it is created exactly once, we will get a static counter of instances of this class (for clarity, let's forget about multi-threaded execution).
object UserService { var isCreated: Boolean = false def getInstance(admin: User) = { isCreated = true new UserService(admin) } }
And also the conditional controller dependent on this service
class UserController(service: UserService) { def renderUser(name: String): String = { val user = service.getUser(name) s"User is $user" } } object UserController { def getInstance(service: UserService) = new UserController(service) }
By default, all our bindings are lazy - instances of classes are created only on demand - and are created as many times as requested.
We can also add a singleton
method to the declaration - in this case, the component will be created strictly once. The nonLazy method marks the binding as non-lazy, which means that this component will be created when the module calls the build()
method. Only singleton components can be non-awkward.
Let's see how the creation of a dependency graph will look like using the DIsciple library (note that the order of declaring bindings is not important):
val depGraph = Module(). // , singleton bind(UserController.getInstance _).singleton. // , , , // nonLazy, build(). forNames('admin).bind(UserService.getInstance).singleton.nonLazy. // , 'admin bind(User("Admin")).byName('admin). // , 'customer. // bind(User("Jack")).byName('customer). // build()
Note 1 : You probably noticed that in the case of the controller, we force the transfer of the parameter as a function, and in the case of service - not. This happens due to the fact that there is an overload of the bind function for the by-name function without arguments, in connection with which the compiler cannot understand how to treat a function without parameters - as an object or as a function. I would be glad if someone tell me how to fix this minor inconsistency.
Using:
assert(UserService.isCreated) // build() println(depGraph[User]('customer)) // User customer println(depGraph[UserService].admin) // println(depGraph[UserController].renderUser("George")) // George
Note 2 : if one argument needs to be obtained by name, and the other by type, then you can use the *
operator:
case class A(label: String) case class B(a: A, label: String) case class C(a: A, b: B, label: String) val depGraph = Module(). forNames('labelA).bind { A }. forNames(*, 'labelB).bind { B }. forNames(*, *, 'labelC).bind { C }. bind("instanceA").byName('labelA). bind("instanceB").byName('labelB). bind("instanceC").byName('labelC). build()
val depGraph = Module(). bind { A("instanceA") }. bind { C(_: A, _: B, "instanceC") }. build()
In this example, an exception will be thrown: IllegalStateException: Not found binding for {Type [io.ics.disciple.B]}. (it may be better to create a hierarchy of exceptions, instead of using IllegalStateException everywhere, but so far have not reached out)
case class Dep1(label: String, d: Dep2) case class Dep2(d: DepCycle) case class DepCycle(d: Dep1) Module(). bind(Dep1("test", _: Dep2)). bind(Dep2). bind(DepCycle). build()
An exception will be thrown here: IllegalStateException: Dependency graph contains cyclic dependency: ({Type [io.ics.disciple.DepCycle]} -> {Type [io.ics.disciple.Dep1]} -> {Type [io.ics.disciple .Dep2]} -> {Type [io.ics.disciple.DepCycle]})
By default, components are linked by the final types of results of functions passed to bind()
, but often this is not exactly the behavior we want, for example, if we need to bind a component by a treyt:
trait Service class ServiceImpl extends Service val depGraph = Module(). bind(new ServiceImpl(): Service). build()
I will try to convey the general concept, without going into the details of implementation. By calling the .bind()
method, we generate a list of pairs (DepId, List[Dep])
, where DepId
is either a description of the type of result, or it is the same + identifier:
sealed trait DepId case class TTId(tpe: Type) extends DepId { override def toString: String = s"{Type[$tpe]}" } case class NamedId(name: Symbol, tpe: Type) extends DepId { override def toString: String = s"{Name[${name.name}], Type[$tpe]}" }
A Dep is a pair wrapped constructor function (Injector) for a dependency + a list of IDs, components on which it itself depends:
case class Dep[R](f: Injector[R], depIds: List[DepId])
How often one has to do in Scala in order to make method overloads for different numbers of arguments, one has to generate boilerplates. One of these places is the bind()
method. But, fortunately, the sbt-boilerplate plugin makes this occupation a little less sad. You simply enclose duplicate parts of the declaration between square brackets and grids, and the plug-in understands that they need to be repeated, replacing the whole unit with n, deuces with n + 1, etc. BindBoilerplate.scala.template . As a result, the pattern is compact and eliminates the need to manually hold these huge sheets.
When the build()
method is called, the list of dependencies is converted into a graph (that is, DepId -> Dep), checked for completeness and absence of cyclic dependencies using the DFS algorithm, whose complexity is estimated at O (V + E), where V is the number of components , E - the number of dependencies between them. If something goes wrong, an exception is thrown, otherwise a DepGraph class object is returned, which can already be used to get the final component: depGraph[T]
or depGraph[T]('Id)
- if we need to get a named component.
I used the Symbol, and not the String for the names of the components, because it visually immediately distinguishes identifiers from ordinary string constants in the code. Plus, in addition, we get forced internment, which can be useful in this case.
More dry, but detailed and rich description of the examples, as well as the source code here
Source: https://habr.com/ru/post/311590/