deft/notes/2021-03-14--13-00-04Z--the_service_pattern.org
Yann Esposito (Yogsototh) c1d2459d0c
save
2024-08-14 11:35:42 +02:00

10 KiB

The Service Pattern

tags
programming functional programming clojure haskell architecture blog
source
related
Effects system in Clojure

Introduction

The question about code structure and organization is one of the most prolific one.

In this article I describe one possible solution in this huge design space.

First, I will focus on a functional programming pattern. I think the lessons could be extended to any generic programming language.

Design Patterns

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 when choosing a pattern for your code is about finding 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. 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 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. With FRP the author clearly though it was a good-enough design pattern. 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 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. 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. 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. 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. 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 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 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.

The Service Pattern-level 1

The service pattern should be easy to grasp with a few concrete examples.

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. So the number of isolated state should not grow dynamically but should be mostly static after the runtime init.

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
  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. 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.

This look easy, but I dare you to achieve this in Haskell. Because it relies a lot on runtime properties. This is frustrating as we feel we could put a lot of static information.

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.
(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)))

ANOTHER TRY

Introduction

How to organize modern and big software is a very prolific domain. But I don't really know any way to think clearly about it. We know very few strong facts about code organization.

Generally it goes by: "by enforcing this constraint, or using the language feature, your code is more maintainable, easier to understand, etc…" But, I don't think there is any good non ambiguous objective measure of code quality. And the main reason being that code quality has inherently a part of human feeling. This is like trying to chose between two paints. It could be easy to find one a lot better than the other. But in some case, it starts to be a matter of opinion.

So, let's forget about providing a proof, that the pattern I will present will be better than any other programming pattern.

Note however, there are level of abstractions. The pattern design I will describe is about a quite high level of abstraction, probably only useful for application that reached a certain level of complexity and size. I think this is still very suitable for small programs without adding much noise.

The "Service Pattern" should provide most intuitive high level benefits you could get from Free Monads. Mainly, easy to replace impure code with pure code during testing. But also other benefits, easy to react, replace dynamically part of the code.

So what does this pattern attempt to resolve:

  1. Composability of your application
  2. Replaceability of your components
  3. Simplify life-cycle of your application

The Service Pattern is already quite used in the Clojure community. But we should add another layer of composability that will make it really, really great. Like go beyond the already nice concepts.

So services are about:

  1. You declare a Protocol, mainly just a list of functions your service provide.
  2. You define an instance of this Protocol (at least one instance), this instance also declare dependencies to other Protocols (not service instances)
  3. To launch your application you list the instances of your service to use. As we know the dependencies, if a service instance is missing, we could throw an error.