Я пробовал различные варианты реализации
шаблона Cake в Scala, который считается одним из многих способов внедрения зависимости без использования какой-либо дополнительной инфраструктуры. Существуют и другие (более функциональные) способы сделать то же самое, об одном я
писал ранее, а также
говорил на встрече в Нью-Йорке Scala. Но я отвлекся ..
Назовите это как DI или нет, шаблон Cake является одним из полезных методов для реализации модульных абстракций в Scala. Вы вплетаете свои абстрактные компоненты (черты характера), разделяете их по зависимостям и обязываетесь к реализации только в конце света. Я пытался придумать реализацию, которая не использует аннотации собственного типа. Это не значит, что я думаю, что аннотации типа себя глупы или что-то в этом роде, но я не нахожу их в других местах, кроме шаблона Cake. И, конечно же, взаимно рекурсивные самоаннотации — это запах кода, который делает вашу систему антимодульной.
В следующей реализации я использую зависимые от пути типы, которые стали обычной функцией в Scala 2.10. Между прочим, это было там с давних времен под благословением экспериментальной функции, но появилось на публике только в 2.10. Следствием этого является то, что вместо аннотаций собственного типа или наследования я буду настраивать свои зависимости с помощью композиции.
Позвольте мне начать с некоторых основных абстракций очень простой предметной модели. Основным компонентом, который я создам, является сервис, который сообщает о портфеле клиентов в виде баланса. Пример был упрощен в целях иллюстрации — реальная модель в реальной жизни имеет гораздо более сложную реализацию.
Портфель представляет собой набор
противовесов .
Баланс
позиция
Аккаунта в определенной
Валюте на определенную
Дату . Выражая это в простых терминах, мы имеем следующие черты ..
// currency sealed trait Currency case object USD extends Currency case object EUR extends Currency case object AUD extends Currency //account case class Account(no: String, name: String, openedOn: Date, status: String) trait BalanceComponent { type Balance def balance(amount: Double, currency: Currency, asOf: Date): Balance def inBaseCurrency(b: Balance): Balance }
Интересно отметить, что фактический тип
Balance
был абстрагирован
BalanceComponent
, поскольку различные службы могут использовать различные представления баланса. И это один из слоев торта, который мы наконец-то смешаем.
Просто примечание для непосвященных, базовой валютой обычно считается внутренняя валюта или валюта учета. Для целей бухгалтерского учета фирма может использовать базовую валюту для представления всех прибылей и убытков. Таким образом, у нас может быть какой-либо сервис или компонент, который хотел бы, чтобы сальдо сообщалось в базовой валюте.
trait Portfolio { val bal: BalanceComponent import bal._ def currentPortfolio(account: Account): List[Balance] }
Портфолио использует реферат
BalanceComponent
и не фиксирует какую-либо конкретную реализацию. И
Balance
возвращаемый тип метода
currentPortfolio
на самом деле является типом, зависящим от пути, который хорошо выглядит благодаря синтаксису импорта объектов.
Теперь давайте представим несколько отдельных реализаций вышеперечисленных компонентов .. мы еще не готовы смешать торт.
// report balance as a TUPLE3 - simple trait SimpleBalanceComponent extends BalanceComponent { type Balance = (Double, Currency, Date) override def balance(amount: Double, currency: Currency, asOf: Date) = (amount, currency, asOf) override def inBaseCurrency(b: Balance) = ((b._1) * baseCurrencyFactor.get(b._2).get, baseCurrency, b._3) } // report balance as an ADT trait CustomBalanceComponent extends BalanceComponent { type Balance = BalanceRep // balance representation case class BalanceRep(amount: Double, currency: Currency, asOf: Date) override def balance(amount: Double, currency: Currency, asOf: Date) = BalanceRep(amount, currency, asOf) override def inBaseCurrency(b: Balance) = BalanceRep((b.amount) * baseCurrencyFactor.get(b._2).get, baseCurrency, b.asOf) }
И пример реализации,
ClientPortfolio
который добавляет логику, но не фиксирует какой-либо конкретный тип для
BalanceComponent
.
trait ClientPortfolio extends Portfolio { val bal: BalanceComponent import bal._ override def currentPortfolio(account: Account) = { //.. actual impl will fetch from database List( balance(1000, EUR, Calendar.getInstance.getTime), balance(1500, AUD, Calendar.getInstance.getTime) ) } }
Аналогично
ClientPortfolio
, мы можем иметь несколько реализаций отчетов Портфолио, в которых отчеты представлены в различных формах. Итак, наш торт начал обретать форму. У нас есть
Portfolio
компонент, и компонент BalanceComponent уже встроен без какой-либо реализации. Давайте добавим еще один слой в микс, может быть, для удовольствия — декоратор для
Portfolio
.
Мы добавляем
Auditing
в качестве компонента, который может украсить * любой *
Portfolio
компонент и сообщать баланс счета в базовой валюте. Обратите внимание, что
Auditing
необходимо абстрагировать реализации,
BalanceComponent
так как
Portfolio
идея заключается в том, чтобы декорировать любой
Portfolio
компонент, используя любую из базовых
BalanceComponent
реализаций.
Многие реализации тортов используют для этого аннотации собственного типа (или наследование). Я буду использовать состав и зависимые от пути типы.
trait Auditing extends Portfolio { val semantics: Portfolio val bal: semantics.bal.type import bal._ override def currentPortfolio(account: Account) = { semantics.currentPortfolio(account) map inBaseCurrency } }
Обратите внимание, как
Auditing
компонент использует ту же
Balance
реализацию, что и базовый декорированный Portfolio
компонент, реализуемый
через зависимые от пути типы.
И мы достигли конца света, еще не взяв на себя обязательства по реализации наших компонентов. Но теперь давайте сделаем это и создадим конкретный сервис.
object SimpleBalanceComponent extends SimpleBalanceComponent object CustomBalanceComponent extends CustomBalanceComponent object ClientPortfolioAuditService1 extends Auditing { val semantics = new ClientPortfolio { val bal = SimpleBalanceComponent } val bal: semantics.bal.type = semantics.bal } object ClientPortfolioAuditService2 extends Auditing { val semantics = new ClientPortfolio { val bal = CustomBalanceComponent } val bal: semantics.bal.type = semantics.bal }
Попробуйте в своем Repl и посмотрите, как эти две службы ведут себя одинаково, абстрагируя все реализации компонентов от пользователя.
scala> ClientPortfolioAuditService1.currentPortfolio(Account("100", "dg", java.util.Calendar.getInstance.getTime, "a")) res0: List[(Double, com.redis.cake.Currency, java.util.Date)] = List((1300.0,USD,Thu Jan 31 12:58:35 IST 2013), (1800.0,USD,Thu Jan 31 12:58:35 IST 2013)) scala> ClientPortfolioAuditService2.currentPortfolio(Account("100", "dg", java.util.Calendar.getInstance.getTime, "a")) res1: List[com.redis.cake.ClientPortfolioAuditService2.bal.Balance] = List(BalanceRep(1300.0,USD,Thu Jan 31 12:58:46 IST 2013), BalanceRep(1800.0,USD,Thu Jan 31 12:58:46 IST 2013))
Техника, рассмотренная выше, основана на статье «
Полиморфное встраивание DSL» . Я использую эту технику в течение достаточно долгого времени, и я обсуждал несколько похожую реализацию в моей книге
DSLs в действии , обсуждая внутренний дизайн DSL в Scala.
И если вас интересует полный код, я загрузил его на свой
Github .