One of the most significant red flags I have noticed in software applications over the years has been the infiltration of business logic into the user interface code.
This leads to the fact that the system can't neatly be tested with automated test suites, due to part of the logic needing to be tested being dependent on details such as databases and external APIs.
In this article, I'll cover :
- How you can separate the concerns of your code
- How you can write flexible code with Port and Adapters patterns
- Give you the big picture of the core of the "Hexagon"
Have you ever read the "Clean Architecture" book? Or read a blog post about it? Anyway, it's a huge step for any developer who wants to write professional testable and maintainable software applications.
So, in this article, I will introduce you to the concept of Policy vs Detail and how you can use it to build decoupled components in your app.
βSoftware has two types of value: the value of its behavior and the value of its structure. The second of these is the greater of the two because it is this value that makes software soft.β β Robert C. Martin, Clean Architecture
Hexagonal Architecture
The hexagonal architecture is a technique used to divide the system into loosely-coupled interchangeable components, such as the application core, user interface, data repositories, test scripts, message brokers and so on.
A shorter definition is the separation of concerns at the architectural level.
The intent
This architecture allows your application to equally be driven by users, programs, automated tests or batch scripts and to be developed and tested in isolation from its eventual run-time devices and databases.
As events arrive from the outside of the domain layer as a port
, a technology-specific adapter
converts it into a usable function call and passes it to the application.
The application becomes ignorant of the nature of the input device. When the application has something to send out, it sends it out through a port to an adapter.
The intent is clearly to give you tools and techniques to create your application to work without either a UI or a database, so you can run automated regression tests against the application, work when the database becomes unavailable, and link applications together without any user involvement.
Behind the scene
This architecture is based on layers built around each other. The innermost layer is the Domain Layer. This layer contains your business logic and defines how the layer outside of it can interact with it. It can also be described as policy
, and anything which defines how to implement this business logic is detail
.
You have to remember when you're writing code, at any given time, you're either writing Policy or Detail.
Policy is when you specify what should happen and when. It's mainly concerned with the business logic, rules and abstractions in the domain you're coding in.
Detail is when we specify "how" the policy happens. It enforced the policy. Details are implementations of the policy.
An easy way to figure out if the code you're writing is detail or policy is to ask yourself, does this code enforce a rule about how something should work in my domain or does this code make something work?
For this reason, frameworks, libraries, and databases are just details.
Policy and Detail Layers
In the Clean Architecture book, the dependency rule specifies that something declared in an outer circle must not be mentioned in the code by an inner circle.
That means that code can only point inwards. So Domain layer code can't depend on Infrastructure layer code. But Infrastructure layer code can depend on Domain layer code (because it goes inwards). So when we follow this rule, we essentially follow the Dependency Inversion rule from the SOLID Principles.
This layer defines the behavior and the constraints of your application. It's what makes your application different from others, the irreplaceable part. The other layer (Infrastructure) contains everything that makes the code work in the domain layer to execute.
Ports and Adapter
In theory, we describe interactions between the Business Layer and the Infrastructure Layer as Policy & Details. Still, when it comes to diving into practice, they are often called the Ports
and Adapters
.
This is an approach to thinking that the interfaces and abstract classes are the Ports (policy) and the concrete classes (the details) are the Adapters.
Let's dive into a practical example.
Take the case you design an PaymentService
interface. It specifies all the things that a Payment Service can handle. But it doesn't implement any of those things.
public interface PaymentService {
boolean pay(BigDecimal amount, String receiver, String email);
}
Now, let write some code that relies on PaymentService
.
public class TransactionService {
private final PaymentService paymentService;
private final Client client;
public TransactionService(PaymentService paymentService, Client client) {
this.paymentService = paymentService;
this.client = client;
}
private boolean proceedPayment(double amount) {
return this.paymentService.pay(BigDecimal.valueOf(amount), client.getName(), client.getEmail());
}
}
Using Java Records, you can create a demo client object for this article using Java Records.
record Client(String name, String email) {}
Because you're referring to policy, all you have to do now is to create the implementation (details).
// This infrastructure layer code relies on the Domain layer payment service.
// So we can define valid implementations
class PaypalPaymentService implements PaymentService {
public boolean pay(BigDecimal amount, String receiver, String email) {
// write your PayPal algorithm here
return false;
}
}
class CardPaymentService implements PaymentService {
public boolean pay(BigDecimal amount, String receiver, String email) {
// write your VisaCard algorithm here
return false;
}
}
class StripePaymentService implements PaymentService {
public boolean pay(BigDecimal amount, String receiver, String email) {
// write your Stripe algorithm here
return false;
}
}
When I want to use a PaymentService in a real app, I have several (valid) options available.
final PaypalPaymentService paypalPaymentService = new PaypalPaymentService();
final StripePaymentService stripePaymentService = new StripePaymentService();
final CardPaymentService cardPaymentService = new CardPaymentService();
final Client receiver = new Client("Kevin","1kevinson.online@gmail.com");
// I can pass any of these in the infrastructure layer since they are all details
new TransactionService(paypalPaymentService, receiver);
The big picture shows that the port fits perfectly with the adapter.
Impacts
If you follow the dependency rule, domain layer code has 0 dependencies.
That means your code is testable.
But, If you've been referring to concretions in your domain layer, it'll be more challenging to write tests for it. This is caused by tight coupling between your components.
So what's that mean?
Using the Port and Adapters Pattern (Policy - Details) allows you to create your application to work without either a UI or a database. You can run automated regression tests against the application, work when the database becomes unavailable, and link applications together without any user involvement.
When we've separated the concerns between Policy and Detail, we create an explicit relationship (contract) that we know how to deal with. If we change the policy, we might end up affecting the detail (since detail depends on policy).
But if we change the detail, it should never alter the policy because policy doesn't depend on detail. This separation of concerns, combined with adhering to the SOLID principles makes changing code a lot easier.
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn β’ Twitter β’ GitHub or Medium
Conclusion
What to remember? Ports & Adapters Architecture has only one intention: isolate the business logic from the delivery mechanisms and tools used by the system through a contract.
The benefits of this architecture are whatever the input or output; every channel has to implement the same contract to communicate with your software, which also makes him highly maintainable, scalable and testable.
Finally, you will easily admit that understanding all these concepts will guide you in writing better software.