IoC: Еще один вариант реализации для Scala

от автора

Эта статья не про какую-то готовую библиотеку (я накидал реализацию с тестами за пару дней, и, думаю, читателям не составит труда сделать то же самое), а, скорее, попытка презентовать новую идею. Которая с моей точки зрения выглядит достаточно интересной.

Одна из проблем при разработке сколь-либо большого проекта — это проблема зависимостей. Состоящая из
— где взять нужный экземпляр объекта, если он был создан «наверху», а нужен глубоко внутри иерархии вызовов методов?
— как управлять получением этого экземпляра, чтобы можно было подставлять другие реализации? Прежде всего актуально для тестов
— как это сделать таким образом, чтобы любой кусок кода можно было запустить без долгой пляски с бубном над настройками фреймворка, реализующего п.1 и п.2?

Чтобы избежать обвинений в велосипедостроении и прочих смертных грехах, давайте я изложу своё представление о хорошем коде и хороших библиотеках. Если оно не совпадает с вашим, пожалуйста, воздержитесь от комментирования. Вкратце: Библиотеки должны или решать одну сложную, но конретную задачу, или быть достаточно малы по объему, чтобы изучение исходников было проще, чем чтение документации. Фреймворки предназначены для людей, которые ищут какую-то магию, чтобы оно «само работало». При наличии опыта, практичнее написать свою, заточенную под конкретную задачу, реализацю, скомпоновав реализацию из узкозаточенных, качественных библиотек, чем изучать многомегабайтные исходники фреймворка, написанного «для всех».

Spring я даже рассматривать не буду, времена, когда он считался легковесной альтернативой J2EE, давно прошли. Стандартный скаловский подход со статическими переменными, завернутыми в `object`-ы, я не понимаю: нетестируемо и сложно конфигурируемо. Cake pattern страдает теми же недостатками. Остается Guice и аналогичные библиотеки.

Что насчет Guice? Я обожаю Guice. Практически неинтрузивен, конфигурация модульна и отделена от рабочего кода, возможность подменять отдельные объекты при инициализации инжектора. НО:
— по дефолту, новые объекты создаются одноразовыми, а не синглтонами. Кто это придумал? Кому еще не надоело каждый раз явно указывать скоуп?
— рефлекшн. Негативно сказывается на времени старта приложения, что очень, очень нехорошо во времена sbt-revolver.
— композиция и переопределение для модулей сделаны не так чтобы удобно.
— Not Invented Here (шутка)

Итак идея: хранить готовый Injector в ThreadLocal переменной и использовать статический метод для получения экземпляра. Примерно так:

Много кода

/**   * 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   }  } 

Реализация инжектора представляет из себя простой ассоциативный массив [тип объекта] -> [лямбда, возвращающая экземпляр] плюс кеш для синглтонов.

В коде, которому нужны зависимости, используем inject[Type], чтобы получить объект. Разумеется, лучше всего использовать его в качестве дефолтных значений параметров конструктора.

Что это дает?
— если нужен не синглтон, а объект, то его можно создать просто через new MyClass().
— поддержка скоупов не нужна — текущий инжектор из ThreadLocal можно завернуть в делегат, который сначала запрашивает объекты из инжектора уровня сессии
— никакого рефлекшна
— инжектор можно использовать в доменных объектах! Я думаю, это отличная новость для всех любителей зашивать логику в доменную модель.
— В любом месте кода (и особенно тестов!) можно легко заменить инжектор, получая нужное поведение

Конечно, потребуется некоторая доработка тредпулов, которые используются приложением, чтобы пробрасывать Injector из вызывающего кода в другие потоки, но это не так уж сложно.

Тесты, они же примеры использования

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)     }   } }  

Что думаете?

ссылка на оригинал статьи https://habrahabr.ru/post/278469/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *