简体   繁体   中英

Difference between using Cake pattern and functions in Scala - why is the Cake pattern useful?

I was wondering about the difference between using functions and Cake pattern for DI in Scala. I came up with the following understanding(s), I would like to know if this understanding is correct.

Let's imagine a dependency graph.

1) If we use functions as building blocks then the graph consists of functions as nodes and parameters as edges.

2) If we use traits as building blocks (as in Cake) then the graph consists of traits as nodes and abstract members as edges.

So what is the purpose of Cake pattern ? Why is 2 better than 1 ? It is course graining. Graph-1 can be simplified by grouping functions into traits and then we have a smaller, more understandable Graph-2. The grouping/clustering of related concepts is a form of compression and creates understanding (we need to hold less things in our head to get an understanding).

Here is a different comparison (between Cake vs package system):

Cake is similar to grouping related functions into packages but it goes beyond that because using name-spaces (packages/objects) causes dependencies to be hard-wired, Cake is replacing packages/objects with traits and import s with self type annotations/abstract members. The difference between packages and Cake pattern is that the actual implementation of a dependency can change using Cake while it CANNOT change when using packages.

I don't know if these analogies do make sense or not, if not please correct me, if yes, please reassure me. I am still trying to wrap my head around the Cake pattern and how to relate it to concepts that I already understand (functions, packages).

Dependency injection (DI) is commonly done with getters/setters (which is what I assume you mean by functions) and/or constructor params. The getter/setter approach may look something like this:

trait Logger { 
  // fancy logging stuff 
}

class NeedsALogger {
  private var l: Logger = _
  def logger: Logger = l
  def logger_=(newLogger: Logger) {
    l = newLogger
  }
  // uses a Logger here
}

I don't really like the getter/setter approach. There is no guarantee that the dependency is ever injected. If you use certain DI frameworks, you can mandate something is injected, but then your DI is no longer agnostic to your framework. Now, if you use the constructor approach, the dependency must be provided whenever we instantiate (regardless of framework):

class NeedsALogger(logger: Logger) {
  // uses a Logger here
}

Now, how does the Cake Pattern fit in? First, let's adapt our example to the Cake Pattern:

class NeedsALogger {
  logger: Logger => 
  // Uses a Logger here
}

Let's talk about logger: Logger => . That's a self-type , and it simply brings the members of Logger into scope without having to extend Logger . NeedsALogger is not a Logger , so we don't want to extend it. However, NeedsALogger requires a Logger , and that's what we accomplish with the self-type. We mandate that a Logger must be provided when we create a NeedsALogger . The usage would look like this:

trait FooLogger extends Logger {
  // full implementation of Logger
}

trait BarLogger extends Logger {
  // full implementation of Logger
}

val a = new NeedsALogger with FooLogger
val b = new NeedsALogger with BarLogger
val c = new NeedsALogger // compile-time error! 

As you can see, we accomplish the same thing with either approach. For a lot of DI, the constructor approach will suffice, so you can just pick based on your preference. I personally like self-types and the Cake Pattern, but I see a lot of people avoiding it too.

To keep reading about the Cake Pattern specifically, check this out. It's a good next step if you'd like to know more.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM