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.**

DEMO FILE : https://github.com/PawelWlodarski/workshops/blob/master/src/main/scala/jug/lodz/workshops/modeling/storage/RepositoryDemo.scala

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 :

https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/modeling/storage/exercises/RepositoryExercises.scala

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]] = ???

results matching ""

    No results matching ""