Dependency injection для Scala: Cake Pattern

    Я совсем недавно начал изучать Scala. Для тех, кто еще не в курсе, что это за язык, небольшая выдержка с официального сайта:

    Scala — лаконичный, элегантный и статически типизированный язык программирования, который сочитает в себе возможности обьектно-ориентированного и функционального языка. Scala полностью совместима с Java.

    Сегодня я хотел бы показать вам, как, используя богатые выразительные способности этого языка, решить проблему, актуальную для любого более-менее крупного проекта, а именно работу с зависимостями компонентов или dependency injection. Последние несколько лет я использовал spring ioc для решения этой проблемы, однако у этого фрэймворка есть несколько недостатков, самый очевидный из которых это сборка приложения из компонент в runtime и наличие xml-дескрипторов (да, конечно можно использовать и autowiring и аннотации, но и у этих возможностей есть свои серьезные проблемы).

    Может быть я, конечно, страдаю недостатком внимания, но типовая проблема для меня — написав компонент, я, зачастую, забываю прописать его в xml-дескрипторе, или забываю прописать какие-то из его зависимостей. Обнаружить же эту проблему можно только в runtime — запустив либо автоматические тесты, либо само приложение. Да и в целом, наличие в приложении xml дескрипторов вызывает у меня некоторое раздражение еще с времен работы с JSF. Чем меньше дескрипторов, тем лучше.

    На мою радость в Scala есть достаточно лаконичный способ организации DI избавленный от подобных недостатков. Сам шаблон Cake pattern был описан в статье «Scalable Components Abstraction», написанной Martin Odersky и Matthias Zenger, которую я настоятельно рекомендую к прочтению. Название же шаблона было предложено Jon Pretty в обсуждении на Nabble: «Обоснованием является, помимо моей любви к тортам, то что торт сделан из нескольких слоев (разделенных джемом) и может быть порезан на ломтики». Название прижилось.

    Рассмотрим на простеньком примере реализацию этого шаблона. Начнем с обьявления двух компонент приложения:
    trait NameProviderComponent {
    val nameProvider:NameProvider

    trait NameProvider {
    def getName:String
    }

    }

    trait SayHelloComponent {
    val sayHelloService:SayHelloService

    trait SayHelloService {
    def sayHello:Unit
    }

    }

    Для чего необходима упаковка наших интерфейсов компонент в контейнеры NameProviderComponent и SayHelloComponent, а так же зачем потребовались константы nameProvider:NameProvider и sayHelloService:SayHelloService станет ясно чуть позже, пока же напишем реализацию этих компонент. Начнем с провайдера имени:
    trait NameProviderComponentImpl extends NameProviderComponent {

    class NameProviderImpl extends NameProvider {
    def getName:String = "World"
    }

    }

    Всё достаточно просто, однако, пока не проясняет процесса вставки зависимостей. Рассмотрим единственный модуль нашего приложения, который будет иметь зависимости:
    trait SayHelloComponentImpl extends SayHelloComponent {
    this: SayHelloComponentImpl with NameProviderComponent =>

    class SayHelloServiceImpl extends SayHelloService {
    def sayHello:Unit = println("Hello, "+nameProvider.getName+"!")
    }

    }

    Разберемся как это работает. Первой обращает на себя внимание конструкция: «this: SayHelloComponentImpl with NameProviderComponent =>», которая буквально сообщает компилятору какой именно тип должен иметь обьект, реализующий данный trait. После такой декларации, становится возможным использование неопределенной пока константы nameProvider в теле функции sayHello, так как trait NameProviderComponent обьявляет такую константу. Это и есть тот самый джем, соединяющий слои.

    Перейдем к торту вцелом:
    object ComponentRegistry
    extends SayHelloComponentImpl
    with NameProviderComponentImpl {
    val nameProvider = new NameProviderImpl
    val sayHelloService = new SayHelloServiceImpl
    }

    ; объект ComponentRegistry реализует оба trait реализаций наших модулей и определяет последние, до сих пор неопределенные, члены класса — константы nameProvider и sayHelloService. Полученный из этого обьекта sayHelloService будет использовать для получения имени nameProvider NameProviderImpl. ComponentRegistry является прямым аналогом SpringContext.

    Как это использовать:
    object MyApplication {
    def main(args : Array[String]) : Unit = {
    ComponentRegistry.sayHelloService.sayHello
    }
    }

    исполнение этого приложения дает вполне ожидаемый результат: «Hello, World!»

    Теперь немного о приемуществах:
    • «сборка» контекста/реестра частично осуществляется в момент компиляции. «Забытые» зависимости сломают сборку. Невозможна компиляция с зависимостями несоответствующих типов. Например:
      object BrokenComponentRegistry
      extends SayHelloComponentImpl {
      val sayHelloService = new SayHelloServiceImpl
      }

      сломает компиляцию с вполне вменяемым сообщением «illegal inheritance; self-type BrokenComponentRegistry.type does not conform to SayHelloComponentImpl's selftype SayHelloComponentImpl with NameProviderComponent»
    • Пусть это и не сильно критично, но время старта приложения уменьшается, поскольку не требуется парсинга xml дескрипторов и активной работы с reflections API.


    Понятное дело, dependency injection — всего лишь маленький необходимый в типовом проекте кусочек. Кроме этого совсем не помешали бы и AOP, скажем для обеспечения проверки прав на исполнение кода или декларативного управления транзакциями, да и многие другие вещи, но об этом я напишу как-нибудь в следующий раз.

    • +9
    • 10,2k
    • 2
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 2
    • +2
      xml конфигурация это не недостаток, а великая возможность не меняя само приложение изменить его.
      • 0
        И да и нет. В десятке последних проектов, после деплоймента production версии, конфигурация не менялась ни разу. Я понимаю что данное утверждение справедливо не для всех проектов. Но на моей памяти спринговые xml чаще живут внутри jar'а и никем не меняются во время эксплуатации. Большой разницы исправить ли xml или реестр модулей для меня нет, однако, проверка реестра компиляцией — очень удобная вещь.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое