DDD series fifth lecture: talk about how to avoid writing journal code

DDD series fifth lecture: talk about how to avoid writing journal code

Preface

I apologize to the readers. Due to the busy work and the pursuit of article quality, the output of this article is slow, but I can assure you that the content in the article has been repeatedly practiced and stepped on. The first few articles of the DDD series can be read by clicking below the text~

DDD series first lecture

DDD series second lecture

DDD series third lecture

DDD series fourth lecture

In the past year, our team has done a lot of refactoring and migration of the old system. A lot of the code belongs to the running account code. It can usually be seen that the business logic code is written directly in the external API interface, or in a service. A large number of heap interfaces causes the business logic to actually fail to converge, and the interface reusability is poor. So this lecture mainly wants to explain systematically how to transform the original running account code into a module with clear logic and clear responsibilities through the reconstruction of DDD.

Case Introduction

Here is a simple common case: placing an order link. Suppose we are doing a checkout interface, we need to do various verifications, query product information, call the inventory service to deduct the inventory, and then generate an order:

A more typical code is as follows:

@RestController @RequestMapping("/") public class CheckoutController { @Resource private ItemService itemService; @Resource private InventoryService inventoryService; @Resource private OrderRepository orderRepository; @PostMapping("checkout") public Result<OrderDO> checkout(Long itemId, Integer quantity) { //1) Session management Long userId = SessionUtils.getLoggedInUserId(); if (userId <= 0) { return Result.fail("Not Logged In"); } //2) Parameter verification if (itemId <= 0 || quantity <= 0 || quantity >= 1000) { return Result.fail("Invalid Args"); } //3) External data completion ItemDO item = itemService.getItem(itemId); if (item == null) { return Result.fail("Item Not Found"); } //4) Invoke external services boolean withholdSuccess = inventoryService.withhold(itemId, quantity); if (!withholdSuccess) { return Result.fail("Inventory not enough"); } //5) Domain calculation Long cost = item.getPriceInCents() * quantity; //6) Domain object operations OrderDO order = new OrderDO(); order.setItemId(itemId); order.setBuyerId(userId); order.setSellerId(item.getSellerId()); order.setCount(quantity); order.setTotalCost(cost); //7) Data persistence orderRepository.createOrder(order); //8) return return Result.success(order); } } Copy code

Why does this typical running account code have problems in practical applications? The essential problem is that it violates the Single Responsbility Principle (SRP). This code is mixed with business calculations, verification logic, infrastructure, and communication protocols. In the future, no matter which part of the logic changes will directly affect this code, for a long time, when future generations continue to overlay new logic on it , It will increase the complexity of the code, more and more logic branches, and eventually cause bugs or historical baggage that no one dares to reconstruct.

Therefore, we need to refactor the above code with the layering idea of DDD. Through different code layers and specifications, we can split out layers and modules with clear logic and clear responsibilities, which also facilitate the precipitation of some common capabilities.

The main steps are divided into:

  1. Separate the independent Interface interface layer, responsible for processing network protocol-related logic
  2. From the real business scenarios, find out specific use cases (Use Cases), and then take the specific use cases through dedicated Command instructions, Query queries, and Event event objects
  3. Separate the independent Application application layer, responsible for the orchestration of business processes, and respond to Command, Query, and Event. Each application layer method should represent a node in the entire business process
  4. Handle some cross-cutting concerns across layers, such as authentication, exception handling, verification, caching, logging, etc.

The following will give a detailed explanation for each point.

Interface layer

With the popularization of REST and MVC architectures, it is often seen that development students write business logic directly in the Controller, such as the typical case above, but in fact, MVC Controller is not the only hard-hit area. The following common code writing methods may usually contain the same problem:

  • HTTP framework: such as Spring MVC framework, Spring Cloud, etc.
  • RPC framework: such as Dubbo, HSF, gRPC, etc.
  • "Consumers" of message queue MQ: such as onMessage of JMS, MessageListener of RocketMQ, etc.
  • Socket communication: receive of Socket communication, onMessage of WebSocket, etc.
  • File system: WatcherService, etc.
  • Distributed task scheduling: SchedulerX, etc.

These methods have one thing in common that they have their own network protocols, and if our business code and network protocol are mixed together, it will directly cause the code to be bound to the network protocol and cannot be reused. Therefore, in the layered architecture of DDD, we will separately extract the Interface interface layer as all external portals, decoupling network protocols and business logic.

The composition of the interface layer

The interface layer is mainly composed of the following functions:

  1. Network protocol conversion: Usually this has been encapsulated by various frameworks. The class we need to build is either an annotated bean or a bean that inherits an interface.
  2. Unified authentication: For example, in some scenarios that require AppKey+Secret, authentication is required for a certain tenant, including verification of some encrypted strings
  3. Session management: Generally, in the user-oriented interface or login state, the current calling user can be obtained through the Session or RPC context, so that it can be passed to downstream services.
  4. Current limit configuration: limit the interface to prevent large traffic from hitting downstream services
  5. Pre-caching: For read-only scenarios where changes are not frequent, the results can be pre-cached to the interface layer
  6. Exception handling: usually avoid exposing exceptions directly to the caller at the interface layer, so it is necessary to do a unified exception capture at the interface layer and convert it into a data format that the caller can understand
  7. Log: Call log on the interface layer for statistics and debugging. The general microservice framework may directly include these functions.

Of course, if you have an independent gateway facility/application, you can extract the authentication, session, current limiting, log and other logic, but at present, the API gateway can only solve a part of the function, even in the scenario where there is an API gateway Next, an independent interface layer in the application is still necessary. At the interface layer, authentication, session, current limiting, caching, logging, etc. are all relatively straightforward, and there is only one exception handling point that needs to be emphasized.

Return value and exception handling specification, Result vs Exception

Note: This part is mainly for REST and RPC interfaces, and other protocols need to generate return values according to the specifications of the protocol.

In some codes I have seen, the return value of the interface is more diverse, some directly return DTO or even DO, and some return Result. The core value of the interface layer is external, so if you just return DTO or DO, you will inevitably face exceptions and error stack leakage to the user, including the consumption of error stacks being serialized and deserialized. So, here is a specification:

Specification: HTTP and RPC interfaces of the Interface layer, the return value is Result, and all exceptions are caught

Specification: The return value of all interfaces of the Application layer is DTO, and it is not responsible for handling exceptions

The specific specifications of the Application layer will be discussed later, and the logic of the Interface layer will be shown here.

for example:

@PostMapping("checkout") public Result<OrderDTO> checkout(Long itemId, Integer quantity) { try { CheckoutCommand cmd = new CheckoutCommand(); OrderDTO orderDTO = checkoutService.checkout(cmd); return Result.success(orderDTO); } catch (ConstraintViolationException cve) { //Catch some special exceptions, such as Validation exception return Result.fail(cve.getMessage()); } catch (Exception e) { //Abnormal capture at the bottom of the pocket return Result.fail(e.getMessage()); } } Copy code

Of course, it is annoying to write exception handling logic for each interface, so you can use AOP to make an annotation

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ResultHandler { } @Aspect @Component public class ResultAspect { @Around("@annotation(ResultHandler)") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { Object proceed = null; try { proceed = joinPoint.proceed(); } catch (ConstraintViolationException cve) { return Result.fail(cve.getMessage()); } catch (Exception e) { return Result.fail(e.getMessage()); } return proceed; } } Copy code

Then the final code is simplified to:

@PostMapping("checkout") @ResultHandler public Result<OrderDTO> checkout(Long itemId, Integer quantity) { CheckoutCommand cmd = new CheckoutCommand(); OrderDTO orderDTO = checkoutService.checkout(cmd); return Result.success(orderDTO); } Copy code

The number of interfaces at the interface layer and the isolation between services

In the traditional REST and RPC interface specifications, usually the interface of a domain, whether it is the GET/POST/DELETE of REST Resource resources, or the RPC method, pursues relatively fixed, unified, and will pursue a unified domain. The method is placed in a service or Controller in a domain.

However, I found that in the actual business process, especially when the supporting upstream business is relatively large, deliberately pursuing the unification of the interface usually leads to the expansion of the parameters in the method, or the expansion of the method. For example: suppose there is a pet card and a parent-child card business sharing the same card opening service, but the pet needs to be imported in the pet type, and the parent-child needs to be imported in the baby's age.

//can be RPC Provider or Controller public interface CardService { //1) Unified interface, parameter expansion Result openCard(int petType, int babyAge); //2) Unified generalized interface, parameter semantics is lost Result openCardV2(Map<String, Object> params); //3) No generalization, expansion of interfaces in the same class Result openPetCard(int petType); Result openBabyCard(int babyAge); } Copy code

It can be seen that no matter how it is operated, it may cause the CardService service to become more and more difficult to maintain in the future. There are more and more methods. A business change may cause the entire service/Controller to change and eventually become unmaintainable. A service I have participated in provides dozens of methods and tens of thousands of lines of code. It is conceivable that both the user's understanding of the interface and the maintenance cost of the code are extremely high. So, here is another specification:

Specification: A class in the Interface layer should be "small and beautiful", and should be oriented to "a single business" or "a type of business with the same needs". It is necessary to avoid using the same class to undertake the needs of different types of business.

Based on the above specification, it can be found that although pet cards and parent-child cards look like similar needs, they are not "same needs". It can be foreseen that at some point in the future, the needs and needs of these two businesses will be provided. The interface will go further and further, so you need to separate these two interface classes:

public interface PetCardService { Result openPetCard(int petType); } public interface BabyCardService { Result openBabyCard(int babyAge); } Copy code

The advantage of this is that it conforms to the Single Responsibility Principle, which means that an interface class will only change due to changes in one (or one type of) business. One suggestion is that when an existing interface class is over-expanded, you can consider splitting the interface class. The principle of splitting is consistent with SRP.

Someone may ask, if we follow this approach, will a large number of interface classes be generated, resulting in duplication of code logic? The answer is no, because in the DDD layered architecture, the core role of the interface class is only the protocol layer, the protocol of each type of business can be different, and the real business logic will be deposited in the application layer. In other words, the relationship between Interface and Application is many-to-many:

Because business requirements change rapidly, the interface layer must also change rapidly. Through independent interface layers, mutual influence between businesses can be avoided, but we hope that the logic of the Application layer is relatively stable. So let's take a look at some specifications of the Application layer.

Application layer

Components of the Application layer

Several core classes of the Application layer:

  • ApplicationService application service: the core class, responsible for the orchestration of business processes, but not responsible for any business logic itself
  • DTO Assembler: responsible for transforming the internal domain model into an external DTO
  • Command, Query, Event objects: as input parameters of ApplicationService
  • Returned DTO: as the output parameter of ApplicationService

The core object of the Application layer is ApplicationService, and its core function is to undertake "business processes". But before talking about the specification of ApplicationService, we must first focus on several special types of objects, namely Command, Query, and Event.

Command, Query, Event objects

Essentially, these types of objects are Value Objects, but there are big differences in semantics:

  • Command instruction: refers to the instruction that the caller clearly wants the system to operate, and its expectation is to affect a system, that is, write operation. Generally speaking, the instruction needs to have a clear return value (such as the result of a synchronous operation, or an asynchronous instruction has been accepted).
  • Query query: Refers to what the caller clearly wants to query, including query parameters, filtering, paging and other conditions. The expectation is that it will not affect the data of a system at all, that is, read-only operation.
  • Event event: refers to an existing fact that has occurred, and the system needs to change or respond based on this fact. Generally, event processing will involve certain write operations. The event handler will not have a return value. It should be noted here that the Event concept of the Application layer and the DomainEvent of the Domain layer are similar concepts, but not necessarily the same thing. The Event here is more of an external notification mechanism.

To summarize briefly:

CommandQueryEvent
Semantics"Hope" can trigger actionsVarious conditions of inquiryWhat has happened
Read/writewriteRead onlyUsually write
return valueDTO or BooleanDTO or CollectionVoid

Why use CQE objects?

Usually in many codes, you can see that there are multiple parameters on the interface, such as the above case:

Result<OrderDO> checkout(Long itemId, Integer quantity); copy code

If you need to add parameters to the interface, considering the forward compatibility, you need to add a method:

Result<OrderDO> checkout(Long itemId, Integer quantity); Result<OrderDO> checkout(Long itemId, Integer quantity, Integer channel); Copy code

Or common query methods, due to different conditions, lead to multiple methods:

List<OrderDO> queryByItemId(Long itemId); List<OrderDO> queryBySellerId(Long sellerId); List<OrderDO> queryBySellerIdWithPage(Long sellerId, int currentPage, int pageSize); Copy code

It can be seen that there are several problems with the traditional interface writing:

  1. Interface expansion: one query condition, one method
  2. Difficult to expand: each new parameter may require the caller to upgrade
  3. Difficult to test: With more interfaces, responsibilities become complicated, business scenarios vary, and test cases are difficult to maintain

But the other most important problem is that this type of parameter listing does not have any business "semantics", it is just a bunch of parameters, and cannot clearly express the intention.

CQE specifications:

Therefore, in the interface of the Application layer, a strongly recommended specification is:

Specification: The input parameter of the ApplicationService interface can only be a Command, Query or Event object, and the CQE object needs to be able to represent the semantics of the current method. The only possible exception is based on a single ID query, you can omit the creation of a Query object

According to the above specification, the implementation case is:

public interface CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd); List<OrderDTO> query(OrderQuery query); OrderDTO getOrder(Long orderId);//Note that a single ID query can be without Query } @Data public class CheckoutCommand { private Long userId; private Long itemId; private Integer quantity; } @Data public class OrderQuery { private Long sellerId; private Long itemId; private int currentPage; private int pageSize; } Copy code

The benefits of this specification are: it improves the stability of the interface, reduces low-level repetition, and makes the interface input more semantic.

CQE vs DTO

It can be seen from the above code that the input parameter of ApplicationService is a CQE object, but the output parameter is a DTO. From the code format, they are all simple POJO objects. So what is the difference between them?

  • CQE: The CQE object is the input of ApplicationService and has a clear "intent", so this object must ensure its "correctness".
  • DTO: DTO object is just a data container, just for interacting with the outside, so it does not contain any logic itself, it is just an anemic object.

But perhaps the most important point: because CQE is "intent", CQE objects can theoretically have "infinite", each representing a different intent; but DTO, as a model data container, corresponds to the model one-to-one, so it is limited of.

CQE verification

As the input of ApplicationService, CQE must ensure its correctness, so where is this check? In the earliest code, there was such a verification logic, which was written in the service at that time:

if (itemId <= 0 || quantity <= 0 || quantity >= 1000) { return Result.fail("Invalid Args"); } Copy code

This kind of code is very common in daily life, but its biggest problem is that a large amount of non-business code is mixed in business code, which obviously violates the single responsibility principle. But because the input was just a simple int at that time, this logic can only appear in the service. Now when the input parameter has been changed to CQE, we can use the Bean Validation of Java standard JSR303 or JSR380 to precede this validation logic.

Specification: The verification of CQE objects should be pre-checked to avoid parameter verification in ApplicationService. Can be achieved through JSR303/380 and Spring Validation

The previous example can be transformed into:

@Validated//Spring's annotation public class CheckoutServiceImpl implements CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd) {//Here @Valid is the annotation of JSR-303/380 //If the verification fails, an exception will be thrown and captured at the interface layer } } @Data public class CheckoutCommand { @NotNull(message = "User not logged in") private Long userId; @NotNull @Positive(message = "Need to be a valid itemId") private Long itemId; @NotNull @Min(value = 1, message = "at least 1 piece") @Max(value = 1000, message = "Cannot exceed 1000 pieces at most") private Integer quantity; } Copy code

The advantage of this approach is that it makes ApplicationService more refreshing, and various error messages can be customized through the Bean Validation API.

Avoid multiplexing CQE

Because CQE has "intention" and "semantics", we need to avoid the reuse of CQE objects as much as possible, even if all the parameters are the same, as long as their semantics are different, try to use different objects as much as possible.

Specification: For instructions with different semantics, avoid reuse of CQE objects

Counter-example: A common scenario is "Create" and "Update". Generally speaking, the only difference between these two types of objects is an ID. There is no ID for creation, but there is for update. Therefore, it is often seen that some students use the same object as the input parameter of the two methods. The only difference is whether the ID is assigned. This is an incorrect usage, because the semantics of these two operations are completely different, and their verification conditions may also be completely different, so the same object should not be reused. The correct approach is to produce two objects:

public interface CheckoutService { OrderDTO checkout(@Valid CheckoutCommand cmd); OrderDTO updateOrder(@Valid UpdateOrderCommand cmd); } @Data public class UpdateOrderCommand { @NotNull(message = "User not logged in") private Long userId; @NotNull(message = "OrderID must be present") private Long orderId; @NotNull @Positive(message = "Need to be a valid itemId") private Long itemId; @NotNull @Min(value = 1, message = "at least 1 piece") @Max(value = 1000, message = "Cannot exceed 1000 pieces at most") private Integer quantity; } Copy code

ApplicationService

ApplicationService is responsible for the orchestration of the business process. It is the "glue layer" code that is the remaining process after the original business account code has been stripped of the verification logic, domain calculation, persistence and other logic.

Refer to a simple transaction process:

In this case, it can be seen that there are 5 use cases in the transaction field: order placement, payment success, payment failure closing order, logistics information update, and order closing. These 5 use cases can be replaced by 5 Command/Event objects, which corresponds to 5 methods.

I have seen 3 organizational forms of ApplicationService:

  1. An ApplicationService class is a complete business process, in which each method is responsible for handling a Use Case. The advantage of this is that the entire business logic can be completely converged, and the business logic can be mastered from the interface class, which is suitable for relatively simple business processes. The disadvantage is that complex business processes will lead to too many methods in a class, and there may be too much code. Examples of specific cases of this type:
public interface CheckoutService { //place an order OrderDTO checkout(@Valid CheckoutCommand cmd); //payment successful OrderDTO payReceived(@Valid PaymentReceivedEvent event); //payment cancellation OrderDTO payCanceled(@Valid PaymentCanceledEvent event); //Ship OrderDTO packageSent(@Valid PackageSentEvent event); //Receiving OrderDTO delivered(@Valid DeliveredEvent event); //Batch query List<OrderDTO> query(OrderQuery query); //single query OrderDTO getOrder(Long orderId); } Copy code
  1. For more complex business processes, you can reduce the amount of code in a class by adding independent CommandHandler and EventHandler:
@Component public class CheckoutCommandHandler implements CommandHandler<CheckoutCommand, OrderDTO> { @Override public OrderDTO handle(CheckoutCommand cmd) { // } } public class CheckoutServiceImpl implements CheckoutService { @Resource private CheckoutCommandHandler checkoutCommandHandler; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { return checkoutCommandHandler.handle(cmd); } } Copy code
  1. A bit more radical, through CommandBus, EventBus, directly throw instructions or events to the corresponding Handler, EventBus is more common. The specific case code is as follows. After receiving the MQ message through the message queue, an Event is generated, and then the EventBus is routed to the corresponding Handler:
//Application layer //Here the framework can usually identify this responsible for processing PaymentReceivedEvent according to the interface //It can also be recognized by adding annotations @Component public class PaymentReceivedHandler implements EventHandler<PaymentReceivedEvent> { @Override public void process(PaymentReceivedEvent event) { // } } //Interface layer, this is the Listener of RocketMQ public class OrderMessageListener implements MessageListenerOrderly { @Resource private EventBus eventBus; @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { PaymentReceivedEvent event = new PaymentReceivedEvent(); eventBus.dispatch(event);//no need to specify consumers return ConsumeOrderlyStatus.SUCCESS; } } Copy code

Not recommended: This approach can achieve complete static decoupling of the Interface layer and a specific ApplicationService or Handler, and dynamically dispatch at runtime. A better framework such as AxonFramework can be done. Although it seems very convenient, according to our own business practice and pitting, when there are more and more CQE objects in the code and the handler becomes more and more complicated, the dispatch at runtime lacks the association relationship between the static codes, resulting in The code is difficult to read, especially when you need to trace a complex call chain, because dispatch is run-time, it is difficult to figure out the specific call object. So although we have tried this before, it is no longer recommended.

Application Service is an encapsulation of business processes and does not deal with business logic

Although it has been repeated countless times before that ApplicationService is only responsible for business process series, not business logic, but how to judge whether a piece of code is business process or logic? Take the previous example, after the initial code refactoring: Determine whether there are several points in the business process:

  1. Do not have if/else branch logic: that is to say, the Cyclomatic Complexity of the code should be equal to 1 as much as possible

Usually there is branch logic, which represents some business judgment, and the logic should be encapsulated in DomainService or Entity. But this does not mean that there is no if logic at all. For example, in this code: boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity()); if (!withholdSuccess) {throw new IllegalArgumentException("Inventory not enough");} Although CC> 1, it only represents an interruption condition, and the specific business logic processing is not affected. Think of it as Precondition.

@Service @Validated public class CheckoutServiceImpl implements CheckoutService { private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE; @Resource private ItemService itemService; @Resource private InventoryService inventoryService; @Resource private OrderRepository orderRepository; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { ItemDO item = itemService.getItem(cmd.getItemId()); if (item == null) { throw new IllegalArgumentException("Item not found"); } boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity()); if (!withholdSuccess) { throw new IllegalArgumentException("Inventory not enough"); } Order order = new Order(); order.setBuyerId(cmd.getUserId()); order.setSellerId(item.getSellerId()); order.setItemId(item.getItemId()); order.setItemTitle(item.getTitle()); order.setItemUnitPrice(item.getPriceInCents()); order.setCount(cmd.getQuantity()); Order savedOrder = orderRepository.save(order); return orderDtoAssembler.orderToDTO(savedOrder); } } Copy code
  1. Do not have any calculations:

There is this calculation in the earliest code:

//5) Domain calculation Long cost = item.getPriceInCents() * quantity; order.setTotalCost(cost); Copy code

By encapsulating this calculation logic into the entity, avoid doing calculations in ApplicationService

@Data public class Order { private Long itemUnitPrice; private Integer count; //Migrate the original calculation in ApplicationService to Entity public Long getTotalCost() { return itemUnitPrice * count; } } order.setItemUnitPrice(item.getPriceInCents()); order.setCount(cmd.getQuantity()); Copy code
  1. Some data conversion can be handed over to other objects to do:

For example, DTO Assembler, which deposits the logic of transformation between objects in a separate class, reduces the complexity of ApplicationService

OrderDTO dto = orderDtoAssembler.orderToDTO (savedOrder); duplicated code

Commonly used ApplicationService "routines"

We can see that the code of ApplicationService usually has a similar structure: AppService usually does not make any decisions (except Precondition), but only hands all decisions to DomainService or Entity, and hands the external interaction to the Infrastructure interface, such as Repository or anti-corrosion. Floor.

The general "routine" is as follows:

  • Prepare data: including taking out the corresponding Entity, VO and DTO returned by the external service from the external service or persistence source.
  • Perform operations: including the creation and assignment of new objects, as well as invoking methods of domain objects to operate on them. It should be noted that this time is usually pure memory operations, non-persistent.
  • Persistence: Persistence of operation results, or operation of external systems to produce corresponding effects, including asynchronous operations such as sending messages.

If it involves changes to multiple external systems (including its own DB), this time is usually in a "distributed transaction" scenario, whether it is using distributed TX, TCC, or Saga mode, depending on the specific The design of the scene is temporarily skipped here.

DTO Assembler

An often overlooked question is whether ApplicationService should return Entity or DTO? Here is a specification, in the DDD layered architecture:

ApplicationService should always return DTO instead of Entity

why?

  1. Constructing the domain boundary: The input parameter of ApplicationService is the CQE object, and the output parameter is the DTO. These are basically simple POJOs to ensure that the inside and outside of the Application layer do not affect each other.
  2. Reduce rule dependence: Entity usually contains business rules. If ApplicationService returns Entity, it will cause the caller to directly rely on business rules. If the internal rules are changed, it may directly affect the outside.
  3. Cost reduction through DTO combination: Entity is limited. DTO can be a free combination of multiple Entity and VO, which can be packaged into a complex DTO at one time, or some parameters can be selectively packaged into DTO to reduce external costs.

Because the object we operate is Entity, but the output object is DTO, here we need a special type of object called DTO Assembler. The only responsibility of the DTO Assembler is to convert one or more Entity/VO into DTO. Note: DTO Assembler usually does not recommend reverse operations, that is, from DTO to Entity, because usually a DTO is converted to Entity, the accuracy of Entity cannot be guaranteed.

Generally, there is a cost to Entity to DTO, whether it is the amount of code or the operation of the runtime. Handwriting conversion code is prone to errors. In order to save the amount of code, using Reflection will cause a great performance loss. So here I still spare no effort to recommend the MapStruct library. MapStruct generates code during static compilation. Corresponding code can be generated by writing interfaces and configuration annotations, and because the generated code is directly assigned, its performance loss is basically negligible.

With MapStruct, the code can be simplified to:

import org.mapstruct.Mapper; @Mapper public interface OrderDtoAssembler { OrderDtoAssembler INSTANCE = Mappers.getMapper(OrderDtoAssembler.class); OrderDTO orderToDTO(Order order); } public class CheckoutServiceImpl implements CheckoutService { private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { //... Order order = new Order(); //... Order savedOrder = orderRepository.save(order); return orderDtoAssembler.orderToDTO(savedOrder); } } Copy code

Combined with the previous Data Mapper, the relationship between DTO, Entity and DataObject is as follows:

Result vs Exception

Finally, it was mentioned above that Result should be returned in the Interface layer, and DTO should be returned in the Application layer. Here again, the specification is repeated:

The Application layer only returns DTO and can throw exceptions directly without uniform processing. All called services can also throw exceptions directly, unless special handling is required, otherwise there is no need to deliberately catch exceptions

The advantage of exceptions is that they can clearly know the source of the error, stack, etc. The purpose of catching exceptions in the Interface layer is to prevent the exception stack information from leaking outside the API, but in the Application layer, the exception mechanism is still the largest amount of information and the clearest code structure The method avoids some common and complicated Result.isSuccess judgments of Result. Therefore, in the Application layer, Domain layer, and Infrastructure layer, it is the most reasonable method to throw an exception directly when encountering an error.

Briefly talk about Anti-Corruption Layer anti-corrosion layer

This article only briefly describes the principles and functions of ACL, the specific implementation specifications may have to wait until another article.

In ApplicationService, external services are often relied upon, which relies on external systems from the code level. For example, in the above:

ItemDO item = itemService.getItem(cmd.getItemId()); boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity()); Copy code

We will find that our ApplicationService will strongly depend on the ItemService, InventoryService and ItemDO objects. If the method of any service changes, or the ItemDO field changes, it may affect the code of ApplicationService. In other words, our own code will be changed due to strong reliance on changes in the external system. This should be avoided as much as possible in a complex system. So how to isolate the external system? Need to add ACL anti-corrosion layer.

The simple principle of ACL anti-corrosion layer is as follows:

  • For dependent external objects, we extract the required fields and generate an internally required VO or DTO class
  • Construct a new facade, encapsulate the call link in the facade, and convert the external class to the internal class
  • For external system calls, the same facade method is used to encapsulate external call links

Without anti-corrosion layer:

With anti-corrosion layer:

Specific and simple implementation, assuming that all external dependencies are named ExternalXXXService:

//Custom internal value class @Data public class ItemDTO { private Long itemId; private Long sellerId; private String title; private Long priceInCents; } //Commodity Facade interface public interface ItemFacade { ItemDTO getItem(Long itemId); } //Commodity facade realization @Service public class ItemFacadeImpl implements ItemFacade { @Resource private ExternalItemService externalItemService; @Override public ItemDTO getItem(Long itemId) { ItemDO itemDO = externalItemService.getItem(itemId); if (itemDO != null) { ItemDTO dto = new ItemDTO(); dto.setItemId(itemDO.getItemId()); dto.setTitle(itemDO.getTitle()); dto.setPriceInCents(itemDO.getPriceInCents()); dto.setSellerId(itemDO.getSellerId()); return dto; } return null; } } //Inventory Facade public interface InventoryFacade { boolean withhold(Long itemId, Integer quantity); } @Service public class InventoryFacadeImpl implements InventoryFacade { @Resource private ExternalInventoryService externalInventoryService; @Override public boolean withhold(Long itemId, Integer quantity) { return externalInventoryService.withhold(itemId, quantity); } } Copy code

After the ACL transformation, the code of our ApplicationService is changed to:

@Service public class CheckoutServiceImpl implements CheckoutService { @Resource private ItemFacade itemFacade; @Resource private InventoryFacade inventoryFacade; @Override public OrderDTO checkout(@Valid CheckoutCommand cmd) { ItemDTO item = itemFacade.getItem(cmd.getItemId()); if (item == null) { throw new IllegalArgumentException("Item not found"); } boolean withholdSuccess = inventoryFacade.withhold(cmd.getItemId(), cmd.getQuantity()); if (!withholdSuccess) { throw new IllegalArgumentException("Inventory not enough"); } //... } } Copy code

Obviously, the advantage of doing this is that the code of ApplicationService no longer directly depends on external classes and methods, but on our own internally defined value classes and interfaces. If there are any changes to the external service in the future, it is the facade class and data conversion logic that need to be modified, rather than the logic of the ApplicationService.

Repository can be considered as a special ACL that shields the details of specific data operations. Even if the underlying database structure changes, the database type changes, or other persistence methods are added, the Repository interface remains stable and the ApplicationService can remain unchanged.

In some theoretical frameworks, ACL Facade is also called Gateway, which has the same meaning.

Orchestration vs Choreography

At the end of this article, I want to talk about the design specifications of complex business processes. In complex business processes, we usually face two modes: Orchestration and Choreography. I feel helpless. The Baidu Translate/Google Translate of these two English words are both "orchestration", but in fact these two models are completely different design models. Orchestration orchestration (such as SOA/microservice service orchestration Service Orchestration) is a familiar usage. Choreography has only recently become popular with the emergence of event-driven architecture EDA. There may be other translations on the Internet, such as compilation, choreography, collaboration, etc., but they don t really express the meaning of English words, so in order to avoid misunderstandings, I will try to use the original English words below. If anyone has a better translation method, please contact me.

Introduction to Mode

Orchestration: What usually comes to mind is a symphony orchestra (Orchestra, pay attention to the similarity of these two words), as shown in the figure below. The core of the symphony orchestra is a sole conductor Conductor. In a symphony, all musicians must obey the Conductor's conduct and cannot play alone. So in the Orchestration model, all processes are triggered by a node or service. Our common business process code, including calling external services, is Orchestration, which is triggered by our services.

Choreography: The scene that usually comes to mind is a dance drama (from the Greek dance, Choros), as shown below. Each of the different dancers is doing their own thing, but there is no centralized command. Through cooperation and cooperation, everyone does their own thing, and the whole dance can show a complete and harmonious picture. So in Choreography mode, each service is an independent individual, and may respond to some external events, but the entire system is a whole.

Case study

Take a common example: pay and ship after the order is placed. If this case is Orchestration, the business logic is: when the order is placed, funds are deducted from a pre-stored account, and a logistics order is generated for delivery, as shown in the figure. of:

If this case is Choreography, the business logic is: place an order, then wait for the payment success event, and then ship, similar to this:

The difference and choice of modes

Although it seems that these two models can achieve the same business purpose, there are huge differences in actual development:

From the perspective of code dependencies:

  • Orchestration: It involves calling another service from one service. For the caller, it is a strongly dependent service provider.
  • Choreography: Each service just does its own thing, and then triggers other services through events. There is no direct call dependency between services. But it should be noted that downstream will still rely on upstream code (such as event classes), so it can be considered that downstream depends on upstream.

From the point of view of code flexibility:

  • Orchestration: Because the dependencies between services are hard-coded, adding new business processes will inevitably require code modification.
  • Choreography: Because there is no direct call relationship between services, services can be added or replaced without changing the upstream code.

From the point of view of the call link:

  • Orchestration: Actively call another service from one service, so it is driven by Command-Driven instructions.
  • Choreography: Each service is passively triggered by external events, so it is driven by Event-Driven events.

From the perspective of business responsibilities:

  • Orchestration: There is an active caller (for example: ordering service). No matter who the downstream dependency is, the active caller needs to be responsible for the entire business process and results.
  • Choreography: There is no active caller, each service only cares about its own trigger conditions and results, no one service will be responsible for the entire business link

To sum up a comparison:

OrchestrationChoreography
Driving forceCommand-DrivenEvent-Driven
Call dependencyUpstream strongly depends on downstreamNo direct call dependency but code dependency can be considered as downstream dependency upstream
flexibilityPoorHigher
Business responsibilitiesUpstream responsible for the businessNo overall responsibility

In addition, it needs to be clear: the difference between "instruction-driven" and "event-driven" is not "synchronous" and "asynchronous". The instruction can be a synchronous call, or it can be triggered by an asynchronous message (but an asynchronous instruction is not an event); in turn, an event can be an asynchronous message, but it can also be a synchronous call within a process. Therefore, the essence of the difference between instruction-driven and event-driven is not the calling method, but whether something "has" happened.

So when you encounter a requirement in daily business, how do you choose to use Orchestration or Choreography?

Here are two judgment methods:

  1. Clarify the direction of dependence:

The dependence in the code is relatively clear: if you are downstream and the upstream does not perceive you, you can only use event-driven; if the upstream must be aware of you, you can use instruction-driven. Conversely, if you are upstream and need to rely heavily on downstream, it is instruction-driven; if it doesn't matter who the downstream is, you can use event-driven.

  1. Find the "person in charge" in the business:

The second method is to find the "person in charge" according to the business scenario. For example, if the business needs to notify the seller, the single responsibility of the ordering system should not be responsible for the message notification, but the order management system needs to actively trigger the message according to the advancement of the order status, so it is the person in charge of this function. In a complex business process, there are usually two modes, but it is also easy to design errors. If the dependency relationship is strange, or the call link/person in charge in the code is not clear about the situation, you can try to change the mode, which may be much better.

Which model is better?

Obviously, there is no best model, only the model that best suits your business scenario.

Counter-example: In recent years, the more popular Event-Driven Architecture (EDA) event-driven architecture, and Reactive-Programming (such as RxJava), although there are many innovations, but to a certain extent, "When you have a hammer, All problems are typical cases of "nails". They have a miraculous effect on some event-based, stream processing problems, but if they use these frameworks to set instruction-driven services, they will feel that the code is extremely "uncoordinated" and the cognitive cost will increase. Therefore, in daily selection, we must first sort out which parts of the process are Orchestration and which are Choreography according to the business scenario, and then choose the corresponding framework.

Relationship with DDD layered architecture

Finally, after talking about so much O vs C, what does it have to do with DDD? It's simple:

  • O&C is actually the focus of the Interface layer, Orchestration = external API, and Choreography = message or event. After you decide O or C, you need to take on these "driving forces" at the interface layer.
  • No matter how the O&C is designed, the Application layer is "unaware", because ApplicationService is inherently capable of processing Command, Query, and Event. As for how these objects come from, it is the decision of the Interface layer.

Therefore, although Orchestration and Choreography are two completely different business design patterns, the code that ultimately falls on the Application layer should be the same. This is why the Application layer is a "use case" rather than an "interface", which is relatively stable.

summary

As long as you are doing business, you will definitely need to write business processes and service orchestration, but it does not mean that this kind of code must be of poor quality. Through the reasonable separation of the Interface layer and the Application layer in the DDD's layered architecture, the code can become elegant and flexible, which can respond to business faster but at the same time can be better precipitated. This article mainly introduces some code design specifications to help you master certain skills.

Interface layer:

  • Responsibilities: Mainly responsible for undertaking network protocol conversion, Session management, etc.
  • Number of interfaces: To avoid the so-called unified API, there is no need to artificially limit the number of interface classes. Each/each type of business corresponds to a set of interfaces. The interface parameters should meet the business needs and avoid large and comprehensive input parameters.
  • Interface parameter: return Result uniformly
  • Exception handling: All exceptions should be caught to avoid the leakage of exception information. It can be processed uniformly through AOP to avoid a lot of repetitive code in the code.

Application layer:

  • Input parameters: The actualized Command, Query, and Event objects are used as the input parameters of ApplicationService. The only possible exception is the scenario of single ID query.
  • Semanticization of CQE: CQE objects have semantic meanings, and different use cases have different semantics. Avoid reuse even if the parameters are the same.
  • Entry validation: basic validation is solved through Bean Validation api. Spring Validation comes with Validation AOP, you can also write your own AOP.
  • Outcome: Return DTO uniformly instead of Entity or DO.
  • DTO conversion: use DTO Assembler to be responsible for the conversion of Entity/VO to DTO.
  • Exception handling: exceptions are not captured uniformly, and exceptions can be thrown at will.

Part of the Infra layer:

  • Use ACL anti-corrosion layer to convert external dependencies into internal codes to isolate external influences

Business process design pattern:

  • There is no best model, depending on the business scenario, dependencies, and whether there is a business "person in charge". Avoid using a hammer to find nails.

Preview

  • CQRS is a design pattern of the Application layer. It is a design concept based on the separation of Command and Query, from the simplest object separation to the most complex Event-Sourcing at present. This topic has many points that need to be in-depth, and it can often be used, especially in combination with complex Aggregates. I will pull it out separately later, and the title is tentatively set as "The 7th Level of CQRS"
  • In today's complex microservice development environment, it is inevitable to rely on services developed by external teams, but the cost of strong coupling (whether it is changes, code dependencies, or even indirect dependencies on Maven Jar packages) is a complex system that cannot be ignored for a long time. Point. The ACL anti-corrosion layer is an isolation concept that removes external coupling and makes the internal code more pure. There are many types of ACL anti-corrosion layers. Repository is a special ACL for face data persistence. K8S-sidecar-istio can be said to be a network layer ACL, but it can be more efficient and more efficient than Istio in Java/Spring. The general method will be introduced later.
  • When you start to use DDD, you will find that many code patterns are very similar. For example, the main and sub-orders are the total score pattern, the CPV pattern of the category system can also be used for some activities, the ECS pattern can play a role in interactive business, etc. . Later, I will try to summarize some common domain design patterns, their design ideas, the types of problems that can be solved, and the methods of practice.

Welcome to contact, keep asking for resume

Welcome to see the students here to ask me any questions about DDD, I will answer as much as possible. The code case in the article will be applied for and published on github later for your reference. My email: guangmiao.lgm@alibaba-inc.com , you can also add my nail number: luangm (Yin Hao)

At the same time, our team is also continuing to recruit. My team is responsible for the Taobao industry and shopping guide business, including the four major industries of Tmall and Taobao (apparel, FMCG, consumer, home improvement) and several large horizontal businesses of Taobao (corporate services, global shopping, good goods, etc.) ) Daily business needs and innovative business (3D/AR, 360 panoramic video, matching, customization, size shopping guide, SPU shopping guide, etc.), front desk (iFashion, global shopping, good goods, etc.), as well as some complex finances and transactions , Performance link (IP matching, financial services, transaction customization, distribution, CPS commission, service supply chain docking, etc.), the total DAU (daily average number of users) is about 3000W. Our team has docked a large number of business forms, from front-end shopping guide to back-end contract fulfillment, with extremely rich application scenarios. In the new fiscal year, we hope to penetrate the industry, explore new business models and fulfillment links, cover some business models that cannot be covered by the traditional B2C model, and help businesses grow on the new track. Interested students are welcome to join us.

Author|Yin Hao

Edit|Orange

Produced | Alibaba's new retail technology