laitimes

DDD学习与感悟——向屎山冲锋

A software system is a deliverable that is produced by software development to solve a business area or problem unit. Software design can help us develop more robust software systems. Therefore, software design is the bridge between the business domain and software development. DDD is one of the ideas in software design, which aims to provide a design idea and specification for large and complex software. Through DDD thinking, our business architecture, system architecture, deployment architecture, data architecture, engineering architecture, etc. can be highly scalable, highly maintainable and highly testable.

But landing DDD is a very difficult thing to do. First of all, it is relatively difficult to break through at the level of ideology and cognition.

DDD itself is an idea, not a specific technology, so there are no constraints at the level of code implementation and system architecture. Due to the mature ORM frameworks on the market (such as hibernate, mybatis, etc.), most software development is directly oriented to database development. The application layering architecture in traditional development is very similar to the layered architecture of DDD thinking. As a result, many people have a certain understanding bias when they first learn DDD, which leads to the inability to implement DDD ideas.

This article records my learning, perception and practical experience of project engineering code refactoring in DDD!

一、Domin Primitive

The meaning of the realm "metadata". The main thing is to explain the basic principles of the field. This is also the basic guideline for using DDD ideas.

1.1 Implicit concepts become explicit

exp: A phone number is usually a combination of an area code code + a number. In practice, there are many businesses that require phone numbers. For example, login authentication, shopping guide distribution and other businesses; We need to do a basic verification of the phone number; Obtain area code codes, etc.; In normal practice, a large number of such check codes and judgment codes will be written at the entrance of each method that uses a phone number, although we can extract its checksum to get the area code code into the util class (which is actually done in most projects), but this method is not a cure. Based on the idea of DDD, we can find that there is an implicit concept here: area code coding.

Based on the idea of DDD, we can create a phone number as a value object with independent concepts and behaviors: PhoneNumber, and encapsulate stateless behaviors such as basic checksum encoding in the value object. This eliminates the need to write a lot of checksum judgments in the method.

1.2 Implicit contextualization

exp: In the bank transfer scenario, we usually say that account A transfers 1,000 yuan to account B. The 1000 yuan here actually has two meanings, the number 1000, the currency dollar. But we usually ignore the currency unit. As a result, units are not taken into account when implementing the transfer function. As soon as there is an international transfer, it will get bogged down in a lot of if else.

Based on DDD ideas, we create money as a value object with independent concepts and behaviors: Money, so that what we call money has a complete concept. In this way, the implicit context of the currency can be explicit, thus avoiding bugs that are not recognized at the moment but may explode in the future.

1.3 Encapsulating Multi-Object Behavior

exp: In the cross-border transfer scenario, we need to convert the exchange rate, and we can encapsulate the conversion exchange rate into a value object. By encapsulating the amount calculation logic and various check logics, the whole method is extremely simple.

1.4 Differences between DP and Value Objects

DP is a concept proposed by Ali God; Value objects are a concept in DDD thinking.

After learning this, I personally think that DP is a further complement to the value object and gives it a more complete concept. On the basis of the value object [Invariance], [Verifiability] and [Independent Behavior] are added. Of course, it is also required to have no side effects. The so-called no side effects means that the state is immutable.

1.5 Difference between DP and DTO

DISC DP
function The object of data transmission is a technical detail A business concept that belongs to the domain
Data relevance There is no data relevance There is a strong correlation between the data
behavior No action It has a very rich set of behaviors and business logic

1.6 Using DP VS Not Using DP

DP is not used Use DP
API interface clarity Mixed The method signature is clear and easy to understand
Data verification and error handling The validation logic is distributed in multiple places and there is a lot of duplicate code The validation logic is cohesive and is done outside the method boundary
Clarity of business code Flooded with glue code, drowning out the core logic of the business The code is concise and clear, and the business logic is clear at a glance
Test complexity Number of TCs: NMP (N parameters, M checks for each parameter, P methods are being called) Number of TCs: N+M+P
Other benefits: Overall security, immutability, and thread safety

Second, the application architecture

2.1 Standard application architecture under the idea of DDD

The traditional MVC architecture is divided into presentation layer, business logic layer and data access layer, and pays more attention to the top-down interaction from the presentation layer to the data access layer, and the code written is like scripted code.

Based on the DDD principle, the engineering architecture is divided into the application layer, the domain layer, and the infrastructure layer. Divide the different functions and responsibilities in the project into different hierarchies. The core business logic is placed at the domain level.

DDD学习与感悟——向屎山冲锋

2.1.1 Application Layer

According to DDD, the application layer is responsible for coordinating the interaction between the user interface and the domain layer. It can be colloquially thought of as the orchestration of domain services, which does not contain any business logic itself.

2.1.2 Domain Layer

The domain layer is responsible for implementing the logic and rules of the core business. According to DDD, this layer contains entity modules, value object modules, events, and domain services.

2.1.3 Infrastructure Layer

The infrastructure layer does not process any business logic and only contains infrastructure, which usually includes databases, scheduled tasks, MQs, southbound gateways, and northbound gateways.

2.2 My understanding of evolving in and out of hexagonal architectures

2.2.1 Let's talk about the application layer

In the actual business logic, in addition to the user interface layer, there are other external systems that will call this service, such as xxljob, MQ, or provide HTTP or RPC interfaces to external systems. Therefore, in practice, the application layer should coordinate the interaction between external systems and the domain layer.

According to the standard architecture hierarchy dependencies, the application layer depends on the domain layer and the infrastructure layer. Because of the dependency on the infrastructure layer, the maintainability and testability of the application layer itself is undermined. Therefore, we need to do dependency inversion based on the interface.

In order to prevent the domain concept from leaking, the application layer needs to be further abstracted into external services and internal services, and all external services must be invoked by the domain layer through internal services. This prevents the domain model from leaking.

2.2.2 Let's talk about the domain layer

Similarly, according to the standard architecture hierarchy dependencies, the domain layer depends on the infrastructure layer, but this also undermines the maintainability and testability of the domain layer itself. Therefore, based on the idea of the repository in DDD, we abstract the repository layer and implement dependency reversal through interfaces. Let the domain layer no longer depend on the infrastructure layer. This improves the maintainability and testability of the domain layer itself.

2.2.3 Let's talk about the infrastructure layer

For the infrastructure layer, its main role is to provide infrastructure capabilities, such as databases, MQ, remote service calls, etc. Further abstraction reveals that they are ports and adapters. Interaction with external systems is achieved through ports, and data and concept conversion are completed through adapters.

2.2.4 Evolve in and out of hexagonal architectures

With dependency reversal, the magic happens. The infrastructure layer becomes the outermost layer.

DDD学习与感悟——向屎山冲锋

Combined with a further understanding of the application layer, domain layer, and infrastructure layer, and the inverted application architecture, we can get a hexagonal architecture:

DDD学习与感悟——向屎山冲锋

2.3 Where should I put the code for tools and configurations?

In a practical project, in addition to the above three layers, some utility classes (JSON parsing tools, string tools, etc.) are usually used. Utility classes may be used at all levels.

From the perspective of the positioning of the tool class, it should belong to the infrastructure layer, but the infrastructure layer belongs to the top layer, and if it is placed in the infrastructure layer, then it will break the dependency order. Therefore, in terms of logical division, we can classify the tool class as infrastructure or general domain, and in the specific engineering structure, we can put the tool class in a separate module.

In the actual project, there is another type of code that is configuration-related. From the business dimension, it can be divided into business configuration and infrastructure configuration. Therefore, we need to put it in the corresponding place according to the type of configuration. For example, in order to flexibly respond to the business, we usually configure a dynamic switch to dynamically adjust the logic of the business, and the configuration of this service switch should be placed at the domain layer. For example, the configuration of the database belongs to the infrastructure configuration, and this kind of configuration should be placed at the infrastructure layer.

2.4 My practice of the hexagonal architecture of the project

Our team's role is the foundation of the business, which includes a series of basic capacity building. For the IDaaS system, the following engineering structures are implemented based on the hexagonal architecture:

DDD学习与感悟——向屎山冲锋

3. Repository mode

3.1 What is the repository pattern?

In DDD thinking, repository represents the concept of a repository that is used to distinguish between a data model and a domain model. The object it operates on is the aggregate root, so it belongs to the domain layer.

3.2 Why use the repository pattern?

The repository pattern has two very important functions: 1. It is decoupled from the underlying storage; 2. It provides a norm for solving the anemia model.

3.3 What is the anemia model?

Due to the development of the ER model and mainstream ORM frameworks in the past, many developers still have a concept of entities at the level of mapping with relational databases. As a result, the entity only has empty attributes, and the business logic of the entity is scattered in various corners such as service, util, helper, handler, etc. This phenomenon is known as the anemia model phenomenon.

How can I tell if my project has an anemia model?

1. A large number of XxxDO or Xxx: Entity objects only contain attributes mapped to database tables, and have no or a small amount of behavior;

2. Business logic in various services, controllers, util, helpers, and handlers: The business logic of entities is scattered in different levels, different classes, and different methods, and there are a large number of repetitive codes in similar scenarios.

3.4 Why is the anemia model bad?

The integrity and consistency of entity objects cannot be guaranteed: Under the anemic model, the state and value of entity properties can only be guaranteed by the caller, but the get and set of the properties are public, so all callers can call them. Therefore, the integrity and consistency of the object cannot be guaranteed.

It is difficult to find the boundaries of the operating entity object: since the object only has attributes, the boundary value and invocation range of the attribute are not controlled by the entity itself, and can be called everywhere, and the boundary value and scope can only be guaranteed by the caller. If the boundary value of the entity changes, then all callers need to adjust it, which can easily lead to bugs.

Strong dependency on the underlying layer: entity and database model mappings, protocols, etc. under the anemia model. Therefore, if the bottom layer changes, then the upper level logic needs to be changed completely. "Software" becomes "Firmware".

To sum up: under the anemia model, the maintainability, scalability, and testability of the software are extremely poor!

扩展:
软件的可维护性=底层基础设施变化时,需要新增/修改的代码量是多少(越少可维护性越好)
软件的可扩展性=新增或变更业务逻辑时,需要新增/修改的代码量是多少(越少可扩展性越好)
软件的可测试性=每条TC执行的时长 * 新增或变更业务逻辑时产生的TC(时长越低/TC越少,测试性好)
           

3.5 In practice, why is the anemia model so difficult to eliminate?

1. Database thinking

With the development of ER and ORM frameworks, most developers think that entities are database table mappings when they are just getting started (self-study, training, etc.); As a result, it was simple to change business-oriented development to database-oriented development, and gradually thought that software development was CRUD.

2. Simple

Although some architects or developers know that the anemic model is not good, companies need to launch products quickly in order to capture the market. As a result, the construction period was greatly compressed. The anemia model happens to be simple, and in the early stages of the software, the business logic can be quickly implemented. This forces developers to "implement it before you talk". This phenomenon is also a common phenomenon in the industry.

3. Script thinking

Some developers have a certain amount of abstract thinking and write some common code into classes such as util, helper, handler, etc. But writing code is still scripting. For example, in a method, first come a field validation code, then an object conversion code, and then call the remote service to convert the result returned by the remote service,...... Finally, call the method of the Dao class to save the object. This kind of code is all too common in many projects.

Based on these factors, it is difficult to eliminate the anemia model.

What are the root causes of these factors?

The root cause is that most developers confuse the concepts of data model and domain model.

Data Model: The data model solves the problem of how data is persisted and how it is transmitted.

Domin Model: A domain refers to an independent business domain or problem space, and the domain model is a model designed to solve this business domain or problem space. It's a business domain problem that is solved.

In DDD, repository is a concept that is used to distinguish between a data model and a domain model.

3.6 How do I convert the data model to the domain model after using the repository?

DDD学习与感悟——向屎山冲锋

With the repository, both the data model and the domain model do their jobs. Conversion between models via Assembler and Converter.

In code, dynamic transformation mapping VS static transformation mapping

Although Assembler/Converter is a very easy object to use, when the business is complex, handwriting Assembler/Converter is a time-consuming and bug-prone thing, so there will be a variety of bean mapping solutions in the industry, which are essentially divided into dynamic and static mapping.

Dynamic mapping schemes include the primitive BeanUtils.copyProperties, the xml-configurable Dozer, etc., the core of which is to dynamically assign values based on reflection at runtime. The drawback of the dynamic scheme lies in the large number of reflection calls, poor performance, and large memory usage, which is not suitable for particularly high-concurrency application scenarios. However, copy tools such as BeanUtils hide the internal copy process, which is easy to cause bugs and difficult to troubleshoot.

MapStruct statically generates mapping code at compilation time through annotations, and the final compiled code is completely consistent with the handwritten code in terms of performance, and has powerful annotation capabilities. There will be significant cost savings.

3.7 Code-level model specification and comparison

OF Entity DISC
Naming conventions XxxDO Xxx XxxDTO/XxxRequest/XxxVO/XxxCommand等
Code level Infrastructure layer Domain layer Application layer
Field name standard Consistent with database fields Business language and the caller
Field type criteria This field is the same as that of the database Determine whether the underlying type or value object is based on the characteristics of the business and the caller
Whether serialization is required No, you don't No, you don't need
converter Assembler Assemble/Converter Converter

3.8 Code-level repository specification

1. Naming conventions for interface names

repository中的接口名不要使用底层存储的名称(insert、update、add、delete、query等),而是尽量使用具有业务含义的命名。 比如save、remove、find等。

2. Parameter specification of the interface

The object of the repository operation is the aggregate root. Therefore, you can only manipulate aggregate roots or entities. In this way, the underlying data model can be shielded and the data model can be prevented from penetrating into the domain layer.

Fourth, the domain layer design specifications

4.1 Entity Classes

At the heart of most DDD architectures are entity classes, which contain the state in a domain and the direct manipulation of state. The most important design principle of an entity is to ensure that the entity is invariant, that is, to ensure that no matter what the external operation is, the internal properties of an entity cannot be in conflict with each other and the state is inconsistent.

4.1.1 Creation is consistent

The constructor parameter should contain all the necessary attributes, or have a sensible default value in the constructor.

4.1.2 Use the Factory mode to reduce caller complexity

Due to the principle of consistency in creation, the method of constructing entities can be complex, so you can use the Factory pattern to quickly construct a new entity. Reduce caller complexity.

4.1.3 尽量避免public setter

One of the most common causes of inconsistency is that entities expose public's setter methods, especially if a single set parameter can lead to state inconsistencies. If you need to change the state, try to be semantic about the method name.

4.1.4 Ensure the consistency of the master and sub-entities by aggregating the root

Usually the main entity will contain subentities, and then the main entity needs to act as an aggregate root, i.e.:

  • Daughter entities cannot exist on their own, but can only be obtained by aggregating the root. No external object can directly retain a reference to a child entity
  • Child entities do not have an independent repository and cannot be stored or retrieved separately, but must be instantiated through the repository of the aggregate root
  • Child entities can modify their own state independently, but the consistency of state between multiple child entities needs to be guaranteed by the aggregate root
exp:常见的电商域中聚合的案例如主子订单模型、商品/SKU模型、跨子订单优惠、跨店优惠模型等。
           

4.1.5 Do not strongly rely on other aggregate root entities or domain services

The principle of an entity is high cohesion and low coupling, that is, an entity class cannot directly depend on an external entity or service directly internally.

对外部对象的依赖性会直接导致实体无法被单测;
以及一个实体无法保证外部实体变更后不会影响本实体的一致性和正确性。
           
Properly rely on external ways

Only the IDs of external entities are saved: Here again I strongly recommend using a strongly typed ID object, not a Long-type ID. Strongly typed ID objects not only contain their own verification code to ensure the correctness of the ID value, but also ensure that various input parameters will not be bugged due to changes in the order of parameters.

For the external dependency of "no side effects", the method is passed in the form of parameters. For example, the equip(Weapon,EquipmentService) method above.

4.1.6 The actions of any entity may only directly affect the entity (and its subentities)

This principle is more about ensuring that the code is readable and understandable, that is, the behavior of any entity cannot have a "direct" "side effect", i.e., directly modify other entity classes. The advantage of this is that the code reads without surprises.

Another reason for compliance is to reduce the risk of unknown changes. All changes to an entity object in a system should be expected, and if an entity can be modified directly from the outside, it will increase the risk of code bugs.

4.1.7 Enum can be used instead of inheritance, and the Type Object design pattern can be used to be data-driven

4.2 Domain Services

When a business logic needs to use multiple domain objects as inputs, and the output result is a value object, it means that the domain service needs to be used.

4.2.1 Single-object policy-based

This kind of domain object is mainly oriented to the change of a single entity object, but involves multiple domain objects or some rules of external dependencies.

In this case, the entity should pass in the domain service with a method input argument, and then use Double Dispatch to reverse the method that calls the domain service.

什么是Double Dispatch
exp:对于“玩家”实体而言,有一个“equip()”装备武器的方法。
按照常规思路,“玩家”实体需要注入一个EquipmentService,然而实体只能保留自己的状态,
除此之外的其他对象实体无法保证其完整性,因此我们不通过注入的方式使用EquipmentService;
而是通过方法参数引入的方式来使用。即“玩家”实体的"equip()"方法定义为:
public void equip(Weapon weapon, EquipmentService equipmentService) {
	if(equipmentService.canEquip(this, weapon)) {
			this.weaponId = weapon.getId();
	}
}
这种方式就称为Double Dispatch方式。
Double Dispatch是一个使用Domain Service经常会用到的方法,类似于调用反转。
           

4.2.2 Cross-object transactions

When a single behavior directly modifies multiple entities, it can no longer be handled by a single entity, but must be operated directly using the methods of the domain service. Here, domain services are more of a cross-object transaction, ensuring consistency between changes across multiple entities.

4.2.3 General component type

This type of domain service provides componentized behavior, but is not directly tied to an entity class itself. The benefit is that you can reduce code repetitiveness by componentizing services.

Interfaces are componentized to implement common domain services
exp:在游戏系统中,原价、NPC、怪物都是可移动的。因此可以设计一个Movable接口,
让玩家、NPC、怪物实体实现Movable接口。然后再实现一个MoveService,从而实现一个移动通用服务。
           

4.3 策略对象(Domain Policy)

The Policy or Strategy design pattern is a general design pattern, but it is often found in DDD architectures, and its core is to encapsulate domain rules.

A Policy is a stateless singleton object that typically requires at least two methods: canApply and a business method.
canApply方法用来判断一个Policy是否适用于当前的上下文,如果适用则调用方会去触发业务方法。
通常,为了降低一个Policy的可测试性和复杂度,Policy不应该直接操作对象,而是通过返回计算后的值,
在Domain Service里对对象进行操作。
           

4.4 Handling of side effects - domain events

What are the side effects?

"Side effects" are also a domain rule. A common side effect occurs when the state of the core domain model changes, synchronously or asynchronously on the influence or behavior of another object. For example, when the number of points reaches 100, the membership level will be upgraded by 1 level.

In DDD, the means of addressing the "side effects" is domain events. Domain events can be propagated through the EventBus event bus.

Deficiencies and prospects for current events in the field

Since entities need to be complete, they can't rely on EventBus directly, so EventBus can only keep a global singleton. However, it is difficult for the global singleton object to be single-tested, which makes it difficult for the Entity object to be fully covered by the complete single-test.

5. Write at the end

Through the study and practice of DDD, I can more and more realize that it is very helpful for the construction of large and complex software as a software design idea and guidance. It also provides a good guiding direction for the reconstruction of the historical Shishan project.

Read on