preface
There are so many DDD articles on the web, but there is not a good example of code engineering. In this article, you will write DDD code hand by hand and learn the benefits of combining DDD ideas with code.
First, start with the hexagonal architecture
Hexagonal architecture is also called ports and adapters. For each external type, there is an adapter corresponding to it. The outside world interacts with the inside through application layer apis.
Hexagonal architecture encourages a new way of looking at the whole system. There are two zones in this architecture, “outer zone” and “inner zone.” In the external area, different customers can submit input; The internal system is used to retrieve persistent data and store the program output (such as a database) or forward the output somewhere else (such as messages, to message queues) halfway through.
Each type of customer has its own adapter that converts customer input into input understood by the program’s internal apis. Each different edge of the hexagon represents a different type of port, which handles either input or output. The figure shows three customer requests arriving at the same input ports (adapters A, B, and C), and another customer request using adapter D. It is possible that the first three requests use the HTTP protocol (browser, REST, SOAP, etc.) and the last request uses the MSMQ protocol. Ports are not clearly defined and are a very flexible concept. Regardless of how the ports are divided, when a client request arrives, an adapter should transform the input, which then invokes an application operation or sends an event to the application, giving control to the internal region.
Second, dependence inversion
The principle of dependency inversion (DIP) was proposed by Robert C. Martin, and the core definition is:
High-level modules should not depend on low-level modules; both should depend on abstractions; abstractions should not depend on implementation details; implementation details should depend on interfacesCopy the code
According to the DIP principle, the domain layer can no longer depend on the infrastructure layer, and the infrastructure layer can complete the decoupling of the domain layer through the implementation of injection persistence. The new layered architecture model using dependency injection principle becomes as follows:
With dependency injection, the concept of layering is virtually eliminated. Both the high level and the low level actually rely only on abstraction, the whole layer seems to be bulldozed.
Third, DDD code layer
Overall code structure
- com. ${company}. ${system}. ${appname} | - UI (user interface layer) | service | - impl | - web | - controller | - filter | - application (application layer) | - service | - impl | -command | - query | - dto | - mq | - domain (domain) | - service | - the facade | - model | - event | - the repository | | - infrastructure (infrastructure layer) - dal | - DOS | - dao | - mapper | - factoryCopy the code
3.1 User interface Layer
The user interface layer, as the external portal, decouples network protocols from business logic. It can be used for authentication, Session management, traffic limiting, exception handling, and logging.
The return value generally use {” code “: 0,” MSG “:” success “, “data” : {}} format returned. Generally, some common Response objects will be encapsulated, as follows:
public class Response implements Serializable {
private boolean success;
private String code;
private String msg;
private Map<String, Object> errData;
}
public class SingleResponse<T> extends Response {
private T data;
}
public class ListResponse<T> extends Response {
private int count = 0;
private int pageSize = 20;
private int pageNo = 1;
private List<T> data;
}
Copy the code
Interfaces at the user interface layer do not need to correspond with application interfaces one by one. Different interfaces must be used in different scenarios to ensure compatibility and maintainability of subsequent services.
3.2 the application layer
The application layer connects the user interface layer and the domain layer and mainly coordinates the domain layer. Oriented to use cases and business processes, the application layer coordinates the composition and orchestration of multiple aggregations to complete services. It does not realize any business logic at this layer and is only a thin layer.
Core classes of the application layer:
- ApplicationService: The core class responsible for orchestration of business processes, but not for any business logic itself. This is sometimes abbreviated to “AppService”. An ApplicationService class is a complete business process in which each method is responsible for handling a Use Case, such as various Use cases for an order (place, pay successful, ship, receive, query).
- DTO Assembler: Responsible for converting internal domain models into externally available DTO.
- Dtos returned: as an exit argument to ApplicationService.
- Command: An instruction that the caller explicitly wants the system to operate with the expectation that it will affect a system, namely, a write operation. In general, instructions need to have an explicit return value (such as synchronous operation result, or asynchronous instruction already received).
- Query: Something that the caller explicitly wants to Query, including Query parameters, filtering, paging, etc., and is expected to have no impact on a system’s data.
- Event: Refers to an existing Event that has happened and requires the system to change or respond to it. Usually, Event processing involves writing operations. The event handler does not return a value. It is important to note that the concept of an Event in the Application layer is similar to that in the Domain layer, but not necessarily the same. The Event here is more of an external notification mechanism.
The interface input parameter of ApplicationService can only be a Command, Query, or Event object. The CQE object must represent the semantics of the current method. The benefits are improved interface stability, reduced low-level repetition, and more semantic interface input parameters.
Case code
public interface UserAppService {
UserDTO add(@Valid AddUserCommand cmd);
List<UserDTO> query(UserQuery query);
}
@Data
public class AddUserCommand {
private Integer age;
privateString name; . }@Data
public class OrderQuery {
private Long userId;
private int pageNo;
private int pageSize;
}
@Data
public class UserDTO {
private Long userId;
private Integer age;
privateString name; . }Copy the code
Reuse of CQE objects should be avoided for instructions with different semantics. Counterexample: A common scenario is “Create Create” and “Update Update”. Generally speaking, the only difference between these two types of objects is an ID. So it’s not uncommon to see students using the same object as an input parameter to both methods, the only difference being whether the ID is assigned. This is incorrect because the semantics of these two operations are completely different, and their validation conditions may be completely different, so you should not reuse the same object. The correct approach is to produce two objects.
3.2 1 Response vs Exception
The HTTP and RPC interfaces of the Interface layer, which return a value of Result, catch all exceptions. All interfaces in the Application layer return dTOS and are not responsible for handling exceptions.
3.2.2 CQE versus DTO
On the surface, both types of objects are simple POJO objects, but there are big differences:
- CQE: Is the input to the ApplicationService. It has an explicit “intent” and the object’s internal need to ensure its correctness. Every CQE has a clear “intent”, so it should be avoided as far as possible, even if all parameters are the same, as long as the semantics are different, it should not be reused.
- DTO: just a data container, only for external interaction, so it does not contain any logic, just anaemic objects.
Since CQE is “intentional”, the amount of CQE is theoretically infinite. However, as a data container, DTO is corresponding to the model, so it is limited.
3.2.3 Anti-corruption Layer
In ApplicationService, external services are often relied upon, creating dependencies on external systems at the code level. For example, when creating a user that may rely on account services, we introduce an embalming layer. The class name of the anticorrosion layer is usually “Facade”.
Implementation of ACL anti-corrosion layer:
- For dependent external objects, we extract the required fields to generate an internally required VO or DTO class.
- Build a new Facade that encapsulates the invocation link to transform the external class into an inner class.
- For external system calls, the same Facade method wraps external call links.
3.3 domain layer
The domain layer is the core of the domain model, which mainly realizes the core business logic of the domain model and reflects the business capability of the domain model. The domain layer focuses on the atomized business logic that implements the congestion model of domain objects and the aggregation itself, leaving the user operations and business processes to the application layer to orchestrate. This design can ensure that the domain model is not susceptible to changes in external requirements and ensure the stability of the domain model.
Core classes at the domain level:
- Entity Classes: At the heart of most DDD architectures are Entity classes, which contain state in a domain and direct operations on that state. The most important design principle of Entity is Invariants, ensuring that no matter what the external world does to it, the properties of an Entity cannot be in conflict with each other or in inconsistent states.
- Value object (VO) : Usually used to measure and describe things. It is very easy to create, test, use, optimize, and maintain, so we try to use value objects for modeling.
- Aggregation root (Aggr) : Aggregation is composed of entities and value objects that are closely related to business and logic. Aggregation is the basic unit of data modification and persistence. Each aggregate has a root entity, called an aggregate root, through which the outside world can only communicate with the aggregate. The main purpose of the aggregation root is to avoid aggregation and data inconsistencies between entities due to the lack of uniform business rule control for complex data models.
- Domain Services: When an operation does not fit on aggregate and value objects, it is best to use domain services. Where domain services can be used, overuse of domain services will lead to anaemic domain models. Perform a significant business operation process; Transform domain objects; Multiple domain objects have been evaluated as input, resulting in a value object.
- A Repository is a Repository of data that is stored as a collection and retrieved when needed. As a connecting component between the domain layer and the infrastructure layer, the domain layer does not have to pay too much attention to the storage details. In the design, the warehouse interface is placed in the domain layer, and the concrete implementation of the storage is placed in the infrastructure layer. The domain layer accesses the data storage through the interface, without paying too much attention to the details of the storage data, which makes the domain layer pay more attention to the domain logic.
- Factory: For entities and other objects that are more difficult to construct, you can use factories to construct.
In general, entities, value objects, and aggregate roots should not be appended without class suffixes to better reflect the meaning of the domain object itself.
public class Order {
// OrderId is implicit and explicit, instead of using a String directly, String can only represent a value
private OrderId orderId;
private BuyerId buyerId;
private OrderStatus status;
private Long amount;
private List<OrderItem> orderItems;
public static Order create(...). {
// If there are too many parameters, the construction is cumbersome, can be migrated to Factory. }public void pay(...). {}public void deliver(...). {}... }public class OrderItem {
private Long goodsId;
private Integer count;
public static OrderItem create(Long goodsId, Integer count) {... }}Copy the code
Public class OrderDomainService {@resource private OrderRepository OrderRepository; public Order create(Order order) { ... orderRepository.create(order); return order; }}Copy the code
public interface OrderRepository {
void add(Order order);
Order getByOrderId(OrderId orderId);
}
Copy the code
3.4 Infrastructure Layer
Mainly responsible for technical details, such as database CRUD, cache, message service, etc.
public class OrderDO {
}
public class OrderItemDO {
}
Copy the code
public class OrderDao implements OrderRepository { @Resource private OrderMapper orderMapper; @Resource private OrderItemMapper orderItemMapper; @Override public void add(Order order) { OrderDO orderDO = OrderFactory.build(order); List<OrderItemDO> orderItemDOList = OrderFactory.build(order); orderMapper.insert(orderDO); orderItemMapper.batchInsert(orderItemDOList); }}Copy the code
The resources
- Implementing Domain-Driven Design
- DDD: How to avoid writing ledger code
- DDD: Domain level design specification