Scala Cake Pattern и коллизии зависимостей

Я пытаюсь реализовать внедрение зависимостей в Scala с помощью Cake Pattern, но сталкиваюсь с коллизиями зависимостей. Поскольку я не смог найти подробный пример с такими зависимостями, вот моя проблема:

Предположим, у нас есть следующая черта (с 2 реализациями):

trait HttpClient {
  def get(url: String)
}

class DefaultHttpClient1 extends HttpClient {
  def get(url: String) = ???
}

class DefaultHttpClient2 extends HttpClient {
  def get(url: String) = ???
}

И следующие два модуля шаблона торта (которые в этом примере являются API, которые зависят от нашего HttpClient за их функциональность):

trait FooApiModule {
  def httpClient: HttpClient        // dependency
  lazy val fooApi = new FooApi()    // providing the module's service

  class FooApi {
    def foo(url: String): String = {
      val res = httpClient.get(url)
      // ... something foo specific
      ???
    }
  }
}

а также

trait BarApiModule {
  def httpClient: HttpClient        // dependency
  lazy val barApi = new BarApi()    // providing the module's service

  class BarApi {
    def bar(url: String): String = {
      val res = httpClient.get(url)
      // ... something bar specific
      ???
    }
  }
}

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

object MyApp extends FooApiModule with BarApiModule {
  // the same dependency supplied to both modules
  val httpClient = new DefaultHttpClient1()

  def run() = {
    val r1 = fooApi.foo("http://...")
    val r2 = barApi.bar("http://...")
    // ...
  }
}

Мы могли бы по-разному называть зависимости в каждом модуле, добавляя к ним префикс с именем модуля, но это было бы громоздко и не элегантно, а также не сработало бы, если бы мы сами не имели полного контроля над модулями.

Есть идеи? Я неверно истолковываю Шаблон Торта?

5 ответов

Решение

Вы получаете шаблон правильно, и вы только что обнаружили его важное ограничение. Если два модуля зависят от какого-либо объекта (скажем, HttpClient) и случайно объявляют его под тем же именем (например, httpClient), игра окончена - вы не будете настраивать их отдельно в одном Cake. Либо есть два торта, как советует Дэниел, либо поменяйте источники модулей, если можете (как намекает Томер Габель).

У каждого из этих решений есть свои проблемы.

Наличие двух тортов (совет Дэниела) выглядит хорошо, если им не нужны общие зависимости.

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

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

"Вы делаете это неправильно" (ТМ). У вас точно такая же проблема с Spring, Guice или любым контейнером IoC: вы рассматриваете типы как имена (или символы); Вы говорите "Дайте мне HTTP-клиент" вместо "Дайте мне HTTP-клиент, подходящий для связи с fooApi".

Другими словами, у вас есть несколько клиентов HTTP все по имени httpClient, что не позволяет вам делать какие-либо различия между различными экземплярами. Это похоже на использование @Autowired HttpClient без какого-либо способа определения ссылки (в случае Spring, как правило, по идентификатору компонента с внешней проводкой).

В шаблоне тортов один из способов решить эту проблему - определить это различие под другим именем: FooApiModule требует, например, def http10HttpClient: HttpClient а также BarApiModule требует def connectionPooledHttpClient: HttpClient, При "заполнении" разных модулей разные имена ссылаются на два разных экземпляра, но также указывают на ограничения, накладываемые этими модулями на их зависимости.

Альтернатива (выполнимая, хотя и не столь чистая, по моему мнению) - просто требовать именованной зависимости для конкретного модуля, т.е. def fooHttpClient: HttpClient, который просто навязывает явную внешнюю проводку тому, кто смешивает ваш модуль.

Вместо расширения FooApiModule а также BarApiModule в одном месте - что означало бы, что они имеют общие зависимости - сделайте их обоими отдельными объектами, каждый со своими зависимостями, решенными соответственно.

Кажется, это известная проблема "ног робота". Вам нужно построить две ноги робота, однако вам нужно поставить две разные ноги для них.

Как использовать шаблон торта, чтобы иметь как общие зависимости, так и отдельные?

Давайте L1 <- A, B1; L2 <- A, B2, И вы хотите иметь Main <- L1, L2, A,

Чтобы иметь отдельные зависимости, нам нужно два экземпляра меньших тортов, параметризованных с общими зависимостями.

trait LegCommon { def a:A}
trait Bdep { def b:B }
class L(val common:LegCommon) extends Bdep { 
  import common._
  // declarations of Leg. Have both A and B.
}
trait B1module extends Bdep {
  val b = new B1
}
trait B2module extends Bdep {
  def b = new B2
}

В Main у нас будет общая часть в торте и две ноги:

trait Main extends LegCommon {
  val l1 = new L(this) with B1module
  val l2 = new L(this) with B2module
  val a = new A
}

Ваше окончательное приложение должно выглядеть так:

object MyApp {
  val fooApi = new FooApiModule {
    val httpClient = new DefaultHttpClient1()
  }.fooApi
  val barApi = new BarApiModule {
     val httpClient = new DefaultHttpClient2()
  }.barApi
  ...

 def run() = {
  val r1 = fooApi.foo("http://...")
  val r2 = barApi.bar("http://...")
  // ...
 }
}

Это должно работать. (Адаптировано из этого блога: http://www.cakesolutions.net/teamblogs/2011/12/19/cake-pattern-in-depth/)

Другие вопросы по тегам