Статьи

Внедрение зависимостей в Scala: расширение шаблона Cake

Продолжая мини-серию по внедрению зависимостей (см. Мои предыдущие блоги: проблемы с DI , вспомогательные инъекции для CDI и улучшенные вспомогательные инъекции ), я взглянул на то, как обрабатывается DI в Scala.

Есть несколько подходов, один из самых интересных — это Cake Pattern. Это решение DI, использующее только функции родного языка, без какой-либо поддержки инфраструктуры. Хорошее введение можно найти в блоге Джонаса Бонера (на котором в большей степени основан этот пост) или в статье Мартина Одерски « Абстракции масштабируемых компонентов» .

Я хотел бы расширить Cake Pattern, чтобы позволить определять зависимости, которые требуют создания пользовательских данных (например, в автозаводах / ассистированных инъекциях).

Образец Торта: интерфейсы

Но давайте начнем с примера базового шаблона. Допустим, у нас есть класс User,

sealed case class User(username: String)

и что мы хотим создать службу UserRepository. Используя Cake Pattern, сначала мы создаем «интерфейс»:

trait UserRepositoryComponent { // For expressing dependencies
def userRepository: UserRepository // Way to obtain the dependency

trait UserRepository { // Interface exposed to the user
def find(username: String): User
}
}

У нас есть три важные вещи здесь:

  • черта UserRepositoryComponent будет использоваться для выражения зависимостей. Содержит определение компонента, состоящее из:
  • способ получения зависимости: метод def userRepository (также может быть val, но почему лучше def, я объясню позже)
  • сам интерфейс, здесь черта UserRepository, которая дает функциональность поиска пользователей по имени пользователя

Образец Торта: реализации

Реализация компонента выглядит примерно так:

trait UserRepositoryComponentHibernateImpl
extends UserRepositoryComponent {
def userRepository = new UserRepositoryImpl

class UserRepositoryImpl extends UserRepository {
def find(username: String): User = {
println("Find with Hibernate: " + username)
new User(username)
}
}
}

Здесь нет ничего особенного. Реализация компонента расширяет черту компонента «интерфейс». Это позволяет использовать черту UserRepository, которая может быть реализована.

Использование зависимостей

Как один компонент / услуга может сказать, что это зависит от другого? Самостоятельные аннотации Scala здесь очень полезны. Например, если компонент UserAuthorization требует UserRepository, мы можем написать это следующим образом:

// Component definition, as before
trait UserAuthorizationComponent {
def userAuthorization: UserAuthorization

trait UserAuthorization {
def authorize(user: User)
}
}

// Component implementation
trait UserAuthorizationComponentImpl
extends UserAuthorizationComponent {
// Dependencies
this: UserRepositoryComponent =>

def userAuthorization = new UserAuthorizationImpl

class UserAuthorizationImpl extends UserAuthorization {
def authorize(user: User) {
println("Authorizing " + user.username)
// Obtaining the dependency and calling a method on it
userRepository.find(user.username)
}
}
}

Важной частью здесь является следующее: UserRepositoryComponent =>. Этим фрагментом кода мы указываем, что UserAuthorizationComponentImpl требует некоторой реализации UserRepositoryComponent. Это также переносит содержимое UserRepositoryComponent в область видимости, так что видны как метод получения пользовательского репозитория, так и сам признак UserRepository.

электропроводка

Как мы соединяем различные компоненты вместе? Опять довольно легко. Например:

val env = new UserAuthorizationComponentImpl
with UserRepositoryComponentHibernateImpl

env.userAuthorization.authorize(User("1"))

Сначала нам нужно создать среду, объединив все реализации компонентов, которые мы хотим использовать, в один объект. Далее мы можем вызывать методы в среде для получения услуг.

Как насчет тестирования? Также легко:

val envTesting = new UserAuthorizationComponentImpl
with UserRepositoryComponent {
def userRepository = mock(classOf[UserRepository])
}
envTesting.userAuthorization.authorize(User("3"))

Здесь мы смоделировали пользовательский репозиторий, чтобы мы могли тестировать UserAuthorizationComponentImpl изолированно.

defs over vals

Почему defs в определении компонента лучше в качестве способа получения зависимости? Потому что, если вы используете val, все реализации заблокированы и должны предоставить один экземпляр зависимости (константа). С помощью метода вы можете возвращать разные значения при каждом вызове. Например, в веб-среде это отличный способ реализовать область видимости! Метод может читать из запроса или состояния сеанса. Конечно, все еще можно предоставить синглтон. Или новый экземпляр зависимости от каждого вызова.

Зависимости, которые требуют пользовательских данных

Наконец, мы подошли к главному. Что если нашим зависимостям нужны данные во время выполнения? Например, если мы хотим создать службу UserInformation, которая оборачивает экземпляр User?

Ну, кто сказал, что методы, с помощью которых мы получаем зависимости, должны быть без параметров?

// Interface
trait UserInformationComponent {
// What is needed to create the component
def userInformation(user: User)

trait UserInformation {
def userCountry: Country
}
}

// Implementation
trait UserInformationComponentImpl
extends UserInformationComponent {
// Dependencies
this: CountryRepositoryComponent =>

def userInformation(user: User) = new UserInformationImpl(user)

class UserInformationImpl(val user: User) extends UserInformation {
def userCountry: Country {
// Using the dependency
countryRepository.findByEmail(user.email)
}
}
}

// Usage
val env = new UserInformationComponentImpl
with CountryRepositoryComponentImpl
env.userInformation(User("[email protected]")).userCountry

Разве это не лучше, чем передача экземпляра User в качестве параметра метода?

Использование Cake Pattern, создание зависимостей с состоянием, которые могут быть созданы во время выполнения с данными, предоставленными пользователем, и при этом зависеть от других компонентов, — очень просто. Это похоже на заводской метод, но с гораздо меньшим шумом.

Хорошее и плохое

Добро:

  • рамки не требуются, используются только языковые функции
  • тип safe — отсутствующая зависимость обнаружена во время компиляции
  • мощный — «вспомогательный ввод», определение области возможно путем соответствующей реализации метода обеспечения зависимости

Плохо:

  • довольно много стандартного кода: каждый компонент имеет интерфейс компонента, реализацию, интерфейс службы и реализацию службы

Однако я не думаю, что определение всех четырех частей всегда необходимо. Если есть только одна реализация компонента, вы можете объединить интерфейс компонента и реализацию в одну, и, если есть необходимость, выполнить рефакторинг позже.

Адам

С http://www.warski.org/blog/?p=291