📜 ⬆️ ⬇️

IoC: Another Scala implementation

This article is not about some kind of ready-made library (I’ve been throwing in the implementation with tests for a couple of days, and I think it’s easy for readers to do the same), but rather an attempt to present a new idea. Which from my point of view looks quite interesting.

One of the problems in developing any large project is the problem of dependencies. Consisting of
- where to get the necessary instance of the object, if it was created “above”, but is needed deep inside the hierarchy of method calls?
- How to manage the receipt of this instance so that you can substitute other implementations? First of all, relevant for tests
- how to do it in such a way that any piece of code can be run without a long dance with a tambourine over the settings of the framework that implements step 1 and 2?


')
To avoid accusations of cycling and other deadly sins, let me set out my idea of ​​good code and good libraries. If it does not match yours, please refrain from commenting. In short: Libraries should either solve one complex, but specific task, or be small enough to make the study of sources easier than reading the documentation. The frameworks are designed for people who are looking for some kind of magic so that it “works itself”. If you have experience, it is more practical to write your own, customized for a specific task, for realizing, putting together the implementation from narrow, high-quality libraries, than to study the multi-megabyte source code of the framework written “for all”.

I will not even consider Spring, the days when it was considered a lightweight alternative to J2EE are long gone. I don’t understand the standard skal approach with static variables wrapped in `object`: untestable and difficult to configure. Cake pattern suffers from the same drawbacks. Remains Guice and similar libraries.

What about Guice? I adore Guice. Almost non-intrusive, the configuration is modular and separated from the working code, the ability to replace individual objects during the initialization of the injector. BUT:
- by default, new objects are created by disposable, not singleton. Who came up with it? Who hasn't bothered to explicitly specify a scop each time?
- reflexion. It has a negative effect on the launch time of the application, which is very, very bad at the time of sbt-revolver.
- composition and redefinition for modules are not made so conveniently.
- Not Invented Here (joke)

So the idea is to store the finished Injector in a ThreadLocal variable and use a static method to get an instance. Like that:

Lot of code
/** * Marker interface for custom injector keys, useful to encode object type into the key. * * @tparam T type of object returned by this key */ trait InjectorKey[T] /** * The injector - main interface used to bind code to this injector and access stored objects directly */ trait Injector { /** * Bind this injector to current thread. * @param func code that will be bound to this injector * @tparam T return type * @return value, returned by func */ def let[T](func: => T): T def getInstance[T](classTag: ClassTag[T]): Option[T] def getInstance[T](key: Any, classTag: ClassTag[T]): Option[T] /** * Initialize all known dependencies eagerly. Good for production mode and validation of dependencies. */ def eagerInit(): Unit } object Injector { private[depend] val context = new ThreadLocal[Injector]() /** * Get object instance by type. Fails if this type cannot be resolved to single instance * * @param classTag class tag * @tparam T type of value to return * @return value */ def inject[T](implicit classTag: ClassTag[T]): T = injector.getInstance(classTag).getOrElse { throw new InjectorException("No instance registered for " + classTag) } /** * Get object instance by type and some type-assisted key. Useful if you have multiple instances of the same type. * * @param key key, used to identify object * @param classTag class tag * @tparam T type of value to return * @return value */ def inject[T](key: InjectorKey[T])(implicit classTag: ClassTag[T]): T = injector.getInstance(key, classTag).getOrElse { throw new InjectorException("No instance registered for " + classTag + ", key=" + key) } /** * Get object instance by type and some key. Useful if you have multiple instances of the same type. * * @param key key, used to identify object * @param classTag class tag * @tparam T type of value to return * @return value */ def inject[T](key: Any)(implicit classTag: ClassTag[T]): T = injector.getInstance(key, classTag).getOrElse { throw new InjectorException("No instance registered for " + classTag + ", key=" + key) } /** * Return current injector * @return current injector or exception */ def injector: Injector = { val r = context.get() if (r == null) { throw new IllegalStateException("There is no injector in current context. Forgot to run Injector.let for this thread?") } r } } 



The implementation of the injector is a simple associative array [object type] -> [lambda, returning an instance] plus the cache for singletons.

In code that needs dependencies, use inject [Type] to get an object. Of course, it is best to use it as the default value of the parameters of the constructor.

What does this give?
- if you need not a singleton, but an object, then you can create it simply through new MyClass ().
- support for scopes is not needed - the current injector from ThreadLocal can be wrapped in a delegate that first requests objects from a session-level injector
- no reflex
- the injector can be used in domain objects! I think this is great news for all lovers of stitching logic in a domain model.
- In any place of the code (and especially tests!) You can easily replace the injector, getting the desired behavior

Of course, it will take some refinement of the trapwords that are used by the application to forward Injector from the calling code to other threads, but this is not so difficult.

Tests, they are examples of use.
 class InjectorTest extends FunSuite { import Injector.inject test("basic query by type") { val i = Injector.newModule() .bind[String]("hello") .injector() i.let { assert(inject[String] == "hello") //no object of type Date intercept[InjectorException] { inject[Date] } //we have no keys intercept[InjectorException] { inject[String]("hello") } } } test("objects are singletons") { class A val i = Injector.newModule() .bind[Date](new Date()) .bind[A]("key")(new A) .injector() i.let { assert(inject[Date] eq inject[Date]) assert(inject[A]("key") eq inject[A]("key")) } } test("query by key works") { val i = Injector.newModule() .bind[Date]("key1")(new Date(1)) .bind[Date]("key2")(new Date(2)) .injector() i.let { assert(inject[Date]("key1") == new Date(1)) assert(inject[Date]("key2") == new Date(2)) intercept[InjectorException] { inject[String]("key1") } intercept[InjectorException] { inject[Date]("key3") } } } test("object cannot be bound twice") { intercept[InjectorException] { Injector.newModule() .bind[Date](new Date(0)) .bind[Date](new Date(1)) } } test("Binding with dependencies works") { class A class B case class C(a: A, b: B) val i = Injector.newModule() .bind[C](C(inject[A], inject[B])) .bind[A](new A) .bind[B](new B) .injector() i.let { assert(inject[C].a eq inject[A]) assert(inject[C].b eq inject[B]) } } test("Binding with cyclic dependencies does not work") { case class A(c: C) class B case class C(a: A, b: B) val i = Injector.newModule() .bind[C]{ C(inject[A], inject[B]) } .bind[A](A(inject[C])) .bind[B](new B) .injector() i.let { intercept[InjectorException] { inject[C] } } } test("modularization") { class A class B case class C(a: A, b: B) val m1 = Injector.newModule().bind[C](C(inject[A], inject[B])) val m2 = Injector.newModule() .bind[A](new A) .bind[B](new B) m1.injector().let { intercept[InjectorException] { inject[C] } } (m1 + m2).injector().let { inject[C] } } test("It is possible to inject primitive type") { val i = Injector.newModule() .bind[Int](42) .injector() i.let { assert(inject[Int] == 42) } } } 



What do you think?

Source: https://habr.com/ru/post/278469/


All Articles