Scala cake pattern for objects with different lifetimes
Asked Answered
C

2

10

I tried to use cake pattern in my project and liked it very much, but there is one problem which bothers me.

Cake pattern is easy to use when all your components have the same lifetime. You just define multiple traits-components, extend them by traits-implementation and then combine these implementations within one object, and via self-types all dependencies are automatically resolved.

But suppose you have a component (with its own dependencies) which can be created as a consequence of user action. This component cannot be created at the application startup because there is no data for it yet, but it should have automatic dependency resolution when it is created. An example of such components relationship is main GUI window and its complex subitems (e.g. a tab in notebook pane) which are created on user request. Main window is created on application startup, and some subpane in it is created when user performs some action.

This is easily done in DI frameworks like Guice: if I want multiple instances of some class I just inject a Provider<MyClass>; then I call get() method on that provider, and all dependencies of MyClass are automatically resolved. If MyClass requires some dynamically calculated data, I can use assisted inject extension, but the resulting code still boils down to a provider/factory. Related concept, scopes, also helps.

But I cannot think of a good way to do this using cake pattern. Currently I'm using something like this:

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]
}

Then I mix DefaultSubpaneProvider in my top-level cake and inject SubpaneProvider in all components which need to create subpanes.

The problem in this approach is that I have to manually pass dependencies (model in ModelContainerComponent) down from the top-level cake to the dynamically created cake. This is only a trivial example, but there can be more dependencies, and also there can be more types of dynamically created cakes. They all require manual passing of dependencies; moreover, simple change in some component interface can lead to massive amount of fixes in multiple providers.

Is there a simpler/cleaner way to do this? How is this problem resolved within cake pattern?

Coben answered 8/7, 2013 at 11:53 Comment(6)
If my answer doesn't address your question, I'll re-delete it.Rosamariarosamond
What about something likePensile
trait ModelContainerComponentProxy extends ModelContainerComponent { def originalModelContainer: ModelContainerComponentProxy; def model = originalModelContainer.model} -- that could solve at least problem of passing all component contents explicitly.Pensile
How about using cake with DI framework like MacWire? github.com/adamw/macwireDuiker
Why do you need the cast? Can't you write def newSubpane():SubPane = {}Bankruptcy
@Edmondo1984, as far as I remember, the cast was necessary for IDEA to understand the code. It is likely it is not needed in the most recent versions of it.Coben
B
0

Have you considered the following alternatives:

  • Use inner classes in Scala, as they automatically have access to their parent class member variables.

  • Restructuring your application in an actor based one, because you will immediately benefit of:

    • Hierarchy / supervision
    • Listening for creation / death of components
    • Proper synchronization when it comes to access mutable state

It will probably be helpful having some more code to provide a better solution, can you share a compiling subset of your code?

Bankruptcy answered 26/12, 2014 at 10:16 Comment(3)
It is impossible to use inner classes for dependency injection because they are statically tied to their enclosing classes (I also don't really understand how do you think inner classes and cake pattern are related in this regard), and actors can't be used with GUI code - I personally don't know any GUI library which is based on actors. I'm also pretty sure that the code I've provided already describes the problem, and I can't provide more now - I worked on that project a year and a half ago :)Coben
Let's discuss it further offline, are you available on google chat? edmondo . porcu at gmail.comBankruptcy
I'm afraid the question is obsolete now. I'm working on other projects and I don't really need an answer anymore :)Coben
A
0

Let's say we have a program that has only two components: one contains the business logic of our program and the other one contains the dependency of this program, namely printing functionality.

we have:

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

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

For injecting the fooBar logic, the cake-pattern defines:

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")
    }
}

Note that this implementation does not leave any field unimplemented and when it comes to mixing all these components together, we would have: val fooBarComp = new FooBarImpl. For the cases where we only have one implementation, we don't have to leave fooBarComp unimplemented. we can have instead:

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")
    }
}

Not all components are like this. For example Printer, the dependency used for printing foo or bar needs to be configured and you want to be able to print text in different colours. So the dependency might be needed to change dynamically, or set at some point in the program.

trait PrintComponent {

    def printComp: PrinterInterface

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

For a static configuration, when mixing this component, we could for example have, say:

val printComp = PrinterImpl(Blue)

Now, the fields for accessing the dependencies do not have to be simple values. They can be functions that take some of the constructor parameters of the dependency implementation to return an instance of it. For instance, we could have Baz with the interface:

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

and a component of the form:

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)
    }
}

Now, if we had the FooBarBaz component, we could define:

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()
        }
    }
}

So we have seen how a component can be configured:

  • statically. (mostly the very low level dependencies)
  • from inside another component. (usually it's one business layer configuring another business layer, see "DEPENDENCIES THAT NEED USER DATA " in here)

What differed in these two cases is simply the place where the configuration is taking place. One is for the low level dependencies at the very top level of the program, the other is for an intermediate component being configured inside another component. Question is, where should the configuration for a service like Print take place? The two options we have explored so far are out of the question. The way I see it, the only options we have is adding a Components-Configurer that mixes in all the components to be configured and returns the dependency components by mutating the implementations. Here is a simple version:

trait UICustomiserComponent {
    this: PrintComponent =>

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

obviously we can have multiple such configurer components and do not have to have only one.

Adelia answered 1/10, 2017 at 21:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.