What is all this Clean Architecture jibber-jabber about? - Part 2
In the last post, we walked through the Clean part, giving a summary of some concepts that you should know if you wish to write well-crafted code. If you want to go more deeper I encourage you to review other principles and patterns that have been explained and discussed for so long, like GRASP and the ones included in the Gang Of Four Design Patterns Book. In this post, we are going to mention some layered architectures, explaining what they have in common and their advantages and disadvantages. Furthermore, we’ll go deeper, describing Hexagonal Architecture and comment some of its potential downsides. So, less talk and more action!
Architectures: Different names, same philosophy
As you may know, over the last several years many different architecture approaches have appeared. These include: Onion Architecture, Hexagonal Architecture (also known as Ports and Adapters) and the popular Clean Architecture, among other layered architectures. Though these approximations have their own fancy names and could seem completely different, they share a lot in common. In fact, its main intent is the same:
Achieve a high-level separation of concerns by layering.
Ultimately, as we’ve seen in the previous article, Clean Architecture represents a group of best practices that allow us to implement code that rocks, which means, spending time on what is important:
Building awesome applications that people love.
What are the main benefits of going that way?
All the above approximations create software that are:
Independent of frameworks
Frameworks are considered services. That is to say, we can focus on the business logic (what makes our app different from others) being agnostic to the outside world so that you have the possibility of choosing whatever framework you want and the most important thing:
The frameworks that you use don’t end up being your application itself.
Here, when referring to frameworks we also include other low level details such as GUIs, databases, network services, etc. In short, whichever part of our application that we might have to change without modifying our inner core rules. That give us the chance of deferring the decision of which technology to choose until when we really need it.
Testable
Our code becomes extremely easy to test because our domain logic is decoupled from the implementations details of the outside world. That results in quicker tests since we are able to validate our system in isolation (without loading the database or making real HTTP requests) using Test doubles for resolving our dependencies. In addition to get immediate feedback that things work as expected, there are other motivators for creating tests like Document the behaviour of the system, TDD or, the one that I like the most, Enable Refactoring.
Easy to understand
Ultimately, we should strive to make our code read like a story. Back in the 90’s, seniority was acquired depending on how clever you were writing code. Some senior developers wrote software that just a few could understand, proving how smart they were. Nowadays, we work in large teams with a bunch of programmers changing the same base code and generating thousands of lines every day. Imagine for a moment that you were in this situation, would you be able to understand everything written that way? What about your new teammates or apprentices? Do you think would be fair for them? Right now, that’s disrespectful and unacceptable. As you can imagine, Architecture plays an important role in all this. We should structure our applications so that you can understand what they do at first glance. One way to achieve that is within vertical slicing.
Summarizing, the above benefits lead us to aim for architectures that contain the following attributes:
High Maintainability
A maintainable application is one that increases technical debt at the slowest rate we can feasibly achieve.
In the beginning of a greenfield project you have everything under control. As time passes, bugs start appearing, adding new features implies changes in different parts of your code… So, ultimately, applications get tougher to work on.
A good architecture in advance can help prevent such potential problems.
Low Technical Debt
Technical debt is the debt we pay for our “bad” decisions, and it’s paid back in time and frustration.
The debt can be thought of as work that needs to be done before a particular job can be considered complete or proper. If the debt is not repaid, then it will be hard to implement changes later on and we will end up doing hacks and work-arounds everywhere.
So we need to reduce as many “bad” decisions as possible and as soon as possible.
Basically, in order to produce maintainable applications we need to make them easy to change and, in order to do that, we must follow the well-known design principle:
Identify the aspects of your application that vary and separate them from what stays the same.
But, all that glitters is not gold…
Like everything in life, all that glitters is not gold. Below we are going to explain some drawbacks that we need to take into consideration when architecting our apps.
Higher level of complexity
Normally, more levels of abstraction mean more difficult to understand. Multiple layers imply more files to handle, which means that following the flow of our use cases becomes harder.
Over-engineering?
There are a lot of scenarios where we aren’t gonna need that level of adaptability. Especially in mobile applications in which the domain logic is really close to the view. Prematurely creating that kind of indirection and isolation is usually a waste of time and it may be a bit overkill.
Hexagonal Architecture
Beginnings
The name of Hexagonal architecture comes from Alistair Cockburn’s article. As he explains in his post, its main purpose is:
“Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases”.
Ports and Adapters
As you may know, Hexagonal architecture is also called Ports and Adapters architecture. In fact, those terms are the essence of the architecture.
The number of ports (sides) is arbitrary and it depends on the application. The key point is that the business logic is at the center, inside the hexagon, and each side represents some sort of a conversation the application needs to have with the outside world.
Ports
A port can be viewed as an entry point or a contract, provided by the core logic. It defines the functionality that the application offers.
Adapters
For each external device there is an adapter that converts the API definition to the signals needed by that device and vice versa.
In brief, ports translate to interfaces and adapters to implementations.
Ports and adapters, when implemented, come in two flavors, namely primary and secondary or driven and driving. The distinction between each lies in who triggers or is in charge of the conversation. That is, outside-in the hexagon and the other way around, respectively.
Summing up, outside the hexagon you have layers of ports and adapters that take requests from the outside world into the application. The resulting message from the application is then passed back through this layer of ports and adapters as an appropriate response.
The dependency rule
The dependency rule is key to making this kind of architectures work properly. So we need to understand it very well in order to apply it correctly.
This rule says: Source code dependencies can only flow from outside, in. Nothing in an inner layer can know anything at all about something in an outer layer.
When data is moving in, dependencies are not big deal. We need to be more careful when the dependencies are going out. But remember, we’ve already learnt how to deal with this complexity. We only need to apply Inversion of control and problem solved! Indeed, we can achieve it by using interfaces. These allow our layers to inform other layers how they will be interacted with, and how they need to interact with other layers. It’s up to the other layers to implement these interfaces. In this way, we are letting our inner layers command how they are used and our dependencies keep going in one direction, inwards.
Pros and cons
The same advantages and disadvantages described before apply in this specific approach.
Personally, I’d like to highlight the following:
Pros
-
Ports and adapters are interchangeable. That allows us to swap an adapter with a different implementation that conforms to the same interface. Although this might seem rare, you might need to replace them for different reasons, like testing or because one library gets deprecated, for example.
-
Defer decisions until the very end. You are able to code the logic of your application without worrying what aspect will have the user interface or which database you will choose. That’s really powerful because different people can be working on different parts without affecting each other and, if they follow the contracts, when gluing everything together, it will work.
-
Implement features faster. When you know how to convert requests and responses as they come and go from the outside world it is really easy to implement new features. The architecture facilitates you the separation of concerns, which means you can concentrate on one specific task at a time and you develop faster.
Cons
- YAGNI. The main concern of Hexagonal architecture is that you could end up with interfaces with only one implementation and you need to be careful because the architecture holds you to the usage of ports and adapters in every layer. But, as always, you need to be pragmatic and don’t forget to KISS!
Conclusion
As we’ve seen, pros beat cons! So, it’s clear that having an architecture for our apps is something that we must always take into consideration. Call it what you want but don’t forget that it’s the only way to achieve a maintainable, testable application with low technical debt.
In the next and final article we’ll get our hands dirty. I’ll present Catan Architecture, a hexagonal architecture for Android. We’ll analyze the overall structure, how the data crosses the boundaries of the different layers and all the rest of the implementations details by reviewing a sample project. Who wants to play Catan? Don’t settle for less!
P.S I hope you’re enjoying the series but I would love to hear your thoughts! So feel free to leave a comment ;)