cake pattern - why is it so complicated

656 views Asked by At

I am trying to learn about the cake pattern.

I am reading this blog about it.

The example code from that blog is:

case class User (name:String,email:String,supervisorId:Int,firstName:String,lastName:String)

trait UserRepository {
  def get(id: Int): User
  def find(username: String): User
}
trait UserRepositoryComponent {

  def userRepository: UserRepository

  trait UserRepository {
    def get(id: Int): User
    def find(username: String): User
  }
}
trait Users {
  this: UserRepositoryComponent =>

  def getUser(id: Int): User = {
    userRepository.get(id)
  }

  def findUser(username: String): User = {
    userRepository.find(username)
  }
}
trait UserInfo extends Users {
  this: UserRepositoryComponent =>

  def userEmail(id: Int): String = {
    getUser(id).email
  }

  def userInfo(username: String): Map[String, String] = {
    val user = findUser(username)
    val boss = getUser(user.supervisorId)
    Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
  }
}
trait UserRepositoryComponentImpl extends UserRepositoryComponent {

  def userRepository = new UserRepositoryImpl

  class UserRepositoryImpl extends UserRepository {

    def get(id: Int) = {
      ???
    }

    def find(username: String) = {
      ???
    }
  }
}

object UserInfoImpl extends
    UserInfo with
    UserRepositoryComponentImpl

I can simplify that code by removing Users:

package simple {

  case class User(name: String, email: String, supervisorId: Int, firstName: String, lastName: String)

  trait UserRepository {
    def get(id: Int): User

    def find(username: String): User
  }

  trait UserRepositoryComponent {

    def userRepository: UserRepository

    trait UserRepository {
      def get(id: Int): User

      def find(username: String): User
    }

  }

  trait UserInfo {
    this: UserRepositoryComponent =>

    def userEmail(id: Int): String = {
      userRepository.get(id).email
    }

    def userInfo(username: String): Map[String, String] = {
      val user = userRepository.find(username)
      val boss = userRepository.get(user.supervisorId)
      Map(
        "fullName" -> s"${user.firstName} ${user.lastName}",
        "email" -> s"${user.email}",
        "boss" -> s"${boss.firstName} ${boss.lastName}"
      )
    }
  }

  trait UserRepositoryComponentImpl extends UserRepositoryComponent {

    def userRepository = new UserRepositoryImpl

    class UserRepositoryImpl extends UserRepository {

      def get(id: Int) = {
        ???
      }

      def find(username: String) = {
        ???
      }
    }

  }

  object UserInfoImpl extends
    UserInfo with
    UserRepositoryComponentImpl

}

and it compiles just fine.

1) Why is the code in the blog so complicated ?

2) Is that the idiomatic way to use the cake pattern ?

3) Why is the Users class needed in this example ?

4) Is that the way the cake pattern supposed to look like (with that seemingly unnecessary Users class?

5) Or is the simplified version just fine ?

1

There are 1 answers

2
Paweł Jurczenko On BEST ANSWER
  1. At first it might look like it's complicated, but once you get familiar with that pattern it's just... boilerplaty and cumbersome. For every service of yours you have to create an accompanying component that will be wrapping that service. So in the provided example you have a UserRepository that's wrapped by a UserRepositoryComponent. And this is only the abstraction, so you need to have a concrete implementation for both the component and the service (i.e. UserRepositoryComponentImpl wrapping UserRepositoryImpl). And so far you have only one service that might be used in your modules, imagine the effort to create dozens of services ;)

  2. Yes, this is the idiomatic way to use that pattern. However there are also other variations of that pattern, e.g. thin cake pattern or parfait (a term coined by Dick Wall)

  3. You're asking about User, yet your code simplification was to remove Users, so I'll describe both of them. User is a simple case class, that should make that example more accessible/easier to grasp. Users however were not necessary here (it's just another intermediate level of abstraction), and in my opinion they bring some unnecessary noise to the example.

  4. I would say that your simplified version shows exactly how cake pattern should look like. You have an abstract UserRepository wrapped inside of UserRepositoryComponent, you have a concrete implementation of both those traits, and you have some service (UserInfo) that requires the repository of users (which is "injected" using self-type annotation).

  5. Already answered.