Repositories
In this part we will learn how to build repository around "traits as modules". Also in the second part we will see how dependency injection can be implemented using **MonadReader.**
Designing repository
Following idea is taken from the book : https://www.manning.com/books/functional-and-reactive-domain-modeling .
First we create abstract representation of general Repository
trait Repository[A, IdType] {
def query(id: IdType): Try[A] //or Try[Option[A]]
def store(a: A): Try[A]
}
Then we create another abstract sub-representation but this time with domain knowledge
trait MeetupRepository extends Repository[Meetup,Topic]{
def store(m:Meetup) : Try[Meetup]
def startsOn(when:Date) : Seq[Meetup]
def where(topic:Topic) : Try[Address] = query(topic) match { //or just query(topic).map(_.topic)
case Success(meetup) => Success(meetup.topic)
case Failure(ex) => Failure(ex)
}
}
Notice that domain operations are based on still abstract primary operations. Finally we create concrete technical implementation of base operations.
object MeetupRepositoryInMemory extends MeetupRepository{
private var data:Map[Topic,Meetup]= ...
override def store(m: Meetup): Try[Meetup] = {
data = data + (m.topic -> m)
Success(m)
}
override def startsOn(when: Date): Seq[Meetup] = data.values.filter(_.date==when).toSeq
override def query(id: Topic): Try[Meetup] = Try{
data.getOrElse(id, throw new RuntimeException(s"missing meetup with topic $id"))
}
}
Dependency Injection
It is important to not see following examples like "one and only way of Functional Dependency Injection" but just an alternative, a tool which can be used if context is correct for this usage.
First observation - modular organization of repositories based on traits can not have state because it is current limitation of language. Even if this would be possible we may not want to set state to early to have better elasticity.
So what we can do? First idea may be to inject repository to each operation
trait MeetupeService1 {
def draft(topic: Topic,address: Address,repo : MeetupRepository) : Try[Meetup]
def schedule(topic : Topic, date:Date,repo : MeetupRepository) : Try[Meetup]
def cancel(topic : Topic,repo : MeetupRepository) : Unit
}
object ProgramUsingNonComposableMeetupService {
val repo = MeetupRepositoryInMemory
val service = NonComposableMeetupService
def schedule(topic:Topic,address: Address,date:Date): Try[Meetup] = for{
_ <- service.draft(topic,address,repo)
meetup <- service.schedule(topic,date,repo)
} yield meetup
}
Reader Monad
One of possible solutions is to actually not return specific type but function which will receive injected arguments.
def operation(...) : Repository => Result
We can actually move one step further and use specific representation of this concept called Reader
Reader[A,B]
We will explain this more deeply during workshops . Generally the idea is that becase Reader is a Monad then you have to your disposal map and flatMap which allow you to join computation without passing actual repository
val result:Reader[Dependency,B]=for{
a <- operation() // Reader[Dependency,A]
_ <- save() // Reader[Dependency,A]
} yield f(a)
Exercises
EXERCISES :
Exercise1
We have very simple domain where an Account identified by an id store certain amount of money.
type Money = Double
case class Account (id:Int, amount: Money)
Similarly like in demo there is an abstract concept of Repository
trait Repository[A, IdType] {
def query(id: IdType): Try[A] //or Try[Option[A]]
def store(a: A): Try[A]
}
there is a domain module where you need to implement a method
//EXERCISE1
def addMoney(id:Int, howMuch:Money) : Try[Account] = ???
notice that you have lens to your disposal
lazy val moneyLens=GenLens[Account](_.amount)
then implement concrete repository methods which will use "InMemoryStorage"
//EXERCISE1
override def store(a: Account): Try[Account] = ???
//EXERCISE1
override def query(id: Int): Try[Account] = ???
Exercise2
Implement custom reader
case class MyCustomReader[R, A](run: R => A) {
def map[B](f: A => B): MyCustomReader[R, B] = ???
def flatMap[B](f: A => MyCustomReader[R, B]): MyCustomReader[R, B] = ???
}
Exercise3
Implement service operations with you reader. This should give you enough intuition how this works
//EXERCISE3
private def addMoney(id:Int,howMuch:Money): MyCustomReader[AccountRepository,Try[Account]] = MyCustomReader{r=>
???
}
//EXERCISE3
private def borrowMoney(id:Int,howMuch:Money): MyCustomReader[AccountRepository,Try[Account]] = ???
//EXERCISE3
private def update(account: Account) : MyCustomReader[AccountRepository,Try[Account]] = ???