Summary: Domain-driven design is more suitable for architectural models of complex business systems and software systems that require continuous iteration than the familiar MVC layered architecture. There are a lot of literature on the concept and advantages of domain-driven design, and most students have read relevant books. Therefore, this paper does not discuss the concept of domain-driven design, but tries to make a simple introduction to domain-driven development from the level of programming practice.
The author | | prosperous night source ali technology to the public
A preface
Compared with the familiar MVC layered architecture, domain-driven design is more suitable for the architectural models of complex business systems and software systems that require continuous iteration. There are a lot of literature on the concept and advantages of domain-driven design, and most students have read relevant books. Therefore, this paper does not discuss the concept of domain-driven design, but tries to make a simple introduction to domain-driven development from the level of programming practice.
After joining Ali Health, my team has also been actively promoting the application of domain-driven design. My classmate also provided excellent scaffolding code, but the landing condition is not ideal at present. In my opinion, there are four main reasons for this result.
- People are more familiar with the MVC programming mode, and when they need to quickly implement a function, they tend to use a more secure and familiar way.
- There is no consensus on how domain-driven programming should be written (the Axon Framework[1] does a good job of implementing domain-driven design, but it’s too “heavy”).
- DDD implementation itself is difficult, often requires Event drivers and Event stores to achieve perfect implementation, which we do not often use.
- Domain-driven design is oriented to complex systems, and it seems relatively simple in the early stage of business development. It is suspected of over-design to engage in domain-driven design at the beginning. This is why domain-driven design is often discussed only when the system has to be refactored.
The author has studied and practiced domain-driven programming in the research and development process, and also made an in-depth understanding of the domain-driven Framework Axon Framework. (Perhaps because the business scenario is relatively simple) at that time, the implementation effect is not bad. From the perspective of front-line r&d students rather than architects, the core advantages of domain-driven programming are:
- Implement object-oriented programming mode, and then achieve high cohesion, low coupling.
- In the iterative process of complex business systems, the code structure is guaranteed not to become chaotic indefinitely, thus ensuring sustainable maintenance of the system.
The most important aspect of domain-driven development is, of course, proper domain disaggregation, which can be carried out under the guidance of theory and combined with in-depth analysis and understanding of the business by the designer. This paper assumes that the domain division has been carried out before the development, focusing on the specific practices in the coding phase to embody the domain-driven advantages.
Introduction to insurance field knowledge
Taking insurance business as an example to carry out programming practice, a highly abstract division of insurance domain is shown in the figure. Through use case analysis, we divided the entire business into product domain, underwriting, underwriting, claims and other fields. Each field could be separated according to business development. Of course, the complete insurance business is much more complicated than that shown in the figure, so we are not going to introduce the business knowledge here, but just for the convenience of subsequent code practice.
Code structure for three domain-driven development
Domain-driven code layering
You can use different Java projects to publish different microservices for domain isolation, or you can use different Modules for domain isolation within the same Java project. Here we use Module to implement domain isolation. However, regardless of how domains are isolated, interactions between domains can only use HTTP services provided by each other’s binary package or API layer, rather than directly importing other services from other domains.
Within each domain, domain-driven design divides application modules into four layers, as illustrated, as opposed to MVC splitting the application’s three-tier architecture.
User interface layer
Responsible for directly facing external users or systems, receiving external input and returning results, such as binary package implementation classes, Controllers in Spring MVC, specific data view converters, etc., are usually located in this layer. Common package names at the code level can be interface, API, facade, and so on. The definition of input and output parameters at the user interface layer adopts POJO style.
The user interface layer is light and contains no business logic. Security authentication, simple entry validation (such as using the @VALID annotation), access logging, unified exception handling logic, and unified return value encapsulation should be done in this layer.
The function realization required by the user interface layer is completed by the application layer, which generally does not need dependency inversion. When coding, the layer can directly import the interfaces defined in the application layer, so the layer depends on the application layer. It is important to note that while the user interface layer can theoretically use the capabilities of the domain and infrastructure layer directly, it is recommended that until you become comfortable with this usage, you adopt a strict hierarchical architecture where the current layer only relies on the adjacent layer below it.
The application layer
The application layer concretely implements the functions required in the interface layer, but this layer does not implement the real business rules, but rather coordinates the capabilities provided by the invocation domain layer according to the actual use case.
Suggestions for message sending, event listening, and transaction control are implemented in this layer. Common package names used at the code level can be Application, Service, Manager, and so on. It is used to replace the Service layer in Spring MVC and move business logic to the domain layer.
Domain layer
Domain level to the object, it is mainly used to reflect and realize the inherent capabilities of the object in the domain. Therefore, in domain-driven programming, the implementation of domain-level programming is not allowed to rely on other external objects. The programming of domain-level programming can be directly encoded after we have a certain understanding of the inherent capabilities of the objects in the domain and what capabilities it needs to display in the current business scenarios.
For example, when we first encountered object-oriented programming, we often encountered an example that birds can fly and dogs can swim. Assuming that our business domain only cared about the movement of these objects, we could do the following implementation.
public interface Moveable { void move(); } public abstract class Animal implements Moveable {} public class Bird extends Animal { public void move(){ //try to fly System.out.println("I'am flying"); } } public class Dog extends Animal { public void move(){ //try to swim System.out.println("I'am swimming"); }}Copy the code
Domain-driven programming requires the ability to implement objects in this way, rather than the anaemic model we often use in MVC architectures, where business logic is written in services.
Of course, even with this approach to programming, we are far from being domain-driven, and seemingly simple problems can cause a great deal of unease. For example, how should complex objects be initialized and persisted? How should the same thing exist in different fields but with different concerns be abstracted separately? How do objects in different domains get information from each other when they need it?
We will also try to present some reference solutions to these problems in the code sample section.
Infrastructure layer
The infrastructure layer provides common technical capabilities for the layers above, such as the ability to listen, send messages, CRUD capabilities for databases/caches /NoSQL databases/file systems, etc.
2 summary
Based on a further analysis of the layers of domain-driven design, a more specific hierarchical structure is as follows.
Based on the above layering principle, a code structure for reference in the aforementioned insurance field is as follows. We will explain the concept and role of each subcontracting in detail in the following coding example.
Four domain driven development code
In theory, DOMAIN is independent of other layers and is the core of business, so we should write DOMAIN layer code first. However, due to our lack of knowledge in the insurance field, we may not know what inherent capabilities the insurance policy has. Second, for the sake of explanation, we’ll show the code directly with a use case.
1 case
- On the front page, the user selects the insurance product, selects the optional protection liability, inputs the information of the investor/insured, selects the payment method (installment/single payment, etc.) and submits the insurance request after the payment.
- The server accepts the insurance request -> underwriting -> issuing -> issuing policy interest.
Here use case 1 is a precursor to use Case 2. We assume that use Case 1 has been successfully completed (the rate calculation is done in Use Case 1), and that use Case 2 is only a rough implementation, as long as the code style is shown.
2 user interface layer programming practice
The subcontract structure
Where client is the implementation of the inushen-Client (common two-party package) part, and Web is the implementation of the REST-style interface.
Use case code
@AllArgsConstructor @RestController @RequestMapping("/insure") public class PolicyController { private final InsuranceInsureService insuranceInsureService; /** * @param Request * @return POLICY ID */ @requestMapping (value = "/issue-policy", method = RequestMethod.POST) public String issuePolicy(IssuePolicyRequest request){ return insuranceInsureService.issuePolicy(request); }}Copy the code
The classes used for input arguments and return values are defined in the application layer.
3. Application layer programming practice
1. Subcontracting structure
- The outermost interface is oriented to specific business scenarios and can be subcontracted based on service development.
- The POJO package defines the various data classes used by the application layer (IssuePolicyRequest above is here) and the converters that need to be converted as they propagate to other layers.
- The Tasks package defines entry points for scheduled tasks.
Note that in domain programming practice, a lot of type conversions are required, and we can use frameworks such as MapStruct[2] to reduce the amount of work these conversions do.
2. Use case code
@Service @AllArgsConstructor public class InsuranceInsureServiceImpl implements InsuranceInsureService { private final PolicyFactory policyFactory; private final StakeHolderConvertor stakeHolderConvertor; private final PolicyService policyService; /** * Transaction control is generally implemented at the application layer * but it is necessary to pay attention to the transaction support features of the underlying storage * When the underlying storage is divided into databases and tables, other means may be needed to ensure the transaction. Or strip non-core operations from transactions (e.g. database ID generation) */ @override @Transactional(rollbackFor = exception.class) public String issuePolicy(IssuePolicyRequest request) { Policy policy = policyFactory.createPolicy(request.getProductId(), stakeHolderConvertor.convert(request.getStakeHolders())); // Issue policyService.issue(policy); PolicyIssuedMessage message = new PolicyIssuedMessage(); message.setPolicyId(policy.getId()); MQPublisher.publish(MQConstants.INSURANCE_TOPIC, MQConstants.POLICY_ISSUED_TAG, message); return policy.getId().toString(); }}Copy the code
This code shows how the application layer handles use case 2.
- Build Policy aggregations using factory classes at the domain level. If you need to pass complex objects, you need to use type converters to convert the data classes at the application layer into entity classes or value objects at the domain layer.
- Use domain level services to control the checkout process
- Send an order success message, and other fields listen to messages of interest and respond.
4. Domain level programming practice
1. Subcontracting structure
There are five primary subcontracts at the domain level.
- Anticorruption Coating is a domain anticorruption layer, or the encapsulation of two-party packages in other fields when the current field requires to learn about other fields or external information. At the code level, the anticorrosion layer avoids complex parameter assembly and result transformation within the domain when calling external clients.
- Factory solves the initialization problem of complex aggregations. We design the domain model for external invocation, but we must know the internal structure of the object if the external must also use how to assemble the object. This is very unfriendly to caller development. Second, domain knowledge (business rules) in complex objects or aggregations needs to be satisfied, and letting outsiders assemble complex objects or aggregations themselves leaks domain knowledge into the caller code. It is important to note that this is mainly about populating the data needed by the aggregate or entity, not the behavior of the object.
So the core role of the factory here is to pull the external data needed to initialize the aggregate or entity from here and there.
@service @allargsconstructor public class PolicyFactory {/** * private final ProductService productService; /** * Select * from various data sources; * @param productId * @param stakeHolders * @return */ public policy createPolicy(Long productId, List<StakeHolder> stakeHolders) { PolicyProduct product = productService.getById(productId); Policy Policy = Policy. Create (product, stakeHolders); return policy; }}Copy the code
- In the Model is the definition of the domain object. The VO package defines the value objects used in the domain. Can see there are PolicyProduct such an insurance product class, in the field of insurance, we focus on and policy related to a product and its snapshot information, so here we define a policy of insurance products, anti-corrosion layer is responsible for the insurance products from a product domain information is converted to a policy of insurance products we care about the class object.
According to the best practices of domain-driven design, the domain object model does not allow services or repositories to obtain external information. The core concept is what a complete entity can do after it is initialized, or how its state changes after it experiences.
Here is sample code for an aggregation Policy for the core of the domain.
@Getter public class Policy { private Long id; private PolicyProduct product; private List<StakeHolder> stakeHolders; private Date issueTime; /** * @param product * @param * @return */ public static Policy create(PolicyProduct product, List<StakeHolder> stakeHolders){ Policy policy = new Policy(); policy.product = product; policy.stakeHolders = stakeHolders; return policy; } /** * public void issue(Long id) {this.id = id; this.issueTime = new Date(); }}Copy the code
- Repository is a repository that only defines the repository interface, not the implementation. The implementation is left to the infrastructure layer, reflecting the idea of dependency inversion.
- A service is a domain service that defines some behavior that is not part of a domain object, but requires some operations, such as process control.
2. Use case code
@Service @AllArgsConstructor public class PolicyService { private final InsureUnderwriteService insureUnderwriteService; private final PolicyRepository policyRepository; public void issue(Policy policy) { if(! InsureUnderwriteService. Underwrite (policy)) {throw new BizException (" underwriting failure "); } policy.issue(IdGenerator.generate()); Save (policy); policyRepository.create(policy); }}Copy the code
Note here that we have omitted a line policyRepository.save(policy); , so why the difference between Save and Create?
Save is the right thing to do in domain-driven design: my aggregation or entity changes, the repository doesn’t care whether it’s new or updated, just save it for me. Sounds nice, but it’s not very friendly to relational database storage. So, in our scenario, contrary to what the book calls best practice, we tell the warehouse whether to create or update, and even which columns to update if at all.
In addition, the best practice of domain-driven is event-driven, which is perfectly implemented by AxonFramework. The application layer issues an IssuePolicyCommand, the domain layer receives the command, and issues PolicyIssuedEvent after the policy is created. The event is listened on and persisted in the Event Store. At present, it seems unlikely that this method will be implemented in our place. I will not give more introduction.
5. Infrastructure layer programming practice
1. Subcontracting structure
Only the Repository implementation is shown here, but there is a lot more going on here, such as binary package implementation of RPC calls, class injection, etc. As mentioned above, the domain layer does not care about the implementation of warehousing, leaving it to the infrastructure layer. The infrastructure layer can use relational databases, caches, or NoSQL as needed; the domain layer is agnostic. Here, we take relational database as an example. Dao and DataObject can be generated using tools such as Mybatis Generator. Convertor is responsible for the conversion between domain object and DataObject.
2. Use case code
@Repository @AllArgsConstructor public class PolicyRepositoryImpl implements PolicyRepository { private final PolicyDAO policyDAO; private final StakeHolderDAO stakeHolderDAO; private final PolicyConvertor policyConvertor; private final StakeHolderConvertor stakeHolderConvertor; @Override public String save(Policy policy) { throw new UnsupportedOperationException(); } @Override public String create(Policy policy) { policyDAO.insert(policyConvertor.convert(policy)); stakeHolderDAO.insertBatch(stakeHolderConvertor.convert(policy)); / /... Return policy.getid ().tostring (); } @Override public void updatePolicyStatus(String newStatus) { } }Copy the code
This part of the code is relatively simple and needless to say.
Five epilogue
As for domain-driven, I’m still in the beginner’s stage, and even the best design will inevitably get messy as the business grows, and everyone involved is responsible for this process. Finally, I want to summarize some of the principles we use to maintain our original code.
- In-depth understanding of business scenarios, analysis of use cases, and proper domain segmentation.
- After determining the implementation method, we should try to program in accordance with the established mode/style. If there is any disagreement, we can discuss and change it together.
- Don’t introduce unnecessary complexity.
- Optimize and improve the system design constantly, optimize the tedious code with design mode.
- Write a comment.
The original link
This article is the original content of Aliyun and shall not be reproduced without permission.