Статьи

Внедрение фабрик в Scala & MacWire 0.3

Фабрики полезны, когда нам нужно создать несколько экземпляров класса во время выполнения, обычно предоставляя некоторые параметры, но все же без new явного использования  ; мы хотим сделать сложное создание объекта абстрактным. Созданный объект может зависеть как от предоставляемых во время выполнения данных (параметров), так и от некоторых других «сервисов».

MacWire 0.3  добавляет поддержку для некоторых сценариев использования фабрик, в то время как другие могут быть реализованы в чистом Scala.

TL; DR:

  • MacWire 0.3  добавляет поддержку для проводки, используя параметры метода, включающего  wire[] макрос
  • традиционные подходы к определению инъекционных фабрик многословны
  • но используя вложенные классы в Scala, можно определить фабрики  простым, компактным и читаемым способом.

В качестве рабочего примера нашей целью будет создание оболочки для  User объекта домена. Мы хотим обернуть пользователя в  PriceCalculator, который рассчитывает цены на продукты с учетом пользовательских скидок. Чтобы прочитать скидки, калькулятор должен получить доступ к базе данных, используя экземпляр  DatabaseAccess класса.

Следовательно, в идеале мы хотели бы иметь частично проводную  PriceCalculatorсеть, которая имеет доступ к базе данных и которая может быть легко создана для любого данного пользователя. Как реализовать это с помощью Scala и MacWire?

ЗАВОДЫ В МОДУЛЯХ

Во-первых, фабрики могут быть частями «модулей», которые содержат проводные экземпляры. В таком случае вместо a  valмы должны использовать a  def.

Новая функция в MacWire 0.3 заключается в том, что параметры метода включения также используются для подключения.

Например:

case class User
class DatabaseAccess
class PriceCalculator(databaseAccess: DatabaseAccess, user: User)
 
trait ShoppingModule extends Macwire {
   lazy val databaseAccess = wire[DatabaseAccess]
   def priceCalculator(user: User) = wire[PriceCalculator]
}

В этом случае макрос расширит код до:

trait ShoppingModule extends Macwire {
   lazy val databaseAccess = new DatabaseAccess
   def priceCalculator(user: User) = new PriceCalculator(databaseAccess, user)
}

Обратите внимание, что это также будет работать, если модуль был вложен в def; следовательно, мы можем создавать целые графы объектных объектов, используя параметры метода для подключения («подмодули»).

Поскольку на фабриках параметры часто представляют данные, может быть несколько параметров одного типа (включая примитивы). Вот почему при подключении с использованием параметров метода включения, если найдено несколько совпадений для типа, добавляется дополнительный шаг сопоставления по имени.

ИНЪЕКЦИОННЫЕ ЗАВОДЫ

Это хорошо работает в модулях, но что если нам нужно передать фабрику в качестве параметра другому классу? Например, если у нас есть  SpecialOfferMailer, что нужно создать для каждого пользователя PriceCalculator?

Мы могли бы передать объект функции и объявить зависимость следующим образом:

class SpecialOfferMailer(priceCalculator: User => PriceCalculator)

Это также можно рассматривать как частично примененный  PriceCalculator конструктор.

Это может хорошо работать в простых случаях, но имеет три недостатка:

  • нам нужно повторять всю сигнатуру функции всякий раз, когда мы объявляем зависимость (однако здесь может помочь псевдоним типа)
  • мы теряем имена параметров, которые могут вызвать проблемы с читаемостью кода, если наша фабрика принимает примитивы или несколько параметров одного типа
  • для автоматического преобразования defs в функциональные объекты потребуется специальная поддержка MacWire  , или отдельный функциональный объект val придется определять вручную.

Мы также могли бы пойти по традиционному пути определения отдельной фабричной черты, например:

class PriceCalculator(databaseAccess: DatabaseAccess, user: User)
 
object PriceCalculator {
   trait Factory {
      def create(user: User): PriceCalculator
   }
}
 
class SpecialOfferMailer(priceCalculatorFactory: PriceCalculator.Factory) { ... }

тогда мы могли бы также иметь некоторую (еще не реализованную) поддержку для подключения таких фабрик с использованием MacWire, например:

trait ShoppingModule {
   lazy val priceCalculatorFactory 
          = wireFactory[PriceCalculator, PriceCalculator.Factory]
   ...
}

который расширится до:

trait ShoppingModule {
   lazy val priceCalculatorFactory = new PriceCalculator.Factory {
      // the wire here uses values from the method parameters and from the 
      // outer module
      def create(user: User) = wire[PriceCalculator]
   }
   ...
}

Однако, опять же, это имеет недостатки:

  • довольно многословный — отдельная черта
  • нужна специальная поддержка для автоматического подключения
  • нам нужно повторить заводские параметры в  create def и в конструкторе класса

SCALA FACTORIES

Однако есть и другой способ — используя классы case немного нетипичным образом, мы получим элегантную фабричную реализацию. Например:

class PriceCalculatorFactory(databaseAccess: DatabaseAccess) {
   case class create(user: User) {
      // methods
   }
}
 
class SpecialOfferMailer(priceCalculatorFactory: PriceCalculatorFactory)
 
// Nothing special here. Just plain ol' MacWire
trait ShoppingModule extends Macwire {
   lazy val databaseAccess = wire[DatabaseAccess]
   lazy val priceCalculatorFactory = wire[PriceCalculatorFactory]
   lazy val specialOfferMailer = wire[SpecialOfferMailer]
}

Обратите внимание, что при таком подходе параметры службы и параметры данных хорошо разделены (первый — это параметры основного класса, последний — параметры вложенного класса наблюдения). Также определение списка параметров не повторяется! И нам не нужна какая-либо специальная поддержка для автоматического подключения.

Поскольку вложенный класс является классом case, использование  create выглядит как вызов метода, хотя на самом деле это конструктор нового объекта, например:

class SpecialOfferMailer(...) {
   def mailOfferOfTheDay(user: User) {
      val priceCalculator = priceCalculatorFactory.create(user)
      products.foreach { product => 
          mailOffer(user, product, priceCalculator.price(product)) }
   }
}

Тип каждого пользователя объекта цена калькулятор  PriceCalculatorFactory#create. Это немного уродливо, особенно если нужно передать его, но мы можем добавить, например, к объекту пакета псевдоним типа:

type PriceCalculator = PriceCalculatorFactory#create

Исходники для  MacWire  доступны на GitHub под лицензией Apache2; и бинарный релиз находится в центральном хранилище.

Веселитесь, и дайте мне знать, что вы думаете о внедрении Scala-factory!