Why use scala's cake pattern rather than abstract fields?
Asked Answered
S

4

25

I have been reading about doing Dependency Injection in scala via the cake pattern. I think I understand it but I must have missed something because I still can't see the point in it! Why is it preferable to declare dependencies via self types rather than just abstract fields?

Given the example in Programming Scala TwitterClientComponent declares dependencies like this using the cake pattern:

//other trait declarations elided for clarity
...

trait TwitterClientComponent {

  self: TwitterClientUIComponent with
        TwitterLocalCacheComponent with
        TwitterServiceComponent =>

  val client: TwitterClient

  class TwitterClient(val user: TwitterUserProfile) extends Tweeter {
    def tweet(msg: String) = {
      val twt = new Tweet(user, msg, new Date)
      if (service.sendTweet(twt)) {
        localCache.saveTweet(twt)
        ui.showTweet(twt)
      }
    }
  }
}

How is this better than declaring dependencies as abstract fields as below?

trait TwitterClient(val user: TwitterUserProfile) extends Tweeter {
  //abstract fields instead of cake pattern self types
  val service: TwitterService
  val localCache: TwitterLocalCache
  val ui: TwitterClientUI

  def tweet(msg: String) = {
    val twt = new Tweet(user, msg, new Date)
    if (service.sendTweet(twt)) {
      localCache.saveTweet(twt)
      ui.showTweet(twt)
    }
  }
}

Looking at instantiation time, which is when DI actually happens (as I understand it), I am struggling to see the advantages of cake, especially when you consider the extra keyboard typing you need to do for the cake declarations (enclosing trait)

    //Please note, I have stripped out some implementation details from the 
    //referenced example to clarify the injection of implemented dependencies

    //Cake dependencies injected:
    trait TextClient
        extends TwitterClientComponent
        with TwitterClientUIComponent
        with TwitterLocalCacheComponent
        with TwitterServiceComponent {


      // Dependency from TwitterClientComponent:
      val client = new TwitterClient

      // Dependency from TwitterClientUIComponent:
      val ui = new TwitterClientUI

      // Dependency from TwitterLocalCacheComponent:
      val localCache = new TwitterLocalCache 

      // Dependency from TwitterServiceComponent
      val service = new TwitterService
    }

Now again with abstract fields, more or less the same!:

trait TextClient {
          //first of all no need to mixin the components

          // Dependency on TwitterClient:
          val client = new TwitterClient

          // Dependency on TwitterClientUI:
          val ui = new TwitterClientUI

          // Dependency on TwitterLocalCache:
          val localCache = new TwitterLocalCache 

          // Dependency on TwitterService
          val service = new TwitterService
        }

I'm sure I must be missing something about cake's superiority! However, at the moment I can't see what it offers over declaring dependencies in any other way (constructor, abstract fields).

Salesin answered 10/8, 2011 at 13:59 Comment(2)
Note, that the book you are referring to is called "Programming Scala" not "Programming in Scala", which also exists.Labyrinthodont
I think that service is coming from the TwitterServiceComponent, but I'm not sure how we know that it's named service?Krissie
M
8

Traits with self-type annotation is far more composable than old-fasioned beans with field injection, which you probably had in mind in your second snippet.

Let's look how you will instansiate this trait:

val productionTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with TwitterConnection

If you need to test this trait you probably write:

val testTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with MockConnection

Hmm, a little DRY violation. Let's improve.

trait TwitterSetup extends TwitterClientComponent with TwitterUI with FSTwitterCache
val productionTwitter = new TwitterSetup with TwitterConnection
val testTwitter = new TwitterSetup with MockConnection

Furthermore if you have a dependency between services in your component (say UI depends on TwitterService) they will be resolved automatically by the compiler.

Mariomariology answered 10/8, 2011 at 19:49 Comment(2)
how would that be different from constructor injection? you can have an interface for the connection and have two implementationsLatreshia
I guess the idea is that you can name certain combinations of dependencies. So you can abstract away on certain combinations of dependencies. Right ? But I am wondering is there anything more to it?Sensitometer
P
7

Think about what happens if TwitterService uses TwitterLocalCache. It would be a lot easier if TwitterService self-typed to TwitterLocalCache because TwitterService has no access to the val localCache you've declared. The Cake pattern (and self-typing) allows for us to inject in a much more universal and flexible manner (among other things, of course).

Prostitute answered 10/8, 2011 at 14:30 Comment(3)
Yes, that's true, but in your example, you are only avoiding one more field assignment via cake than by abstract fields. I think you meant to write 'TwitterService has no access to val localCache'. If so, using abstract fields again, TwitterService would also declare a 'val localCache: TwitterLocalCache' which would also need assignment at instantiation time. It could be the same instance as the TwitterClient instance or another depending on what you require of course.Salesin
Ah, yes, sorry... I'll have to fix that. But I don't think you're really seeing the issue. Yes, TwitterService could declare a localCache value, but it may not have made it abstract, nor even made it protected (i.e. could be private), so TwitterClient would have to have its own reference to it, as does the TwitterService. Ugh... we could go on and on here, but the bottom line is that the standard "Java Injection" concept leaves me wanting in a big, big way and self-typing is uniform and simple.Prostitute
I'm not sure how that's really an issue though, if you simply move the implementation of the abstract to traits there's no problems with the dependency chain.Coffeng
S
1

I was unsure how the actual wiring would work, so I've adapted the simple example in the blog entry you linked to using abstract properties like you suggested.

// =======================  
// service interfaces  
trait OnOffDevice {  
  def on: Unit  
  def off: Unit  
}  
trait SensorDevice {  
  def isCoffeePresent: Boolean  
}  

// =======================  
// service implementations  
class Heater extends OnOffDevice {  
  def on = println("heater.on")  
  def off = println("heater.off")  
}  
class PotSensor extends SensorDevice {  
  def isCoffeePresent = true  
}  

// =======================  
// service declaring two dependencies that it wants injected  
// via abstract fields
abstract class Warmer() {
  val sensor: SensorDevice   
  val onOff: OnOffDevice  

  def trigger = {  
    if (sensor.isCoffeePresent) onOff.on  
    else onOff.off  
  }  
}  

trait PotSensorMixin {
    val sensor = new PotSensor
}

trait HeaterMixin {
    val onOff = new Heater  
}

 val warmer = new Warmer with PotSensorMixin with HeaterMixin
 warmer.trigger 

in this simple case it does work (so the technique you suggest is indeed usable).

However, the same blog shows at least other three methods to achieve the same result; I think the choice is mostly about readability and personal preference. In the case of the technique you suggest IMHO the Warmer class communicates poorly its intent to have dependencies injected. Also to wire up the dependencies, I had to create two more traits (PotSensorMixin and HeaterMixin), but maybe you had a better way in mind to do it.

Sarad answered 10/8, 2011 at 15:14 Comment(2)
Yup, that's the general idea. You don't need the two extra traits though. You can just: val warmer = new Warmer { val sensor = new PotSensor; val onOff = new Heater }. This instantiation is the 'dependency injection' point.Salesin
@Noel: you're right about the extra traits. I thought you wanted to retain the idea of injecting mixins (but in this case, I agree it doesn't make a lot of sense). So basically this is a scala version of java's "constructor injection vs setter injection" where you have many more options to choose from...Sarad
W
1

In this example I think there is no big difference. Self-types can potentially bring more clarity in cases when a trait declares several abstract values, like

trait ThreadPool {
  val minThreads: Int
  val maxThreads: Int
}

Then instead of depending on several abstract values you just declare dependency on a ThreadPool. Self-types (as used in Cake pattern) for me are just a way to declare several abstract members at once, giving those a convenient name.

Woothen answered 10/10, 2013 at 21:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.