I am looking to understand what are the best practices for using the ZIO environment, i.e. the R type in ZIO[R, E, A].
In this tweet, John De Goes gives 3 simple rules for when to use the environment R.
- Methods inside service definitions (traits) should NEVER use the environment
- Service implementations (classes) should accept all dependencies in constructor
- All other code (‘business logic’) should use the environment to consume services
The ZIO home page also gives an example that seems to follow this advice.
My confusion is mostly around what does ZIO consider “services” and what is “business logic”. In the ZIO home page example linked to above, the “business logic” is something like:
val app =
for {
id <- DocRepo.save(...)
doc <- DocRepo.get(id)
_ <- DocRepo.delete(id)
} yield ()
This is a bit too simple to get the point across for me. This “application” fits completely into a single function that uses the abstract repository. A more realistic case would be that this logic is for example part of the business logic to handle some request, for example DocumentHttpServer#handleFoo
method. But in that case is DocumentHttpServer
not itself a “service”? If so, shouldn’t the DocRepo
be passed in as a constructor argument and shouldn’t handleFoo
use it directly?
For clarity, the way I understand this DocumentHttpServer would be implemented is:
trait DocumentHttpServer {
def handleFoo(args: FooArgs): Task[Unit] // this is a service so should have R=Any
}
class DocumentHttpServerImpl(repo: DocRepo) extends documentHttpServer {
def handleFoo(args: FooArgs) =
for {
id <- repo.save(...) // using the constructor argument to match R=Any in trait
doc <- repo.get(id)
_ <- repo.delete(id)
} yield ()
}
In conclusion, am I wrong to assume that “business logic” is described in some “ZIO service” and if so, its interface should not use the environment?