Cake pattern: one component per implementation, or one component per trait?
Asked Answered
A

1

4

I'm currently working to use the cake pattern on my application.

On exemples I have found across the web the exemples are kind of basic but doesn't involve more complex needs. What I'd like to do is not so fancy: I would like to have inside a cake pattern application, 2 services of the same type, using different implementations.

trait UserServiceComponent {
  self: UserRepositoryComponent =>
  val userService: UserService

  class DefaultUserService extends UserService {
    def getPublicProfile(id: String): Either[Error, User] = userRepository.getPublicProfile(id)
  }

  class AlternativeUserService extends UserService {
    def getPublicProfile(id: String): Either[Error, User] = call webservice here for exemple...
  }
}

trait UserService extends RepositoryDelegator[User] {
  def getPublicProfile(id: String): Either[Error, User]
}

It works fine if I use one implementation of the UserService at a time, but if I need both implementations in the same time, I don't really know how to do it.

Should I create 2 distinct components? Each one exposing a different userService value name? (defaultUserService/alternativeUserService). Using one component for both implementation I don't know how other components would be able to know which implementation is used when using the name userService since there are 2 distinct implementations in my application.

By the way, as the component expresses the dependency to the UserRepositoryComponent, while it is not needed by all implementations, I find it a bit weird to have only one component right? Imagine I don't want to build the full application which needs both implementations, but I need, for tests, to build only the AlternativeUserService which doesn't need the UserRepositoryComponent, it would be weird to have to provide this dependency as it will not be used.

Can someone give me some advices so that I know what to do?

Kind of related question: Cake pattern: how to get all objects of type UserService provided by components

Thanks

Azotobacter answered 26/1, 2013 at 0:16 Comment(0)
O
8

First things first, you should decouple the UserServiceComponent from the implementations of UserService:

trait UserService extends RepositoryDelegator[User] {
  def getPublicProfile(id: String): Either[Error, User]
}

trait UserServiceComponent {
  val userService: UserService
}

trait DefaultUserServiceComponent extends UserServiceComponent { self: UserRepositoryComponent =>
  protected class DefaultUserService extends UserService {
    def getPublicProfile(id: String): Either[Error, User] = userRepository.getPublicProfile(id)
  }
  val userService: UserService = new DefaultUserService
}

trait AlternativeUserServiceComponent extends UserServiceComponent {
  protected class AlternativeUserService extends UserService {
    def getPublicProfile(id: String): Either[Error, User] = call webservice here for exemple...
  }
  val userService: UserService = new AlternativeUserService
}

If that looks verbose, well it is. The cake pattern is not particularly concise.

But notice how it solves your problem about having a dependency to UserRepositoryComponent even when not actually required (such as when only using AlternativeUserService).

Now, all we have to do when instantiating the application is to mix either DefaultUserServiceComponent or AlternativeUserServiceComponent.

If you happen to need to access to both implementations, you should indeed expose two userService value names. Well in fact, 3 names, such as:

  • defaultUserService for the DefaultUserService implementation
  • alternativeUserService for the AlternativeUserService implementation
  • mainUserService for any UserService implementation (the application chooses which one at "mix time").

By example:

trait UserService extends RepositoryDelegator[User] {
  def getPublicProfile(id: String): Either[Error, User]
}

trait MainUserServiceComponent {
  val mainUserService: UserService
}

trait DefaultUserServiceComponent { self: UserRepositoryComponent =>
  protected class DefaultUserService extends UserService {
    def getPublicProfile(id: String): Either[Error, User] = userRepository.getPublicProfile(id)
  }
  val defaultUserService: UserService = new DefaultUserService
}

trait AlternativeUserServiceComponent {
  protected class AlternativeUserService extends UserService {
    def getPublicProfile(id: String): Either[Error, User] = ??? // call webservice here for exemple...
  }
  val alternativeUserService: UserService = new AlternativeUserService
}

Then you can instantiate your cake like this:

object MyApp 
  extends MainUserServiceComponent 
  with DefaultUserServiceComponent 
  with AlternativeUserServiceComponent 
  with MyUserRepositoryComponent // Replace with your real UserRepositoryComponent here    
{
  //val userService = defaultUserService
  val mainUserService = alternativeUserService
}

In the above example, services that explicitly want to access the DefaultUserService would put DefaultUserServiceComponent as a dependecy of their component (same for AlternativeUserService and AlternativeUserServiceComponent), and services that just need some UserService would instead put MainUserServiceComponent as a dependency. You decide at "mix time" which service mainUserService points to (here, it points to the DefaultUserService implementation.

Ockham answered 26/1, 2013 at 12:14 Comment(4)
Nice thanks! By the way, should I always instantiate the service inside the component like you did? Or at mix time? What are the pros and cons? For MainUserServiceComponent I guess you couldn't instantiate it with the default because it would require the dependency to the DefaultUserServiceComponent right?Azotobacter
Yes, in general you should instantiate (or in any case implement the method returning the service) the services in the component, as their raison d'être is precisely to provide a specific service instance. For MainUserServiceComponent I did it when wiring everything up mainly by convenience, but I could very well have adhered to the cake pattern here too by defining DefaultAsMainUserServiceComponent extends MainUserServiceComponent { _: DefaultUserServiceComponent => val mainUserService = defaultUserService } (and same thing with an AlternateAsMainUserServiceComponent trait).Sylvia
+1, Good answer! @SebastienLorber One pro of instantiating all of your services in MyApp also includes having a single spot that configures the module, similar to a Guice module. In the end it's more or less a stylistic concern.Tessin
That's what I thought. In the end I start to understand pretty well this cake thing :) thanks allAzotobacter

© 2022 - 2024 — McMap. All rights reserved.