scalaslickcake-pattern

Scala Slick Cake Pattern: over 9000 classes?


I'm developing a Play! 2.2 application in Scala with Slick 2.0 and I'm now tackling the data access aspect, trying to use the Cake Pattern. It seems promising but I really feel like I need to write a huge bunch of classes/traits/objects just to achieve something really simple. So I could use some light on this.

Taking a very simple example with a User concept, the way I understand it is we should have:

case class User(...) //model

class Users extends Table[User]... //Slick Table

object users extends TableQuery[Users] { //Slick Query
//custom queries
}

So far it's totally reasonable. Now we add a "Cake Patternable" UserRepository:

trait UserRepository {
 val userRepo: UserRepository
 class UserRepositoryImpl {
    //Here I can do some stuff with slick
    def findByName(name: String) = {
       users.withFilter(_.name === name).list
    }
  }
}

Then we have a UserService:

trait UserService {
 this: UserRepository =>
val userService: UserService
 class UserServiceImpl { //
    def findByName(name: String) = {
       userRepo.findByName(name)
    }
  }
}

Now we mix all of this in an object :

object UserModule extends UserService with UserRepository {
    val userRepo = new UserRepositoryImpl
    val userService = new UserServiceImpl 
}
  1. Is UserRepository really useful? I could write findByName as a custom query in Users slick object.

  2. Let's say I have another set of classes like this for Customer, and I need to use some UserService features in it.

Should I do:

CustomerService {
this: UserService =>
...
}

or

CustomerService {
val userService = UserModule.userService
...
}

Solution

  • OK, those sound like good goals:

    You could do something like this:

    trait UserRepository {
        type User
        def findByName(name: String): User
    }
    
    // Implementation using Slick
    trait SlickUserRepository extends UserRepository {
        case class User()
        def findByName(name: String) = {
            // Slick code
        }
    }
    
    // Implementation using Rough
    trait RoughUserRepository extends UserRepository {
        case class User()
        def findByName(name: String) = {
            // Rough code
        }
    }
    

    Then for CustomerRepository you could do:

    trait CustomerRepository { this: UserRepository =>
    }
    
    trait SlickCustomerRepository extends CustomerRepository {
    }
    
    trait RoughCustomerRepository extends CustomerRepository {
    }
    

    And combine them based on your backend whims:

    object UserModuleWithSlick
        extends SlickUserRepository
        with SlickCustomerRepository
    
    object UserModuleWithRough
        extends RoughUserRepository
        with RoughCustomerRepository
    

    You can make unit-testable objects like so:

    object CustomerRepositoryTest extends CustomerRepository with UserRepository {
        type User = // some mock type
        def findByName(name: String) = {
            // some mock code
        }
    }
    

    You are correct to observe that there is a strong similarity between

    trait CustomerRepository { this: UserRepository =>
    }
    
    object Module extends UserRepository with CustomerRepository
    

    and

    trait CustomerRepository {
        val userRepository: UserRepository
        import userRepository._
    }
    
    object UserModule extends UserRepository
    object CustomerModule extends CustomerRepository {
        val userRepository: UserModule.type = UserModule
    }
    

    This is the old inheritance/aggregation tradeoff, updated for the Scala world. Each approach has advantages and disadvantages. With mixing traits, you will create fewer concrete objects, which can be easier to keep track of (as in above, you only have a single Module object, rather than separate objects for users and customers). On the other hand, traits must be mixed at object creation time, so you couldn't for example take an existing UserRepository and make a CustomerRepository by mixing it in -- if you need to do that, you must use aggregation. Note also that aggregation often requires you to specify singleton-types like above (: UserModule.type) in order for Scala to accept that the path-dependent types are the same. Another power that mixing traits has is that it can handle recursive dependencies -- both the UserModule and the CustomerModule can provide something to and require something from each other. This is also possible with aggregation using lazy vals, but it is more syntactically convenient with mixing traits.