In my opinion, this architecture is an excellent example of how an application structure should be built. Moreover, when I wrote my projects on Laravel, I, even without knowing it, often used the ideas underlying the hexagonal architecture.
Being one of the variants of a layered architecture, hexagonal implies the division of an application into separate conceptual layers having different areas of responsibility, and also regulates how they are related to each other. Dealing with this type of architecture, we can also understand how, why, and why when designing an application, interfaces are used.
Hexagonal architecture is by no means a new approach to development using frameworks. On the contrary, this is just a summary of the "best practices" - the practices of new and old ones. I wrapped these words in quotes so that people would not take them quite literally. The best practices that work for me may not work for you - it all depends on the task and the goals pursued.
This type of architecture adheres to the classic ideas that developers come to when designing applications: separating application code from the framework. Let our application be formed by itself, and not on the basis of the framework, using the latter only as a tool for solving some problems of our application.
The term “Hexagonal architecture” was introduced (as far as I know) by Alistair Cobern. He perfectly described the basic principles of this architecture on his website.
The main purpose of this architecture is:
Allows you to interact with the application as a user, and programs, automated tests, batch processing scripts. Also allows you to develop and test the application without any additional devices or databases.
Despite the fact that the architecture is called hexagonal, which should indicate a figure with a certain number of faces, the basic idea is that there are many faces. Each face represents a “port” of access to our application or its connection with the outside world.
The port can be represented as any conductor of incoming requests (or data) to our application. For example, HTTP requests (which are converted into commands for an application) come through the HTTP port (browser requests, API). Similarly, various queue managers or anything that interacts with an application using a message transfer protocol (for example, AMQP) can act. All this is only a port through which we can ask the application to do some kind of action. These faces make up a lot of “incoming ports”. Other ports can be used to access data from the application side, such as a database port.
Why do we even talk about architecture? And all because we want our application to have two indicators:
Actually, both of these indicators express the same thing, we want it to be convenient to work with the application, and also it should allow you to quickly make changes in the future.
Sustainability is determined by the absence (or minimization) of technical debt. Supported will be the application, if you change which we will receive the lowest possible level of technical debt.
Sustainability is a concept designed for long periods of time. In the early stages of development, it is easy to work with the application - it is not yet fully formed and the developers form it based on their initial decisions. At this stage, adding new functionality or libraries is easy and fast.
However, over time, it becomes more and more difficult to work with the application. Adding new functionality may conflict with existing ones. Bugs may indicate a more general problem, the solution of which will require large changes in the code (as well as simplifying difficult code sections).
By laying a good architecture in the early stages, we can prevent these problems.
So what kind of support do we want? What metrics can tell us that we have high support for our application?
If you think that you have turned away from the “right path” when solving a problem, just finish it. Go back to it later, or leave it in the “don't touch” state. There are no applications with perfectly written source code in the world.
Technical debt is a debt that we have to pay for our (bad) decisions. And we pay this debt with time and frustration.
All applications include the initial level of technical debt. We have to work within the framework and taking into account the limitations of the data storage mechanisms, programming languages, frameworks, teams and organizations we have chosen!
Bad architectural decisions made in the early stages of development in the aggregate will result in more and more problems.
Every bad decision usually leads to crutches and hacks. Moreover, the fact that the decision is bad is not always obvious - we can simply make a class that “does too much”, or accidentally mix solutions to several problems.
Minor but poor decisions made during development can also lead to problems. Fortunately, this usually does not lead to large-scale problems that can be caused by design errors in the early design stages. A good foundation reduces the growth rate of technical debt!
So, we want to minimize the number of bad decisions, especially in the early stages of the project.
We are discussing architecture so that we can focus on increasing sustainability and reducing technical debt.
How can we make our application supported?
We simplify making changes to the application.
What can we do to simplify application changes? We can identify those parts of the application that can change and separate them from what remains unchanged.
We will return to this statement a couple of times.
Let's digress for a while and discuss something simple (as it may seem) from the world of OOP: Interfaces.
Not all languages ​​(such as JavaScript, Python, and Ruby) have explicit interfaces, but from a conceptual point of view, goals that pursue the use of these can be easily achieved in these languages.
You can understand by interface a contract that governs what the application needs. If an application can or must contain several implementations, then we can apply interfaces.
In other words, we use interfaces whenever we plan several implementations of the same interface .
For example, if our application has the ability to send notifications to the user, then we can make an SES notifier using Amazon SES, a Mandrill notifier using Mandrill, or other implementations using different services to send mail.
The interface ensures that specific methods are available for use in our application, regardless of the underlying implementation.
For example, the interface of our notifier might look like this:
interface Notifier { public function notify(Message $message); }
We know that any implementation of this interface must contain the notify
method. This allows us to define dependencies on this interface in other parts of our application.
Application no matter what implementation it uses. It is important to him that we have a notify
method and we can use it.
class SomeClass { public function __construct(Notifier $notifier) { $this->notifier = $notifier; } public function doStuff() { $to = 'some@email.com'; $body = 'This is a message'; $message = new Message($to, $body); $this->notifier->notify($message); } }
In this example, the implementation of the class SomeClass
does not depend on the specific implementation, but only requires the presence of a subclass of Notifier
. This allows us to use SES, Mandrill, or any other implementation.
Thus, interfaces are a convenient tool for increasing the support of your application. Interfaces allow us to easily change the way we send notifications - for this we just need to add another implementation and that's it.
class SesNotifier implements Notifier { public function __construct(SesClient $client) { $this->client = $client; } public function notify(Message $message) { $this->client->send([ 'to' => $message->to, 'body' => $message->body]); } }
In the example above, we used an implementation using a service from Amazon called Simple Email Service (SES). But what if we still want to use Mandrill to send notifications? And if we want to send notifications via SMS via Twilio?
As we showed earlier, we can easily add another implementation and switch between them according to our whim.
// SES Notifier $sesNotifier = new SesNotifier(...); $someClass = new SomeClass($sesNotifier); // MandrillNotifier $mandrillNotifier = new MandrillNotifier(...); $someClass = new SomeClass($mandrillNotifier); // , $someClass->doStuff();
By the same principle, our frameworks use interfaces. In essence, frameworks are useful for us precisely because they can contain as many implementations as developers need. For example, various SQL servers, email sending systems, drivers for caching and other services.
Frameworks use interfaces because it allows us to increase support for frameworks — it’s easier to add new features, it’s easier for us to extend the framework depending on our needs.
Using interfaces helps us properly encapsulate changes . We can simply add the implementation that we need now!
But what if we suddenly want to add additional functionality within a separate implementation (and maybe all)? For example, we may need to log the work of the implementation of our SES notifier, for example, to debug some problem that we occasionally have.
The most obvious way, of course, is to fix the code right in the implementation:
class SesNotifier implements Notifier { public function __construct(SesClient $client, Logger $logger) { $this->logger = $logger; $this->client = $client; } public function notify(Message $message) { $this->logger->logMessage($message); $this->client->send([...]); } }
Using logging directly in a specific implementation may be a normal solution, but now this implementation does two things instead of one — we mix responsibilities. Moreover, what if we need to add logging to all implementations of our notifier? As a result, we get a similar code for each implementation, which contradicts the principle of DRY. If we need to change the way of logging, then we will have to change the code of all implementations. Is there an easier way to add this functionality so as to keep the code supported? Yes!
Do you recognize any of the SOLID principles that are affected by this chapter?
To make the code cleaner, we can use one of my favorite design patterns — Decorator . This template uses interfaces to “wrap” our implementation to add new functionality. Let's take an example.
// class NotifierLogger implements Notifier { public function __construct(Notifier $next, Logger $logger) { $this->next = $next; $this->logger = $logger; } public function notify(Message $message) { $this->logger->logMessage($message); return $this->next->notify($message); } }
Just like our other notifier implementations, the NotifierLogger
class implements the Notifier
interface. However, as you may have noticed, he does not even think of sending notifications to someone. Instead, it takes a different implementation of the Notifier interface in the constructor. When we ask the notifier to notify someone of something, our NotifierLogger
will add these messages to the log and then ask to send the notification to the real implementation of our notifier.
As you can see, our decorator logs the messages and sends them further to the notifier so that he actually sends something. You can also expand the order so that logging will be performed after sending the message. In this way, we will be able to add to the log not only the message we were going to send, but also the result of sending.
So, NotifierLogger
decorates our notifier, introducing logging functionality.
The convenience here lies in the fact that our client code (in our case, SomeClass
from the example above) does not care that we gave it a decorated object. The decorator implements the same interface on which our SomeClass
depends.
We can also combine several decorators in one chain. For example, we can wrap an email notifier with an SMS implementation to duplicate messages. In this case, we add additional functionality (SMS) on top of sending emails.
As the logger example shows, we are not limited to adding specific implementations of the notifier. For example, we can organize an update of the database, or collect some metrics. The possibilities are endless!
Now we have the opportunity to add additional behavior, while maintaining the responsibility of each class without overloading it with unnecessary work. We can also freely add additional functional implementations, which gives us additional flexibility. Changing such a code becomes much easier!
Decorator is just one of many design patterns that uses interfaces to encapsulate changes. In fact, almost all classic design patterns use interfaces.
Moreover, almost all design patterns are designed to simplify changes. And this is no coincidence. The study of design patterns (as well as where and when to apply them) is a rather important step towards making good architectural decisions. As an aid to learning design patterns, I recommend the book Head First Design Patterns .
I repeat: Interfaces are the main way to encapsulate changes. We can add functionality by creating a new implementation, or we can add behavior to an existing implementation. And all this will not affect the rest of the code!
By providing good encapsulation, the functionality may be easier to change. Simplifying code changes increases application support (they are easier to change) and reduces technical debt (we invest less time to make changes).
Perhaps about the interfaces enough. I hope this has helped clarify some of why we need interfaces, as well as trying out some design patterns that we used to increase application support.
Well, finally, we can begin to get acquainted with the hexagonal architecture.
Hexagonal architecture is a layered architecture, also sometimes called a port and adapter architecture. They call it so because within this architecture there is the concept of various ports that can be used (adapted) for use with other layers.
For example, our framework uses “ports” to work with SQL for any number of different SQL servers with which our application can work. Similarly, we can implement interfaces for some key things, and implement them in other layers of the application. This allows us to have several implementations of these interfaces, depending on our needs or for testing needs. Interfaces will be our main tool for reducing code connectivity between layers.
For parts of the application that may change, create an interface. So we encapsulate the changes. We can create a new implementation or add additional functionality over the existing one.
Before getting acquainted with the concept of ports and adapters, let's look at the layers that exist within the Hexagonal Architecture.
As I said, within the framework of the hexagonal architecture, the application is divided into several layers.
The purpose of such an application as separate layers is the ability to divide the application into different areas of responsibility.
The code within the layers (and on their boundaries) should describe how their interaction should occur. Since layers act as ports and adapters for other layers surrounding them, it is important to have rules of interaction between them.
Layers interact with each other using interfaces (ports) and implementations of these interfaces (adapters).
Each layer consists of two elements:
By code here, we understand exactly what you might think. It's just some kind of code with which we do things. Quite often, this code acts only as an adapter for other layers, but it can be just some useful code (business logic, some services).
Each layer also has a border that separates it from its neighboring layers. At the border, we can find our "ports". As we have said, ports are nothing more than interfaces, which tell other layers how the interaction will take place. We will consider the question of the interaction of layers a little later, for a start we need to get acquainted with the layers we have.
At the very center of our application is the domain layer. This layer contains the business logic implementation and defines how the external layers can interact with it.
Business logic is the heart of our application. It can be described by the word "charter" - the rules that your code must obey.
The domain layer and the business logic implemented in it define the behavior and constraints of your application. This is what distinguishes your application from others, what gives value to an application.
If your application contains complex business logic, various behaviors, then you get a rich layer of the subject area. If your application is a small add-on above the database (and many applications are such), then this layer will be “thinner”.
In addition to business logic (core domain or core domain), additional logic can often be found in the domain layer, such as domain events (domain events, events thrown at key points of business logic) and “use cases” or use- cases (defining what our application should do).
What the domain layer contains is the theme for a whole book (or a series of books) - especially if you are interested in domain-specific design (DDD). This methodology describes in much more detail the ways in which we can implement an application as close as possible to the terms of the subject area, and therefore to the business processes that we want to transfer to the code.
Consider a small example of “core” logic from the core of the domain:
<?php namespace Hex\Tickets; class Ticket extends Model { public function assignStaffer(Staffer $staffer) { if( ! $staffer->categories->contains( $this->category ) ) { throw new DomainException("Staffer can't be assigned to ".$this->category); } $this->staffer()->associate($staffer); // Set Relationship return $this; } public function setCategory(Category $category) { if( $this->staffer instanceof Staffer && ! $this->staffer->categories->contains( $category ) ) { // , // $this->staffer = null; } $this->category()->associate($category); // return $this; } }
In the example above, we can see the restriction in the assignStaffer
method. If the employee (Stuffer) is not assigned to the same category (Category) as our application (Ticket), we throw an exception.
We can also see certain behavior . If we need to change the category of the application, for which some employee has already been assigned, we again try to assign it to him. If this does not work, then we simply detach the wither from the employee. We do not throw an exception, instead we give the opportunity to assign to the request of another employee when changing categories.
We looked at both business logic examples. In one scenario, we added a restriction, in the case of non-compliance with which we throw an exception. In the other - they provided a certain behavior - as soon as we change the category, we must leave the opportunity to assign the application to the employee who can process applications from the new category.
As we have said, the domain layer can also contain supporting business logic:
class RegisterUserCommand { protected $email; protected $password; public function __construct($email, $password) { // email/password } public function getEmail() { ... } // $email public function getPassword() { ... } // $password } class UserCreatedEvent { public function __construct(User $user) { ... } }
Above, we have auxiliary (but very important) domain logic. The first class is a kind of command (use case in UML terminology) that determines how our application can be used. In our case, this command simply takes the necessary data in order to register a new user. How exactly? We will come back to this later.
The second class is an example of a domain event (Domain Event), which our application can put into processing after it creates a user. It is important to note that those things that can occur within our subject area belong to the subject area layer. These are not system events, such as pre-dispatch
hooks, which are often used by frameworks to expand their capabilities.
Immediately behind the domain layer sits our application layer. This layer deals exclusively with the orchestration of actions performed on entities from the domain layer. Also this layer is an adapter of requests from the framework layer and separates it from the domain layer.
For example, this layer may contain a handler class that performs some kind of juz-case. This class handler accepts incoming data that came to it from the framework layer, and will perform on them some actions that are required to run our juz-case.
It can also send event processing (domain events) that occurred in the domain layer.
This is the outermost layer of code that makes up our application.
Of course, you might have noticed that outside the application layer there is also a “framework layer”. This layer contains the auxiliary code of our application (perhaps accepting HTTP requests, or sending emails), but it is not the application itself.
Our application is wrapped in a framework layer (it is also called the infrastructure layer, infrastructure layer). As mentioned above, this layer contains the code that your application uses, but at the same time it is not part of the application itself. Usually this layer is represented by your framework, but of course it can include any third-party libraries, SDK and any other code. Recall all the libraries that you have connected via composer (suppose we still write in PHP). They are not part of your framework, but they are still combined in one layer. This whole layer is only needed for one thing - to perform various tasks to meet the needs of our application.
In this layer, services are implemented whose interfaces are declared in the inner layers. For example, the Notificator
interface for sending notifications via email or SMS can be implemented here. Our application just wants to send notifications to users, it does not need to know exactly how this happens (via email or via SMS or in any other way).
class SesEmailNotifier implements Notifier { public function __construct(SesClient $client) { ... } public function notify(Message $message) { $this->client->sendEmail([ ... ]); // Send email with SES particulars } }
Another example is the event dispatcher . In the level layer of the framework, the interface can be implemented, declared in the application layer. Again, the application may need to handle some events, but why write your own event dispatcher? Most likely your framework already has a ready implementation. Well, or as a last resort, we can connect a ready-made library, and use it as a basis for implementing our event dispatcher interface.
Our framework level layer can also act as an HTTP request adapter to our application. For example, we can put on his shoulders the receipt of HTTP requests, the collection of incoming data and the marshutization of the request / data to the appropriate controller. Further, the controller can already launch a specific usage scenario from the application level layer (instead of processing everything directly in the controller).
Thus, this layer separates our application from all external requests (for example, made from a browser). This layer is an application request adapter.
Now that we have dealt with what is on this or that layer, let's talk about how they interact with each other.
As mentioned above, each layer regulates how other layers can interact with it. , , .
. . , .
.
, ( ), . , .
, .
, ( ).
, ( ). RegisterUserCommand
( ). , DTO (Data Transfer Object).
, , . “ ”. - . , . , ( ) .
, . , “ ” :
interface CommandBus { public function execute($command); }
, “” ( CommandBus
). — execute
, .
, CommandBus
, . , , .
, :
, . , () . , .
- . , , ? , . email-, SMS. .
, . ? ! :
interface Notifier { public function notify(Message $message); }
. , , , . () () . .
“ ” .
, - . “ ”, .
, , . . , “” - .
(: , ), / . - . “ ” . : . , .
- . , . . — . ?
, ! , . - , TCP ( HTTP/HTTPS). ( ). , .
? ( )!
, . , HTTP , SQL , email ..
, . , .
. , .
- , . , API , HTTP. CORS , HTTP , HATEOAS HTTP . , .
“ ” “”. .
(, ). -.
( ).
, . “ ” ( “”). - , , . , RegisterUserCommand
, . UpdateBillingCommand
, , .
(), , .
. , , “”, . , . . , , - . .
— - , . “” (Command). “ ” (Command Bus) , “” (Handler). .
, :
execute
. , - , . , handle
, .
class SimpleCommandBus implements CommandBus { // public function execute($command) { return $this->resolveHandler($command)->handle($command); } }
, , . , , ( ). (CLI, API- ..).
( web, http api, cli, ) .
, , WEB, HTTP CLI .
public function handleSomeRequest() { try { $registerUserCommand = new RegisterUserCommand( $this->request->username, $this->request->email, $this->request->password ); $result = $this->commandBus->execute($registerUserCommand); return Redirect::to('/account')->with([ 'message' => 'success' ]); } catch( \Exception $e ) { return Redirect::to('/user/add')->with( [ 'message' => $e->getMessage() ] ); } }
, , . , . , WEB-, HTTP API -.
, . , (HTTP, CLI, API, AMQP )! . ( ).
- . , , , , . , .
. ( ..) , , .
, . , . , . , - .
, . , , . , , .
. . , . , , , , - . . ( ) DTO (Data Transfer Object).
class RegisterUserCommand { public function __construct($username, $email, $password) { // } // }
, . .
. , , . . , , . “OK”, , .
DTO ( ), , . , , .
, . , .
interface Handler { public function handle($command); }
handle
, , , . , RegisterUserCommand
:
class RegisterUserHandler { public function handle($command) { $user = new User; $user->username = $command->username; $user->email = $command->email; $user->password = $this->auth->hash($command->password); $user->save(); $this->dispatcher->dispatch( $user->flushEvents() ); // , DTO, // , // "" ( ) // . // return $user->toArray(); } }
, , , , ( - ).
, , , .
, — (Command Bus).
. , ( ). , , . , .
, :
interface CommandBus { public function execute($command); }
. :
class SimpleCommandBus implements CommandBus { public function __construct(Container $container, CommandInflector $inflector) { $this->container = $container; $this->inflector = $inflector; } public function execute($command) { return $this->resolveHandler($command)->handle($command); } public function resolveHandler($command) { return $this->container->make( $this->inflector->getHandlerClass($command) ); } }
CommandInflector
, , . , str_replace('Command', 'Handler', get_class($command));
. , ( PSR- ). , . .
, “”, ? ( SynchronousCommandBus
), . , . , ( AsynchronousCommandBus
). , , .
, . , .
class ValidationCommandBus implements CommandBus { public function __construct(CommandBus $bus, Container $container, CommandInflector $inflector) { ... } public function execute($command) { $this->validate($command); return $this->bus->execute($command); } public function validate($command) { $validator = $this->container->make($this->inflector->getValidatorClass($command)); $validator->validate($command); // } }
ValidationCommandBus
. — , . ( ? ), , .
, , , ( )!
. ( ) , . , , .
- . : . ( ) . , . , .
, . , , . , . .
/ “”. HTTP , - , , . , HTTP , . , , - . . . , . , . , ( ) , .
, , , . , . , . . .
, . , - , . :
, . - .
- . email- , AWS SES, , SES .
, , ! ?
“”. Dependency Inversion c. D SOLID. .
. , . . , . .
. ( ). , , . , ( , ).
( ) . , - . Notifier
. , . . , Notifier
. , . . , , .
, . . , . , !
! . , . , “ !”.
, “” . - . , , , .
, .
Source: https://habr.com/ru/post/267125/
All Articles