Let me show you the negative impact of coupling on poorly written HTTP Server (Express Framework) in Node.Js.
It always starts with an interesting prediction that our new server will be “small and simple”.
And yet, as most of the predictions, even this one will soon be doomed.
It all starts with this boilerplate code
The first use case is quite simple. When our server receives a login request from a client it will call the external HTTP API and when everything goes well it will send the 200 status code response to the end user.
We are building middleware. This server stands between UI and the external HTTP endpoint which should not be known to the end user.
Small and simple, as it should be.
This is a test snippet for this use case.
When our external endpoint returns OK status code we are expecting that proper response is sent to the end user.
Couple months later
After some time our middleware has many more use cases to handle. Therefore a lot of new functions like this came up and a lot of these test cases came up as well.
We find out that we have to change our HTTP server (express) because it is too slow and some new fancy HTTP server has just come up.
So we go into the code and realize…. that… well… we are pretty much doomed.
Because we have to change EVERYTHING.
Yes, I mean everything. Just look at our code, we have express everywhere. In our tests and in our production code.
We tightly coupled our high-level policy with low-level details like express response and request object.
Just look at the code. If we want to change our HTTP server we have to go through the whole code base and change the API of the express request and response object to the API of the new server.
Our old test snippet.
New snippet with changed API.
Old production code.
Couple months later #2
We somehow managed to change our HTTP server to the new one (after a lot of time spent).
Couple months later #3
Our product owner came to us that we have to change our external HTTP endpoint to another one which has completely different API.
So we run our IDE and realized…
We are pretty much fuc***, doomed. That we are pretty much doomed again.
because again, we coupled our low-level detail (the external API) with our high-level policy everywhere.
Just look at the code, we have to go through our code base again and change the API of our external HTTP endpoint.
This is how the old external endpoint looks.
The new endpoint API.
The class diagram of whole Login use case looks like this.
So… how do we fix that?
What about this solution?
Login has no knowledge of its surroundings! (Law of Demeter) .
It has knowledge of stable interfaces like Response, Request, and Action. It does not know whether Response is the express object, node object or whatever object. It does not care.
We obey Dependency Inversion Principle because high-level module (Login) depends on abstractions (Response, Request, and Action) and low level-details (Express Response, Request, and Specific External HTTP Endpoint) depends on these abstractions as well.
We inverted the dependencies. Now we can change the Express HTTP server in one place, without breaking anything else. And that is exactly what we wanted. We don’t care about changing the specific external HTTP endpoint either. We depend on the abstraction called Action. It could be anything.
Just look at the new code.
Request and Response Adapters looks as follows.
And Specific External Login Action.
If we need to change the external HTTP action we would only create a new file. If we need to change the HTTP server we would only create new files with new server and adapters for his request and response object.
We would not change the Login file (use case) at all!
Applied Patterns and Principles
Law of Demeter
“a given object should assume as little as possible about the structure or properties of anything else (including its subcomponents)”
Dependency inversion principle
“High-level modules should not depend on low-level modules. Both should depend on abstraction”
“allows otherwise incompatible classes to work together by converting the interface of one class into an interface expected by the clients.”
Don’t be fooled!
There are no explicit dependencies, but there are definitely implicit ones!
If Login action would not use the same API provided from the interfaces Request, Response, and Action it would throw a runtime error during the action somewhere in the code.
I strongly suggest using TypeScript instead because a lot of people fall into this trap. With TypeScript, you can define explicit dependencies of your module and it helps you to keep your design clean.
It’s not all sunshine and rainbows
There is still at least one problem. Our business logic (Login use case) is still coupled with the HTTP protocol. If you look at the diagram you could see that coupling. But imagine how much abstraction would be necessary to decouple that (look at the login example).
Adding more and more abstractions could potentially harm us. Just imagine how much code would you have to write in order to decouple that. And even adding a new use case would be really hard and time-consuming.
It is wise to think about your project and consider such a change to your design because the price for this flexibility is really expensive.