2021-09-15 07:12:29 +00:00
|
|
|
:PROPERTIES:
|
|
|
|
:ID: 9af1dcc8-08d5-4f30-a7cb-6395302771dc
|
|
|
|
:END:
|
2021-03-20 08:41:27 +00:00
|
|
|
#+TITLE: The Service Pattern
|
|
|
|
#+Author: Yann Esposito
|
|
|
|
#+Date: [2021-03-14]
|
|
|
|
|
2021-09-15 07:12:29 +00:00
|
|
|
- tags :: [[id:bec11f07-ffed-487b-9059-bdf6696548ab][programming]] [[id:155859f9-3f3c-4cea-bce4-70f24fca05fa][functional programming]] [[id:debfbdb6-03a6-478e-8316-bce0119c0dd7][clojure]] [[id:28b1b988-b2de-46aa-9a47-78a94aa5e2ce][haskell]] [[id:d2a59fd6-947a-4ce5-9673-1494268678c0][architecture]]
|
2021-03-20 08:41:27 +00:00
|
|
|
- source ::
|
2021-09-15 07:12:29 +00:00
|
|
|
- related :: [[id:9df0dad0-11e9-4c1a-ab27-ff2af21a8f78][Effects system in Clojure]]
|
2021-03-20 08:41:27 +00:00
|
|
|
|
|
|
|
* Introduction
|
|
|
|
|
|
|
|
|
|
|
|
The question about code structure and organization is one of the most
|
|
|
|
prolific one.
|
|
|
|
|
2021-05-10 07:04:09 +00:00
|
|
|
In this article I describe one possible solution in this huge design space.
|
|
|
|
|
2021-03-20 08:41:27 +00:00
|
|
|
First of all, I will focus on a functional programming pattern.
|
|
|
|
But I think the lessons could be extended to any generic programming
|
|
|
|
language.
|
|
|
|
|
2021-05-10 07:04:09 +00:00
|
|
|
** Design Patterns
|
|
|
|
|
2021-03-20 08:41:27 +00:00
|
|
|
Before explaining the pattern I would like to take the time to provide a
|
|
|
|
few distinctions between different programming language patterns.
|
|
|
|
Quite often, one fundamental question very important when choosing a
|
|
|
|
pattern for your code is about find the correct level of the pattern.
|
|
|
|
|
|
|
|
There are a tower of patterns and meta-patterns.
|
|
|
|
For example in imperative programming not using =goto= statement was
|
|
|
|
considered as a programming pattern.
|
|
|
|
Once that idea was accepted there were work done on /Object Oriented
|
|
|
|
Programming/.
|
|
|
|
And OOP was considered as a programming language pattern.
|
|
|
|
But OOP while already providing quite a constraint on your code
|
|
|
|
architecture was enough not sufficient.
|
|
|
|
OOP alone leave a lot of room in the design space.
|
|
|
|
Thus we've seen numerous "OOP Design Pattern".
|
|
|
|
That used the underlying OOP paradigm as a base and constructed
|
|
|
|
abstractions over it.
|
|
|
|
|
|
|
|
Even with all those Design Pattern it was up to the programmer to decide
|
|
|
|
which one applies or not.
|
|
|
|
Quite often there is not a single path easy to detect correct design
|
|
|
|
pattern.
|
|
|
|
Mainly the very hard part in programming is choosing the right abstraction.
|
|
|
|
|
|
|
|
There are other code structures to choose from.
|
|
|
|
In functional programming there are FRP.
|
|
|
|
Here also there are stories about how design pattern once chosen make a
|
|
|
|
natural evolution toward meta-design-patterns.
|
|
|
|
Mainly design pattern that rely on a lower level design pattern.
|
|
|
|
|
|
|
|
If you take the story behind Elm Architecture you can see it.
|
|
|
|
At first there were FRP.
|
|
|
|
Elm removed the behavior from FRP to only deal with events to simplify the
|
|
|
|
model.
|
|
|
|
But with FRP the author clearly though it was a good-enough design pattern.
|
|
|
|
But the design space was a bit too big.
|
|
|
|
So it was difficult to take the right decision.
|
|
|
|
So a natural meta-pattern appeared.
|
|
|
|
It is [[https://guide.elm-lang.org/architecture/][/Elm Architecture/]].
|
|
|
|
So while Elm imposed so structure of your program using static types to
|
|
|
|
prevent common coding mistakes and enforce a specific code structure.
|
|
|
|
Elm did not constrain the file organization, the number of buffers to
|
|
|
|
send/receive events, the way they should talk/listen between each other.
|
|
|
|
|
|
|
|
So Elm Architecture is a non enforced meta structure for your code
|
|
|
|
application.
|
|
|
|
Unlike the underlying layer of architecture.
|
|
|
|
But what Elm Architecture provide is a higher level architecture that will
|
|
|
|
help your program to "scale" and whose natural organization is easy to
|
|
|
|
understand.
|
|
|
|
|
|
|
|
So Elm Architecture is more of a proposal that will potentially have
|
|
|
|
drawback.
|
|
|
|
Typically, if you change the organization of your views, it could cost a
|
|
|
|
lot of change in your code.
|
|
|
|
But most of the time this is acceptable and preferable.
|
|
|
|
Because, the Elm Architecture is simple to understand and quite often this
|
|
|
|
is not such a big deal.
|
|
|
|
Not using the Elm Architecture paradigm put you at risk to end up in a
|
|
|
|
spaghetti code hell.
|
|
|
|
But of course there is a tension between code size/DRYness and easy to
|
|
|
|
understand code organization/architecture.
|
|
|
|
|
|
|
|
If you have a short code base, DRYness could probably be preferable.
|
|
|
|
Because a bit of disorganization and shortcuts will not be unbearable.
|
|
|
|
But as the size of your code grow, it will become more and more prevalent
|
|
|
|
that a strict code organization with perhaps more repetitions and a bit
|
|
|
|
more conventions implying more lines of code become preferable because it
|
|
|
|
minimize the risk of surprise between different part of the code.
|
|
|
|
Clearly, Elm Architecture is selling compactness of your code for an easier to
|
|
|
|
read, discover and understand overall code architecture.
|
|
|
|
|
|
|
|
So we could probably say the same for multiple proposed code architecture
|
|
|
|
mechanism in the Haskell world.
|
|
|
|
Typically we had:
|
|
|
|
|
|
|
|
- no org => spaghetti code
|
|
|
|
- big Monad => lack of composability, leak of abstraction everywhere
|
|
|
|
- Handler Pattern
|
|
|
|
- MTL
|
|
|
|
- RIO
|
|
|
|
- Free Monads (Effects)
|
|
|
|
|
|
|
|
After this first short introduction I hope it is clear that, it will be
|
|
|
|
quite impossible to discover a "best code architecture".
|
|
|
|
There are multiple code architecture and the bigger your code the more
|
|
|
|
constraint you must probably put in your code which will make a lot of code
|
|
|
|
look cumbersome from people used to smaller code size.
|
|
|
|
|
|
|
|
That being said, there are code architecture that could be probably be
|
|
|
|
considered fully superior to other ones.
|
|
|
|
Imagine a code architecture with the same properties but better in some
|
|
|
|
dimensions without worse evaluation in some dimension.
|
|
|
|
Typically, a code architecture is preferable to no code architecture as
|
|
|
|
soon as your code become big enough and you need to not work alone.
|
|
|
|
|
|
|
|
For example I would argue that the Purescript Halogen architecture is
|
|
|
|
probably strictly superior to the Elm Architecture.
|
|
|
|
Because it contains Elm Architecture but also contains a shortcut mechanism
|
|
|
|
which is entirely enforced via static types..
|
|
|
|
The "cost" of these shortcuts are quite limited because you are helped with
|
|
|
|
the types provided by the Halogen framework.
|
|
|
|
One big advantage is the ability to not pay the full price of the Elm
|
|
|
|
Architecture while moving a component.
|
2021-03-20 08:46:59 +00:00
|
|
|
* The Service Pattern-level 1
|
|
|
|
|
|
|
|
The service pattern should be easy to grasp with a few concrete examples.
|
|
|
|
|
2021-03-20 08:58:28 +00:00
|
|
|
So the Service Pattern you split your application in components with
|
|
|
|
internal state and a clear interface.
|
|
|
|
It looks a lot like OOP but it isn't.
|
|
|
|
The inner state is global unlike in OOP where every object has an internal
|
|
|
|
state.
|
2021-03-20 08:59:53 +00:00
|
|
|
So the number of isolated state should not grow dynamically but should be
|
|
|
|
mostly static after the runtime init.
|
2021-03-20 09:01:22 +00:00
|
|
|
|
|
|
|
*Important* Every component has a set of direct sub-service dependencies.
|
|
|
|
Every component have an inner state.
|
|
|
|
|
|
|
|
So you write components like this:
|
|
|
|
|
|
|
|
1. declare a component interface that could access the internal state
|
2021-03-20 09:02:45 +00:00
|
|
|
2. declare a set of sub-component your component need
|
|
|
|
3. declare an init function that could consider every sub-component has
|
|
|
|
already been initialized
|
|
|
|
4. declare a stop function that will be called in case your component is no
|
|
|
|
more needed to cleanup the state.
|
|
|
|
|
|
|
|
Generally a service should be a long-running system like a server.
|
2021-03-20 09:05:26 +00:00
|
|
|
Using this design you could declare an unordered list of components that
|
|
|
|
your application need to launch and you could potentially only enable part
|
|
|
|
of them you just need to take care that every enabled component also have
|
|
|
|
every of its sub-component enabled.
|
2021-03-20 09:06:40 +00:00
|
|
|
|
|
|
|
This look easy, but I dare you to achive this in Haskell.
|
|
|
|
Because it relies a lot on runtime properties.
|
|
|
|
This is frustrating as we feel we could put a lot of static informations.
|
2021-03-20 09:12:16 +00:00
|
|
|
|
|
|
|
What does this buy us?
|
|
|
|
|
|
|
|
This description might not provide the full view about the feature we could
|
|
|
|
get from this design.
|
|
|
|
|
|
|
|
1. easy testing. Exactly like an effect system you could switch the
|
|
|
|
implementation of sub-modules to use pure (and thus reproducible)
|
|
|
|
functions during your tests.
|
|
|
|
|
2021-07-28 08:38:07 +00:00
|
|
|
#+begin_src clojure
|
2021-03-20 16:28:40 +00:00
|
|
|
(defprotocol UserService
|
|
|
|
(get-user [user-id] "returns a user entity from its id"))
|
|
|
|
|
|
|
|
(defservice user-service
|
|
|
|
[[:ConfigService get-in-config]
|
|
|
|
[:StoreService read-entity]]
|
|
|
|
(init [this context]
|
|
|
|
(into context
|
|
|
|
{:raw-db-get-user
|
|
|
|
(fn [user-id]
|
|
|
|
(read-entity db-conf user-id))}))
|
|
|
|
(get-user [this user-id]
|
|
|
|
((:raw-db-get-user (get-context this)) user-id)))
|
|
|
|
|
|
|
|
(defservice stub-user-service
|
|
|
|
(init [_ context] context)
|
|
|
|
(get-user [user-id] {:id "fake-user-id" ,,,}))
|
|
|
|
|
|
|
|
(def test
|
|
|
|
(start-app [config-service store-service stub-user-service
|
|
|
|
my-service]
|
|
|
|
(test-my-service)))
|
2021-07-28 08:38:07 +00:00
|
|
|
#+end_src
|