P.S. example is in kind-of-scala, but language not really matter, I am interesting in functional approach in a whole.
Usually I saw pattern like this
outer world -> controller -> serviceA -> serviceB -> DB Accessor
So we got layers where function of outer layer calls function of inner layer. (I drop IO monad wrapping for simplicity)
class Controller(service: ServiceA) {
def call(req): res {
val req1 = someWorkBefore(req)
val res1 = service.call(req1)
someWorkAfter(res1)
}
private someWorkBefore(req): req1
private someWorkAfter(res1): res
}
class ServiceA(service: ServiceB) {
def call(req1): res1 {
val req2 = someWorkBefore(req1)
val res2 = service.call(req2)
someWorkAfter(res2)
}
private someWorkBefore(req1): req2
private someWorkAfter(res2): res1
}
class ServiceB(db: DBAccessor) {
def call(req2): res2 {
val req3 = someWorkBefore(req2)
val res3 = service.call(req3)
someWorkAfter(res3)
}
private someWorkBefore(req2): req3
private someWorkAfter(res3): res2
}
The problem I see here is all functions are "not pure" and to write a test of some component one should make a mock of it's inner component, whitch is not good in my opinion.
Other option is to forget about separation of concerns in some way, and put every thing in one place. (I drop IO monad wrapping for simplicity)
class Controller(serviceA: ServiceA, serviceB: ServiceB, db: DBAccessor) {
def call(req): res = {
val req1 = someWorkBefore(req)
val req2 = serviceA.someWorkBefore(req1)
val req3 = serviceB.someWorkBefore(req2)
val res3 = db.call(req3)
val res2 = serviceB.someWorkAfter(res3)
val res1 = serviceA.someWorkAfter(res2)
someWorkAfter(res1)
}
private someWorkBefore(req): req1
private someWorkAfter(res1): res
}
Which looks better because every function in services is pure in some way and do not depends on other stuff that should be mocked, but function of Controller
is now a mess.
What other options of architecture could be considered?
Indeed, Dependency Injection makes everything impure, so that kind of object-oriented architecture doesn't work if you want to implement a functional architecture.
What to do instead depends on exactly which language you're using, and the idioms therein. In Haskell, for example, free monads may be one way to address such concerns, but while free monads are also technically possible in F#, I wouldn't consider them idiomatic in that context.
Since web applications are a kind of interactive software I usually find them well-suited for the Functional Core, Imperative Shell architecture. Another metaphor is to view that kind of architecture as a sandwich. Do impure actions, call a pure function, do some more impure actions, and exit.
Over the years, I've written quite extensively on this topic. As one example, other readers have found the following example useful: Refactoring registration flow to functional architecture.
In short, however, in functional programming (at least all examples I've seen), the entry point is always impure (cf. Haskell main
actions), so you push all the impure stuff as close the entry point as possible, and then call pure functions with the values you collected from the impure actions.