Статьи

Модульные абстракции в Scala с тортами и зависимыми от пути типами


Я пробовал различные варианты реализации
шаблона 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 .