Во втором посте серии Real-World Scala я собираюсь рассказать как реализовывать внедрение зависимостей в Scala (Dependency Injection, DI). Scala очень богатый и глубокий язык, который дает несколько способов задать DI исключительно на основе языковых конструкций, но ничто не мешает использовать существующие фреймворки DI из Java, если это предпочтительнее.
Использование Cake Pattern
Шаблон Cake Pattern впервые объяснил Martin Odersky в статье “Scalable Component Abstractions”[pdf] (замечательная статья, которую я рекомендую) как пример того, как Мартин и его команда структурировали компилятор Scala. Вместо того, чтобы объяснять сам шаблон и как его использовать для реализации DI, лучше взглянем на небольшой пример.
Сначала зададим пользовательский репозитарий UserRepository (DAO)
Можно было бы разделить реализацию на trait интерфейс и его определение, но для упрощения в этом нет необходимости.
Теперь создадим пользовательский сервис и завяжем его на репозитарий.
Здесь можно увидеть, что мы используем экземпляр UserRepository. Это та зависимость, которую мы хотели бы внедрить.
Отлично. Начинается самое интересное. Давайте сначала обернём UserRepository в trait и создадим здесь пользовательский репозитарий.
Это просто создание компонентного пространства имён для нашего репозитария. Далее я покажу зачем это нужно.
Теперь давайте взглянем на UserService, репозитарий пользователей. Чтобы внедрить экземпляр userRepository в UserService, мы сначала сделаем тоже, что и с репозитарием выше; обернём его в trait и используем так называемую self-type annotation для объявления зависимости в сервисе UserRepository.
Звучит запутаннее, чем есть на самом деле. Взглянем на код.
Self-type annotation о которой мы говорим здесь это
Если надо объявить более одной зависимости, то можно композировать несколько аннотаций
Отлично. Мы объявили зависимость UserRepository. Осталось только связывание.
Чтобы сделать это, единственная вещь которую мы должны сделать - совместить разные пространства имён в один модуль. Это достигается путём создания модульного объекта, состоящего из всех наших компонентов. Все связывание происходит автоматически.
Одна из красивых сторон этого решения в том, что связывание статически типизировано. К примеру, если мы забываем объявить зависимость, или ошибаемся в написании или что-то еще сломано, мы получим ошибку компиляции.
Красивость этого решения также в том, что всё неизменяемо (все зависимости объявлены как val).
Чтобы использовать этот шаблон, надо использовать компонент ComponentRegistry, все зависимости будут связаны для нас (также работают Guice/Spring).
Всё ли в порядке?
Нет. Это плохое решение.
У нас есть строгая связь между реализацией сервиса и его созданием, связывающая конфигурация используется везде в коде. Это крайне негибко.
Вместо инстанцирования сервисов в trait-ах, поменяем инстанцируемые объекты на абстрактные поля.
Теперь мы можем переместить объявление(и конфигурацию) сервисов в модуль ComponentRegistry
Используя такое определение мы абстрагируемся от конкретной реализации компонента и связываем все в один конфигурационный объект.
Изящность подхода в том, что мы можем переключаться здесь между различными реализациями служб (если бы мы определили интерфейсный trait и несколько его реализаций). Но еще более интересно то, что мы можем создать несколько “миров” или “окружений” просто совмещая trait-ы в различных комбинациях.
Чтобы показать, что я имею в виду, создадим окружение для модульного тестирования.
В этом случае, вместо создания экземпляра фактических сервисов, мы создадим фиктивную mock-реализацию каждому из них. Мы также изменим окружение на trait (я покажу через секунду почему).
Здесь мы не просто создаем mock-и, но также связывем их в виде объявленных зависимостей, где бы они не были определены.
Хорошо, теперь начинается самое интересное. Давайте создадим модульный тест, в котором мы свяжемся с TestEnvironment, содержащим все mock-и.
Это только один пример того, как вы можете композировать свои компоненты.
Другие альтернативы
Давайте теперь взглянем на некоторые другие способы создания DI в Scala. Этот пост уже достаточно большой, и поэтому я только кратко пробегусь по существующим техникам, но стоит надеяться, что этого будет достаточно
для понимания того, как это делается.
Я опирался во всех этих примерах на одну и ту же маленьку dummy-программу, чтобы было проще понимать и сравнивать различные подхода (программа взята из обсуждения найденного в пользовательской рассылке Scala User mailing list).Во всех этих примерах вы можете просто скопировать и запустить код в интерпретаторе Scala, если вы хотите поиграться с ним.
Использование структурной типизации
Этот приём с использованием структурной типизации был размещена Jamie Webb в рассылке Scala User mailing list некоторое время назад. Мне нравится этот подход; элегантный, неизменяемый(immutable), типобезопасный(type-safe).
Использования неявных объявлений (implicit declarations)
Это простой и прямолинейный подход. Но мне он не очень нравится, потому что фактическое связывание (импорт неявных деклараций) рассеяно и запутано с кодом приложения.
Использование Google Guice
Scala прекрасно работает с отдельными DI фреймворками. Вы можете использовать Guice по-разному, но здесь мы будем обсуждать slick-технику, основанную на ServiceInjector-е, которую показал мне Jan Kriesten.
Это пример подводит итог того, что я запланировал в статье. Я надеюсь, что вы получили некоторое представление о том, как можно достичь внедрения зависимостей DI в Scala - либо с помощью абстракций языка либо отдельным DI фреймворком. Выбирайте то, что подходит лучше для вашего случая, требований и вкуса.
**Добавление
Ниже я добавил Cake Pattern версию последнего примера для облегчения сравнения различных стратегий DI. Просто к сведению, если вы сравните различные стратегии используя этот простой пример, то Cake Pattern может выглядеть немного чрезмерно сложным с его вложенными trait-ами, но такой подход имеет место, когда у вас есть менее тривиальный пример со многими компонентами с более или менее сложными зависимостями для управления.