From procedural code to decorator pattern
Motivation: Show how design patterns can improve our spaghetti procedural code and resolve separation of concerns problems of procedural code
Real-example of decorators solve problems and help to maintain and extend existing code and write new without changing the old one
We started the project from simple screens without any authorization flow as a typical start-up. For HTTP requests we decided to write a simple manager instead of a huge Alamofire/Moya (or similar) ready to use 3rd party library. (CODE 1)
After the successful MVP, we deiced to extend our iOS team, and write more modules, flows, and features. And first of all, we needed the auth flow. And it means that our code should be modified and extended. (CODE 2)
The code above was extended and takes more responsibilities. This leads to more bugs because this one class works with 3 different responsibilities and you can easily break something while fixing some part of this code. It means that this code has more than one axis of change
But it is not the end, and after the refresh token process, we needed the third logic for our HTTP manager — a permanent block of users. And the new code looks like CODE 3
Right now, after 3rd iteration of changes, we can see that our code is a mess. It has a lot of responsibilities, high complexity, and is not easy to read form. In the real project, we had 5, but even with 3, we see that it gets harder and harder to understand and maintain this code. Also, it is hard to remove some of the logic, for example, block list logic, or rollback to just simple httpClient without any auth logic
How to solve this?
When the problem is clarified, we can try to solve this with design patterns. Our problem is to intercept the request and do the custom work before or after the actual request to the server in the general way our problem is to extend existing code instead of changing it. The decorator pattern is brilliant for problems like this. At first, we need to define the main protocol for work with our HTTP client CODE 4
Our simple HTTP client that has only 1 responsibility to send a request and deliver a response will look like CODE 5
Now we need to add refresh token logic and this is the urgent moment when the decorator pattern solves the main problem of extending the existing code. We should create a new class that conforms to the httpClient protocol and holds the reference on another httpClient object. The main here is that we should use protocol, not the actual type of our httpClient. Our new class can only intercept the request, check token expiration, refresh and ask decorate how to work with the request after. This Decorator knows only how to deal with refreshing logic and saving credentials. No other responibilietes from not realated areas (CODE 6)
The third part of httpClient is the blocking list. Our client should check if the current account is permanently blocked and returns a specific error without any network call. The plan is the same, new decorator which will be responsible only for blocking list logic CODE 7
We finished creating our decorators and I want to add one bonus requirement. For our analytics, we need to measure the actual requests time it takes to get a response from the server (CODE 8)
How to work with this bunch of objects?
We need a place where all of these objects will be combined together. The factory method is a great pattern that helps to hide the complexity of the creation of the objects graph and add a simple point to use this object graph over the application without forming this object graph each time when you need this (CODE 9)
These decorators have their own responsibilities and can be easily removed, tested, or changed. It is super easy to find a bug only in token refreshing logic or in blocking list logic. For more clarification — the real work of this code is next. If the load method will be called from the client.
All these objects already conform to HTTPClientProtocol that’s why the client controller also can depend on this protocol. All of these decisions lead to more testable code. Because this client with the dependency injections can be replaced with the spy of stub httpClient in the automation testing process (CODE 10)
When the load method will be called, then our objects graph will work in a next way
1. PermanentBlockHTTPClientDecorator checks if users are already blocked or now and send a load command to the next decorator
2. RefreshCredentialsHTTPClientDecorator as the next decorator checks if the token is expired, refreshes it if needed, and sends a load command to the next decorator
3.HTTPClientLoggerDecorator as the next decorator starts to measure the real network request and send a load command to the next decorator
4. HTTPClient as the last main root object makes a simple network request and returns the response to the previous decorator
5. HTTPClientLoggerDecorator as the previous decorator ends the measure of network call, writes the results to some storage and returns a response to the previous decorator
6. RefreshCredentialsHTTPClientDecorator and PermanentBlockHTTPClientDecorator as previous decorators just return the response to the controller
1. Single responsibility and dependency inversion principles are not violated. Decorators are separate objects which have their own responsibilities and can leave even in other modules/frameworks because their dependency is inverted and they depend on protocol and the code (our view controller) depends on protocol, not on actual objects. Also, these objects can be written by different programmers in parallel. All that you need is to define HTTPClientProtocol
2. With the factory method it is easy to define a single object graph of a huge list of simple objects with some single responsibilities like token refreshing, logging, blocking list, and reusing it across the application. You can also build combine these objects in different ways for different environments. For example for production, you don’t want to use HTTPClientLoggerDecorator because it will lead to performance issues. It is easy to define once in the factory method which objects and how should be combined for different environments
3. Automated testing is a super powerful tool for high-quality development. Such principles as dependency inversion, single responsibility, and dependency injection will lead to easy-to-test software because all of their parts can be spied, stubbed, and mocked in the testing process
The main disadvantage here is testing. All of these objects should be tested in good projects because this is a production code written by us. Even clean and maintainable code should be tested and it takes time.
In the next post “From Decorator design pattern to Universal abstraction” I will talk about Universal abstraction and how to use Combine in a functional programming paradigm instead of a decorator design pattern. It solves the main disadvantage of the decorator pattern with lack of testing and looks more functional which is more consistent with SwiftUI frameworks and declarative way of thinking
LinkedIn Twitter Original Blog Github HackerRank
Photo by La-Rel Easter on Unsplash