Внедрение зависимостей является одним из методов, которые я регулярно использую, когда программирую на Java. Это хороший способ отделить приложение от конкретных реализаций и локализовать логику создания объекта в определенных модулях начальной загрузки. Будь то в форме Spring XML или Guice Modules, идея состоит в том, чтобы сохранить его настраиваемым, чтобы конкретные компоненты вашего приложения могли работать с конкретными реализациями абстракции.
Случилось так, что в эти дни, возможно, я начал смотреть на вещи немного по-другому. Я больше программировал на Scala и Clojure, и я познакомился со многими из функциональных парадигм, которые они поощряют и поддерживают, что, как я думаю о программировании, проявилось. В этом посте я расскажу о внедрении зависимости в другую заметку. В конце концов, возможно, мы увидим, что это еще один пример паттерна, смешивающегося с делами идиоматического использования мощного языка.
В одном из моих проектов у меня есть класс, в конструктор которого вводятся некоторые параметры, а другие вручную предоставляются приложением. У Guice есть хорошее расширение, которое делает это за вас — AssistedInject, Это пишет шаблонные вещи, генерируя реализацию фабрики. Вам просто нужно аннотировать конструктор класса реализации и поля, которые не известны инжектору. Вот пример со страницы Guice.
public class RealPayment implements Payment {
@Inject
public RealPayment(
CreditService creditService, // injected
AuthService authService, // injected
@Assisted Date startDate, // caller to provide
@Assisted Money amount); // aller to provide
}
...
}
Затем в модуле Guice мы связываем провайдера <Factory>.
bind(PaymentFactory.class).toProvider(
FactoryProvider.newFactory(
PaymentFactory.class, RealPayment.class));
FactoryProvider отображает параметры метода create () на соответствующие параметры @Assisted в конструкторе класса реализации. Для других аргументов конструктора он запрашивает у обычного инжектора значения.
Таким образом, основная проблема, которую решает AssistedInject, состоит в том, чтобы завершить ( закрыть ) некоторые параметры на уровне модуля, которые будут предоставлены инжектором, при этом оставляя абстракцию открытой, чтобы остальная часть была предоставлена вызывающей стороной.
На функциональной ноте это звучит очень похоже на карри . Лучшее обоснование для карри — это возможность частичного применения функций, что делает то же самое, что и выше, предлагая гибкие средства сохранения частей вашей абстракции открытыми для последующего подключения.
Рассмотрим вышеупомянутую абстракцию, смоделированную как класс case в Scala.
trait CreditService
trait AuthService
case class RealPayment(creditService: CreditService,
authService: AuthService,
startDate: Date,
amount: Int)
Одна из особенностей класса case Scala заключается в том, что он автоматически генерирует объект-компаньон вместе с методом apply, который позволяет вам вызывать конструктор класса как объект функции.
val rp = RealPayment( //..
на самом деле является синтаксическим сахаром для RealPayment.apply (// .., который вызывается неявно. Но вы знаете все это .. верно?
Теперь для конкретного модуля, скажем, я хотел бы завершить PayPal как реализацию CreditService, так что пользователям не нужно многократно передавать этот параметр — как инжектор вашего любимого поставщика внедрения зависимостей. Я могу сделать это функциональным образом следующим образом и передать частично примененную функцию всем пользователям модуля.
scala> case class PayPal(provider: String) extends CreditService
defined class PayPal
scala> val paypalPayment = RealPayment(PayPal("bar"), _: AuthService, _: Date, _: Int)
paypalPayment: (AuthService, java.util.Date, Int) => RealPayment = <function>
Обратите внимание, что интерпретатор Scala теперь обрабатывает paypalPayment как функцию из (AuthService, java.util.Date, Int) => RealPayment. Подчеркивание действует как заполнитель, который помогает Scala создать новый объект функции только с этими параметрами. В нашем случае новый функционал принимает только три параметра, для которых мы использовали синтаксис заполнителя. С точки зрения вашего приложения это означает, что мы частично закрыли абстракцию, завершая поставщика для реализации CreditService, а оставшуюся часть оставили открытой. Разве это не то, что делал инжектор Guice выше, вводя некоторые объекты при запуске модуля?
Теперь в модуле я могу вызвать paypalPayment только с 3 открытыми параметрами.
scala> case class DefaultAuth(provider: String) extends AuthService
defined class DefaultAuth
scala> paypalPayment(DefaultAuth("foo"), java.util.Calendar.getInstance.getTime, 10000)
res0: RealPayment = RealPayment(PayPal(foo),DefaultAuth(foo),Sun Feb 28 15:22:01 IST 2010,10000)
Теперь предположим, что для некоторых модулей я хотел бы закрыть абстракцию для AuthService в дополнение к заморозке PayPal в качестве CreditService. Одной из альтернатив будет определение другой абстракции как paypalPayment посредством частичного применения RealPayment, где мы закрываем оба параметра. Лучшим вариантом будет повторное использование абстракции paypalPayment и использование явного каррирования функции. Подобно ..
scala> val paypalPaymentCurried = Function.curried(paypalPayment)
paypalPaymentCurried: (AuthService) => (java.util.Date) => (Int) => RealPayment = <function>
и закрываем его частично, используя реализацию DefaultAuth.
scala> val paypalPaymentWithDefaultAuth = paypalPaymentCurried(DefaultAuth("foo"))
paypalPaymentWithDefaultAuth: (java.util.Date) => (Int) => RealPayment = <function>
Остальная часть модуля теперь может обрабатывать это как абстракцию, которая использует PayPal для CreditService и DefaultAuth для AuthService. Как и Guice, у нас могут быть иерархии модулей, которые внедряют эти параметры и публикуют более специализированную абстракцию для последующих клиентов.