scaladependency-injectioncake-pattern

Scala Cake Pattern & Self Type Annotations


I'm trying to follow the example from this blog. I understand the example but having trouble implementing it.

trait Database {
  // ...
}

trait UserDb {
  this: Database =>
    // ...
}

trait EmailService {
  this: UserDb =>
    // Can only access UserDb methods, cannot touch Database methods
}

The example mentions that the full Database functionality will be hidden from the EmailService - this is what i'm after but don't know how to implement these traits correctly.

This is what i tried to implement:

trait Database {
    def find(query: String): String
  }

  trait UserDb {
    this: Database =>
  }

  trait EmailService {
    this: UserDb =>
  }

  trait MongoDatabase extends Database {

  }

  trait MongoUserDb extends UserDb with MongoDatabase{

  }

  class EmailServiceImpl extends EmailService with MongoUserDb {
    override def find(query: String): String = {
      "result"
    }
  }

It looks weird to me becasue MongoDatabase trait didn't asked for find implementation and when i implemented EmailService i was then prompted for find implementation,although the example mentioned this will be hidden from the EmailService. What am i missing here?

After reading your comments, I'm trying to implement what i'm being trying to understand on an example that is closer to what i'm actually trying to do.

The first snippet won't compile, but the second one will... At the end of the day i want to have different Repository implementations where i can switch between the Databases they rely on, am i close with one of the snippets below?

trait Database {
    def find(s: String): String
  }

  trait Repository {
    this: Database =>
  }

  class UserRepository extends Repository {
    def database = new MongoDB

    class MongoDB extends Database {
      def find(s: String): String = {
        "res"
      }
    }
  }


trait Repository {
    def database: Database

    trait Database {
      def find(s: String): String
    }
  }

  trait UserRepository extends Repository {
    def database = new MongoDB

    class MongoDB extends Database {
      def find(s: String): String = {
        "res"
      }
    }
  }

Solution

  • As mentioned MongoUserDB will not ask for an implementation as its a trait. However since EmailServiceImpl extends the trait it needs to provide an implementation. What you are looking for could be done by adding another abstraction. I do it using the service and DAO architecture. Below is a working example that you may use to see if it suits you.

    //All future versions of DAO will extend this
    trait AbstractDAO{
      def getRecords:String
      def updateRecords(records:String):Unit
    }
    //One concrete version
    trait concreteDAO extends AbstractDAO{
      override def getRecords={"Here are DB records"}
      override def updateRecords(records:String){
        //Actual DB calls and operations
        println("Updated "+records)
      }
    }
    //Second concrete version
    trait concreteDAO1 extends AbstractDAO{
      override def getRecords={"DB Records returned from DAO2"}
      override def updateRecords(records:String){
        //Actual DB calls and operations
        println("Updated via DAO2"+records)
      }
    }
    //This trait just defines dependencies (in this case an instance of AbstractDAO) and defines operations based over that
    trait service{
      this:AbstractDAO =>
    
      def updateRecordsViaDAO(record:String)={  
      updateRecords(record) 
      }
      def getRecordsViaDAO={
      getRecords
      }
    }
    
    //Test Stub
    object DI extends App{
      val wiredObject = new service with concreteDAO //injecting concrete DAO to the service and calling methods
      wiredObject.updateRecords("RECORD1")
      println(wiredObject.getRecords)
    
      val wiredObject1 = new service with concreteDAO1
      wiredObject1.updateRecords("RECORD2")
      println(wiredObject1.getRecords)
    
    }
    

    EDIT ---

    Here is the code you might want to implement,

        trait Database {
        def find(s: String): String
      }
    
    trait MongoDB extends Database{
      def find(s:String):String = { "Test String" }
    }
    trait SQLServerDB extends Database{
      def find(s:String):String = { "Test String2" }
    }
    
      trait Repository {
        this: Database =>
      }
    
      class UserRepository extends Repository with MongoDB{  //  UserRepository is injected with MongoDB here
        find("call MongoDB") //This call will go to the find method in MongoDB trait
      }
    
      class UserRepository1 extends Repository with SQLServerDB{  //  UserRepository is injected with SQLServerDB here
        find("call SQLServerDB") //This call will go to the find method in SQLServerDB trait
      }