Search code examples
scalaoopdependency-injectionfunctional-programmingreader-monad

Scala: Dependency Injection via Reader and compatibility


When we implement DI via Reader, we make a dependency a part of our method signature. Assume we have (without implementations):

trait Service1 { def f1:Int = ??? }
trait Service2 { def f2:Reader[Service1, Int] = ??? }

type Env= (Service1, Service2)
def c:Reader[Env, Int] = ???  //use Service2.f2 here

Now, f2 needs additional service for implementation, say:

trait Service3
type Service2Env = (Service1, Service3)
//new dependecies on both:
trait Service2 { def f2:Reader[Service2Env, Int] = ??? }

It will break existing clients, they cannot any longer use Service2.f2 without providing Service3 additionally.

With DI via injection (via constructor or setters), which is common in OOP, I would use as a dependency of c only Service2. How it is constructed and what is its list of dependencies, I do not care. From this point, any new dependencies in Service2 will keep the signature of c function unchanged.

How is it solved in FP way? Are there options? Is there a way to inject new dependencies, but somehow protect customers from the change?


Solution

  • Is there a way to inject new dependencies, but somehow protect customers from the change?

    That would kind of defeat the purpose, as using Reader (or alternatively Final Tagless or ZIO Environment) is a way to explicitly declare (direct and indirect) dependencies in the type signature of each function. You are doing this to be able to track where in your code these dependencies are used -- just by looking at a function signature you can tell if this code might have a dramatic side-effect such as, say, sending an email (or maybe you are doing this for other reasons, but the result is the same).

    You probably want to mix-and-match this with constructor-injection for the dependencies/effects that do not need that level of static checking.