Pull to refresh

Dependency injection для Scala: Cake Pattern

Website development *
Я совсем недавно начал изучать 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, скажем для обеспечения проверки прав на исполнение кода или декларативного управления транзакциями, да и многие другие вещи, но об этом я напишу как-нибудь в следующий раз.

Tags:
Hubs:
Total votes 9: ↑9 and ↓0 +9
Views 14K
Comments Comments 2