Scala шаблон торта для объектов с разным временем жизни

Я попытался использовать шаблон торта в своем проекте и очень понравился, но есть одна проблема, которая меня беспокоит.

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

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

Это легко сделать в средах DI, таких как Guice: если я хочу, чтобы несколько экземпляров какого-то класса я просто вводил Provider<MyClass>; затем я вызываю метод get() этого провайдера, и все зависимости MyClass автоматически разрешаются. Если MyClass требует некоторых динамически вычисляемых данных, я могу использовать вспомогательное расширение для инъекций, но полученный код все еще сводится к провайдеру /factory. Связанная концепция, области, также помогает.

Но я не могу придумать хороший способ сделать это, используя шаблон торта. В настоящее время я использую что-то вроде этого:

trait ModelContainerComponent {  // Globally scoped dependency
    def model: Model
}

trait SubpaneViewComponent {  // A part of dynamically created cake
    ...
}

trait SubpaneControllerComponent {  // Another part of dynamically created cake
    ...
}

trait DefaultSubpaneViewComponent {  // Implementation
    self: SubpaneControllerComponent with ModelContainerComponent =>
    ...
}

trait DefaultSubpaneControllerComponent {  // Implementation
    self: SubpaneViewComponent with ModelContainerComponent =>
    ...
}

trait SubpaneProvider {  // A component which aids in dynamic subpane creation
    def newSubpane(): Subpane
}

object SubpaneProvider {
    type Subpane = SubpaneControllerComponent with SubpaneViewComponent
}

trait DefaultSubpaneProvider {  // Provider component implementation
    self: ModelContainerComponent =>
    def newSubpane() = new DefaultSubpaneControllerComponent with DefaultSubpaneViewController with ModelContainerComponent {
        val model = self.model  // Pass global dependency to the dynamic cake
    }.asInstanceOf[Subpane]
}

Затем я смешиваю DefaultSubpaneProvider в топке верхнего уровня и внедряю SubpaneProvider во все компоненты, которые должны создавать подпанели.

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

Есть ли более простой/чистый способ сделать это? Как эта проблема разрешена в шаблоне торта?

Ответ 1

Рассматривали ли вы следующие альтернативы:

  • Используйте внутренние классы в Scala, так как они автоматически получают доступ к своим переменным-членам родительского класса.

  • Реструктуризация вашего приложения на основе актера, потому что вы сразу получите выгоду:

    • Иерархия/наблюдение
    • Прослушивание создания/смерти компонентов
    • Правильная синхронизация при доступе к изменяемому состоянию

Скорее всего, будет полезно использовать еще один код для лучшего решения, можете ли вы поделиться компиляционным подмножеством кода?

Ответ 2

Скажем, у нас есть программа, которая имеет только две компоненты: одна содержит бизнес-логику нашей программы, а другая содержит зависимость этой программы, а именно функциональность печати. ​​

имеем:

trait FooBarInterface {
    def printFoo: Unit
    def printBar: Unit
}

trait PrinterInterface {
    //def color: RGB
    def print(s: String): Unit
}

Для ввода логики fooBar шаблон торта определяет:

trait FooBarComponent { 
    //The components being used in this component:
    self: PrinterComponent => 

    //Ways for other components accessing this dependency.    
    def fooBarComp: FooBarInterface

    //The implementation of FooBarInterface 
    class FooBarImpl extends FooBarInterface {
        def printFoo = printComp.print("fOo")
        def printBar = printComp.print("BaR")
    }
}

Обратите внимание, что эта реализация не оставляет ни одного поля не реализованным, и когда дело доходит до смешивания всех этих компонентов вместе, мы имеем:   val fooBarComp = new FooBarImpl. Для случаев, когда у нас есть только одна реализация, нам не нужно оставлять fooBarComp нереализованными. мы можем вместо этого:

trait FooBarComponent { 
    //The components being used in this component:
    self: PrinterComponent => 

    //Ways for other components accessing this dependency.    
    def fooBarComp: new FooBarInterface {
        def printFoo = printComp.print("fOo")
        def printBar = printComp.print("BaR")
    }
}

Не все компоненты похожи на это. Например Printer, необходимо настроить зависимость, используемую для печати foo или bar, и вы хотите иметь возможность печатать текст в разных цветах. Таким образом, зависимость может потребоваться для динамического изменения или установки в какой-то момент в программе.

trait PrintComponent {

    def printComp: PrinterInterface

    class PrinterImpl(val color: RGB) extends PrinterInterface {
        def print(s:String) = ...
    }
}

Для статической конфигурации при смешивании этого компонента мы можем, например, иметь, скажем:

val printComp = PrinterImpl(Blue)

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

trait BazInterface {
    def appendString: String
    def printBar(s: String): Unit
}

и компонент вида:

trait BazComponent { 
    //The components being used in this component:
    self: PrinterComponent => 

    //Ways for other components accessing this dependency.    
    def bazComp(appendString: String) : Baz = new BazImpl(appendString)

    //The implementation of BazInterface 
    class BazImpl(val appendString: String) extends BazInterface {
        def printBaz = printComp.print("baZ" + appendString)
    }
}

Теперь, если бы мы имели компонент FooBarBaz, мы могли бы определить:

trait FooBarBazComponent { 
    //The components being used in this component:
    self: BazComponent with FooBarComponent => 

    val baz = bazComp("***")
    val fooBar = fooBarComp

    //The implementation of BazInterface
    class BazImpl(val appendString: String) extends BazInterface {
        def PrintFooBarBaz = {
            baz.printBaz()
            fooBar.printFooBar()
        }
    }
}

Итак, мы увидели, как можно настроить компонент:

  • статически. (в основном зависимости очень низкого уровня)
  • из другого компонента. (обычно это один бизнес-уровень, настраивающий другой бизнес-уровень, см. "ЗАВИСИМОСТИ, КОТОРЫЕ НУЖДАЮТ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ" здесь)

В этих двух случаях различалось просто место, где происходит конфигурация. Один для низкоуровневых зависимостей на самом верхнем уровне программы, другой - для промежуточного компонента, настроенного внутри другого компонента. Вопрос в том, где должна произойти конфигурация службы, например Print? До сих пор два вопроса, которые мы изучили, не могут быть и речи. Как я вижу, единственными параметрами, которые у нас есть, является добавление компонента-Configurer, который смешивается во всех компонентах, которые нужно настроить, и возвращает компоненты зависимостей, изменяя реализации. Вот простая версия:

trait UICustomiserComponent {
    this: PrintComponent =>

    private var printCompCache: PrintInterface = ???
    def printComp: PrintInterface = printCompCache
}

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