Patterns to be used judiciously 3: Hexagonal Onion
A solution architect works in the gap between enterprise architects and software architects. This is one of several articles written to help solution architects understand the software architecture of enterprise applications, and to supplement the syllabuses covered in our courses to industry certificates for architects of all threee kinds.
The article summarises what I understand of the application architecture described by Herberto Graca, which he calls Explicit Architecture (shown above as a layered architecture). It combines several patterns and ideas, including what people call hexagonal and onion architectures, and domain-driven design.
Contents: Basic principles and patterns. User interface layer. Application layer. Domain model layer. Componentization into microservices. Postcripts on decoupling and constraints.
Remember: "The common, costly mistake is to build an infrastructure and use tools much heavier weight than are needed, or to undertake a sophisticated design approach that you aren’t committed to carrying all the way."
“Domain-Driven Design: Tackling Complexity in the Heart of Software” Erik Evans 2002
I welcome comments from professional software developers about their experience.
Basic principles and patterns
This section outlines three ideas
The layered architecture
As in domain-driven design, the middle layer of the classic three layer architecture is divided in two:
User interface (presentation) layer: presents information to users; receives input, recognises user commands and queries and passes then to the
Infrastructure layer: provides technical services, e.g., persistence, security, and communications with other systems.
Hiding implementations behind interfaces
The four layers are defined by, and encapsulated behind, the interfaces they provide and require. The same is true of components within a layer. This helps you change or replace the implementation of a layer, or a component, with minimal impact on whatever depend on operations in its interface.
Adapters are components that connect the core application to tools in layers above and below it. Driving adapters tell the application what to do; they specify how user interfaces can use an application. Driven adapters are told by the application what to do; they specify how application can use infrastructure tools.
Graca calls the point where an adapter connects to the application layer a port. In most languages, and in its simplest form, a port is an interface. For simplicity, although a port might actually be composed of several interfaces and DTOs, I use the term interface in this article.
For this pattern to work as it should, interfaces must be created to fit the particular application's needs - not simply mimic the infrastructure tools’ APIs. While the application layer contains its own provided and required interfaces, the adapters sit outside of it (as the graphic shows).
Aside for coders
The driving adapters are controllers or console commands “injected in their constructor” with an object that implements the interface.
A driven adapter implements an interface, and is then injected into the application layer, where and whenever the interface is required.
Inversion of control
The third idea, related to the second, is called “inversion of control”. (You may not find that label clear, but we're stuck with it). Adapters depend on a specific tool and the specific interface they implement. For example:
Since the application layer depends only on the interfaces it provides and requires, it doesn’t depend directly on the interface of any particular FTP server, or other infrastructure tool. However, the adapter that sits between the FTP server and the application must understand both interfaces.?
Application Layer
The application layer progresses the state of use cases (required processes), coordinates tasks to complete them, and delegates business logic work to the domain layer.
Users interfaces send commands and queries that trigger use cases/processes in the application layer, which may be specific to one user interface, or shared by several.
Typically, a command handler or application service:
The application layer also triggers other use case outcomes - application events such as sending emails, notifying a 3rd party API, sending a push notification, or even starting a different use case.
The logic to progress and complete a use case is found in command/query handlers and/or application services. A command/query handler can be designed in two different ways:
领英推荐
Aside on application modularisation
Even a so-called "monolithic" enterprise application is modularised in some way. It may be modularised in ways that are more object-oriented or more procedural. E.g., Martin Fowler contrasts his Rich Domain and Transaction Script design patterns for enterprise application architecture.
So, a command or query handler might be coded as a procedure that accesses the persistent data structure, perhaps via a simple interface. And reuse? Where two procedures have the same access path, they may share a subroutine. Where two commands have the same effect on an entity or aggregate entity, they might share a data-centric subroutine.
Systematically factoring out what transactions share by way of common code might produce the most economical application layer code. Graca discusses the more object-oriented modularisation of the kind in domain-driven design. It is not certain this kind of modularisation will be more performant or adaptable, but most do presume it will be more adaptable.
Domain model layer
The domain (model) layer progresses the state of business domain objects, and applies business rules to the updating and generation of data.
Graca says the domain layer is independent of the uses cases or processes that trigger business logic, and independent and unaware of the application layer. So, the domain model depends on nothing outside the domain layer. (For me, a domain model is an entity-event model that includes the input events that update entities, but let that pass.)
The domain model is composed of domain objects that each represent something in the domain. These objects contain data and operations that manipulate that data in ways specific to the business domain.
The primary objects are entities or aggregate entities of interest (such as resource, movement, and location, in the logistics example I use). But the domain model can contain also value [immutable] objects, enums [constant variables] and other objects.
When an entity changes state, the domain model may generate a domain event, conveying the changed state variable values to any subscriber or interested party. A log of these events might be used in “event sourcing".
Some domain logic does not sit happily inside a domain model object. A solution is to create a domain service, which receives a set of entities and performs some business logic on that data. A domain service knows nothing about the application services or data stores, but it can use domain model objects and other domain services.
Componentisation into microservices
The graphic above shows horizontal layers of classes/objects. The dotted lines divide the application/domain layer code vertically into what might be called microservices for sub-domains or bounded contexts. Click on microservices architecture for more about this idea.
The way Graca sees it, a microservice cannot change data it does not “own”, but he says it can query and use data owned by other microservices. If he means to allow a microservice to directly access and query persistent data maintained by another microservices, this will offend those who demand data is encapsulated.
Graca also suggests that, in a use case, when component A needs component B to do something, it cannot make a direct call to a class/method in component B, because that would couple to B to A too tightly. However, components A and B, can be decoupled by inserting a message broker between them, or by inserting a controller over both of them that invokes each in term.
Further reading
Here is a link to Herberto Graca’s article (November 16, 2017) which goes on to discuss the pattern in more detail, and related matters.
Postscript on decoupling and constraints
On decoupling
Graca treats decoupling as the ultimate goal of system design. He says “the goal, as always, is to have a codebase that is loosely coupled and high cohesive, so that changes are easy, fast and safe to make”.
For example, he proposes an interface in front of each data store, and any abstraction (ORM) layer in front of a data store. So you can more easily replace not only the database management system, but also the abstraction layer.
Decoupling is not itself a goal. It can lead to code that is “interfaced to hell”, which complicates things and hampers performance.
As Craig Larman said, in his book on Applying UML and patterns, “It is not high coupling per se that is the problem; it is high coupling to elements that are unstable in some dimension, such as their
"If we put effort into future proofing or lowering the coupling when we have no realistic motivation, this is not time well spent. Focus on the points of realistic high instability or evolution.”
Whether you need all the elements of this Explicit architecture is up to you.
On constraints
Traditionally, OO programmers speak of a data server as though it is generic infrastructure tool like any other; and does nothing but persist and retrieve data.
Yet in a structured database (as opposed to a schema-less one) the data structure (as defined in a logical data model or a physical database schema) is a definition of business domain-specific terms, concepts, rules, variable types, other constraints on variable values, and how types relate to each other.
Given a "defensive design" principle, developers might conclude it helpful to use a database management system to impose constraints as close to the stored data as possible, especially rules of following kinds.
Alternatively, you can minimise constraint rules imposed by the data server, and minimise your dependence on any constraints that do exist, there by coding them (or tighter rules) in the application/domain layer.
Further reading
A solution architect works in the gap between enterprise architects and software architects. This is one of several articles written to help solution architects understand the software architecture of enterprise applications, and to supplement the syllabuses covered in our courses to industry certificates for architects of all threee kinds.
CSEP Systems/Software Engineering - Improving the world through better systems
1 年I liked your highlights on the Domain Model. I learned to think and design using this approach with Peter Coad, and I still use his UML in Color profile when modeling either with classes or tables. One method that relies heavily on domain models is FDD (Feature-Driven Development).