I already mentioned the video from Mark Seeman about Functional Architecture. I proposed sessions on this subject at SocratesCH to discuss and to understand it better. My understanding is still far from perfect, and in this blog post, I would like to give some feedbacks on experiments I have done with an OOP language, sketching hexagonal architecture implementations, before switching to functional implementation.
The idea behind "functional architecture" is to have a pure "functional core" and an "imperative shell", pushing side effects to the border, removing them from the core (the domain, where rules are enforced) to make it more deterministic and then less error prone.
Setup context
The domain and a proposed model
The code is based on a (subset of) online shopping domain. For the purpose of these experiments, I only focus on the use case of adding a product to cart with the following rules:
- Check enough stock then reserve temporarily (let's imagine there is a timer service that remove outdated reservations)
- Do not allow accumulated quantity (in the cart) for each product to be more than 10 (given it is possible to add a product several times, and cumulated quantity is 0)
I propose the following model (with DDD in mind):
- ProductStock aggregate to enforce the first rule
- Cart aggregate to enforce the second one
Common technical assumptions
In any of the examples, I will rely on these assumptions:
- (sort of MVC) controller end point
- some data store somewhere (but not implemented => it is not the subject)
Hexagonal architecture with OOP
A.K.A. "Onion architecture" or "Clean architecture" or "Port and Adapters"...as Mark Seeman explained there are all about the same thing.
So the first step I propose is to setup an hexagonal architecture, with different flavors. It is a first step towards a functional architecture since the idea is to only rely on abstracted input and output adapters in the core.
"Ifs and primitives" implementation
First, I propose to rely only on primitives return types from the core domain:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: |
|
Drawbacks:
- Check "null" on productStock.MakeATemporaryReservation return => failure very implicit
- Check true/false on cart.Add => no information on why it fails
- Cascading "ifs"
- Pure and impure (Get/Save) functions alternate mixed with business logic
- Duplicate conditionals between aggregates functions and "ifs"
Three first drawbacks are quite well known. The two last ones are more interesting.
If we mix business logic with calls to pure and impure functions, then we need to test and test can become quite hard to write (because of need to write fake implementations for abstracted impure functions).
About "duplicate conditionals", you can imagine that we have to write some conditions to return null or not on productStock.MakeATemporaryReservation, and the same for cart.Add. In the code using these methods, we add conditions based on their return values. Each of these conditions is a duplication of the inner conditions of called methods. That's why we talk about duplicate conditions.
Note we could also have a slightly modified implementation with a ProductStockService and a CartService:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: |
|
It has the same drawbacks, and it adds two others:
- There is a bit more of ceremony around injection
- Injection of data store abstractions hides the impure function calls inside services, which is against the idea of pushing side-effects to the border
Note that I am absolutely not against injection, but sometimes abusing of injection can be worst than better.
"Exceptions" implementation
Then I propose to rely on exceptions for error cases:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: |
|
Advantages:
- No more check on null/bool => more explicit with clear exceptions derived from BusinessException
- No more cascading "ifs"
- No more stop/continue business logic, try/catch is explicit to stop the flow in case of error
- Less duplicate conditionals
But still, there is one drawback. Exceptions should not be used to handle business errors, they are expected, we usually consider exceptions are more for unexpected cases.
It seems quite cool, but the drawback is still not so great. Note I was quite surprised in fact that this implementation feels so close to the next one using "continuation" (did I miss something?).
"Continuation" implementation
With previous implementation based on exceptions, we saw a pattern to continue execution: continues on success, stop and report on errors. Instead of exceptions, we can then rely on an Either generic type that represents success with a first type parameter (special case of void with EitherVoid) OR error with a second type parameter. Either type has a ContinueWith method. Let's see this continuation implementation.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: |
|
The implementation follows:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: |
|
Advantages:
- Same as "exceptions" implementation
- But without using exceptions :)
The main drawback of this implementation is that syntax is a bit ugly, with some sort of cascading.
Note we could use Either type slightly modified to use it in the "if" implementation, removing the primitives drawbacks.
NB: for C# developers, perhaps, it reminds you the Task API, it is very close. Task implement some sort of continuation expression. For JS developers, it reminds use of callbacks, it is the same idea.
Next step
I haven't though to other OOP implementations (at least in C#), if you do, please suggest me your thoughts.
The next step I propose is to implement the same logic with F#, with functional architecture is mind. Stay tuned ;) (hoping I will not take 3 months to write it...).