This is the first article in the Domain-driven Design series. This series of tutorials will take a look at key domain-driven concepts and put a domain-driven Sass software into practice using Spring Data Jpa.
Software is a tool created to help us deal with complex problems in modern life. DDD (Domain-driven Design) is a Design method used to Design software. At the beginning of software design, we need to define the software to solve the problem of a certain business scenario, called the Domain in DDD.
Generally, technical personnel are not familiar with the domain knowledge of the new software project, and those who know the business knowledge of the domain are called domain experts in DDD (usually software designers, but may also be sales, technical support and other business requirements gathering people). Technical staff and domain experts need to use Ubiquitous Language for business communication, such as UML, requirements documents, etc., which are specified in advance and can be understood by both parties.
The ultimate goal of domain-driven design DDD is to obtain the Domain Model that software design complies with. Domain Model is the software Model about a specific business Domain, which is usually realized by using the object Model in the program. The data and behavior of the object accurately express the business meaning of the Domain.
What can DDD bring to team development?
Using DDD enables business people to accurately communicate business rules to technical people. Business people communicate with technical people more easily.
Business knowledge also takes time to learn, and DDD helps centralize business knowledge so that software business knowledge is not just in the hands of a few people.
The domain model extracted by DDD makes the business simple and clear, easy to understand, learn and maintain in the later stage, fast development and strong substitutability of personnel.
DDD basis
Before we dive into DDD, let’s take a look at the current common development approach (Java for example). Most Java developers today develop with an anemia model, and we’ll explain what an anemia model is and how it differs from a congestion model.
Anemia model
An anaemic Domain model is one in which only setters and getters (POJOs) are used in the domain objects, and all business logic is not contained in the domain objects but in the business logic layer Services. Anaemic model, currently the vast majority of developers are using this model for development.
An anaemic model is a domain model that contains no logic and is simply a container for data that can be changed and interpreted by clients. All logic is placed outside the domain objects in the anemia model.
The following example uses the order class to demonstrate an anemia model
public class Order { private BigDecimal total = BigDecimal.ZERO; private List<OrderItem> items = new ArrayList<OrderItem>(); public BigDecimal getTotal() { return total; } public void setTotal(BigDecimal total) { this.total = total; } public List<OrderItem> getItems() { return items; } public void setItems(List<OrderItem> items) { this.items = items; } } public class OrderItem { private BigDecimal price = BigDecimal.ZERO; private int quantity; private String name; public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity= quantity; }... }Copy the code
The anaemia domain service used to calculate the total number of orders might look like this
public class OrderService { public void calculateTotal(Order order) { if (order == null) { throw new IllegalArgumentException("order must not be null"); } BigDecimal total = BigDecimal.ZERO; List<OrderItem> items = order.getItems(); for (OrderItem orderItem : items) { int quantity = orderItem.getQuantity(); BigDecimal price = orderItem.getPrice(); BigDecimal itemTotal = price.multiply(new BigDecimal(quantity)); total = total.add(itemTotal); } order.setTotal(total); }}Copy the code
The logic for anaemic domain model changes and interpretation of data is elsewhere. In most cases, the logic is placed in xxxService. The package that holds xxxService is called the service layer. The service layer defines the boundaries of the application, establishes a set of available operations, and coordinates the response of the application within each operation. From an architectural perspective, the “services” that keep business logic in an anemic model are transaction scripts.
Transaction scripts: You can think of most business applications as a series of transactions, which basically organize all business logic into a process, organized by process, where each process processes one kind of request.
The anaemia model Service for calculating the total price of an order is as follows
public class OrderService { public void calculateTotal(Order order) { if (order == null) { throw new IllegalArgumentException("order must not be null"); } BigDecimal total = BigDecimal.ZERO; List<OrderItem> items = order.getItems(); for (OrderItem orderItem : items) { int quantity = orderItem.getQuantity(); BigDecimal price = orderItem.getPrice(); BigDecimal itemTotal = price.multiply(new BigDecimal(quantity)); total = total.add(itemTotal); } order.setTotal(total); }}Copy the code
You might think this is clear and simple, because it’s procedural programming. But it is also deadly, because anemic models can never be guaranteed to be correct. The anaemic model has no logic to ensure that it is always legal. For example, the order object does not react to changes to its list of items, so it cannot update its totals and needs to be handled manually.
Objects unite data and logic, while anemic models separate them. This contradicts basic object-oriented principles such as encapsulation and information hiding.
The following example illustrates why the anemia model does not guarantee legal status
Public class OrderTest {/** * The anemia model may be inconsistent because it does not handle state changes. * * When using the anaemic model the developer must know which state the object operation should change to, * Manually change the appropriate state, * * / @ Test to guarantee the legal status of public void anAnemicModelCanBeInconsistent () {OrderService OrderService = new OrderService (); Order order = new Order(); BigDecimal total = order.getTotal(); /* * The order has no order item, so the total price of the order must be 0 */ assertEquals(BigDecimal.ZERO, total); OrderItem aGoodBook = new OrderItem(); aGoodBook.setName("Domain-Driven"); aGoodBook.setPrice(new BigDecimal("30")); aGoodBook.setQuantity(5); /* * Here we break object encapsulation because we change the internal state of the order item list * this is a common programming pattern for anaemic models */ order.getitems ().add(aGoodBook); /* * The Order object is illegal when we modify the Order item, we need to manually modify the Order state */ BigDecimal totalAfterItemAdd = order.getTotal(); BigDecimal expectedTotal = new BigDecimal("150"); boolean isExpectedTotal = expectedTotal.equals(totalAfterItemAdd); /* * Of course, the total number of orders cannot be the expected total, because the anaemic model cannot handle their state changes. */ assertFalse(isExpectedTotal); /* * To solve this problem, we must call OrderService to recalculate the total and make the Order object legal again. */ orderService.calculateTotal(order); /* * Now the Order object is legal again */ BigDecimal totalAfterRecalculation = order.gettotal (); assertEquals(expectedTotal, totalAfterRecalculation); }}Copy the code
The interpretation of data in the anemia model is done by stateless services. Although the service is stateless, it does not know when the logic should be executed and when it should not, and the stateless service cannot cache its calculated values. The congestion domain object automatically handles its state changes and knows when the property must be recalculated.
Anemia domain model is not object-oriented programming, anemia model is procedural programming
In the early programming days, the Order example was implemented as follows:
struct order_item {
int amount;
double price;
char *name;
};
struct order {
int total;
struct order_item items[10];
};
int main(){
struct order order1;
struct order_item item;
item.name = "Domain-Driven";
item.price = 30.0;
item.amount = 5;
order.items[0] = item;
calculateTotal(order1);
}
void calculateTotal(order o){
int i, count;
count = 0;
for(i=0; i < 10; i++) {
order_item item = o.items[i];
o.total = o.total + item.price * item.amount;
}
}Copy the code
Using anemic models means using procedural programming. Procedural programming is easy, but understanding application state handling is difficult. In addition, the anaemic model moves the logic of state processing and data interpretation to the client side, which often results in code duplication or very fine-grained services. The result is a large number of services and service methods that are interconnected in a wide and complex network of objects, which is why it is so hard to find an object in a certain state. To find out why an object is in a certain state, you must find the method call hierarchy that the object passes through.
Congestion model
In contrast to the anemia domain model, the congestion model follows the object-oriented principle. Thus, the congestion model is really object-oriented programming. The purpose of congestion modeling or object-oriented programming is to bring data and logic together.
Object orientation means that an object manages its state and ensures that it is legal at all times. The Order class for the anemia model can be easily converted to an object-oriented version.
public class Order { private BigDecimal total; private List<OrderItem> items = new ArrayList<OrderItem>(); /** * Return order total */ public BigDecimal getTotal() {if (total == null) {/** Must count and save the result */ BigDecimal orderItemTotal = BigDecimal.ZERO; List<OrderItem> items = getItems(); for (OrderItem orderItem : items) { BigDecimal itemTotal = orderItem.getTotal(); // get the total price of an OrderItem /* * add the total price of each OrderItem to our total. */ orderItemTotal = orderItemTotal.add(itemTotal); } this.total = orderItemTotal; } return total; /** * add OrderItem to Order */ public void addItem(OrderItem OrderItem) {if (OrderItem == null) {throw new IllegalArgumentException("orderItem must not be null"); } if (this.items. Add (orderItem)) {/* * The list of order items has changed, so we reset the total field to null and * let getTotal recalculate total. */ this.total = null; }} /** ** returns all orderItems in Order. The client cannot modify the returned List<OrderItem> */ public List<OrderItem> getItems() { To prevent customers from manipulating our internal state. */ return Collections.unmodifiableList(items); } } import java.math.BigDecimal; public class OrderItem { private BigDecimal price; private int quantity; private String name = "no name"; public OrderItem(BigDecimal price, int quantity, String name) { if (price == null) { throw new IllegalArgumentException("price must not be null"); } if (name == null) { throw new IllegalArgumentException("name must not be null"); } if (price.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException( "price must be a positive big decimal"); } if (quantity < 1) { throw new IllegalArgumentException("quantity must be 1 or greater"); } this.price = price; this.quantity = quantity; this.name = name; } public BigDecimal getPrice() { return price; } public int getQuantity() { return quantity; } public String getName() { return name; } /** * total = getPrice() * getAmount() */ public BigDecimal getTotal() {int quantity = getQuantity(); BigDecimal price = getPrice(); BigDecimal total = price.multiply(new BigDecimal(quantity)); return total; }}Copy the code
The advantage of object-oriented programming is that objects are guaranteed to be legal at all times, and service classes are no longer required. The test cases will show differences from the anemic model, which does not guarantee that they are in a legitimate state at all times.
Public class OrderTest {/** * This test indicates that the congestion model guarantees that it is in a valid state at all times *. */ @Test public void richDomainModelMustEnsureToBeConsistentAtAnyTime() { Order order = new Order(); BigDecimal total = order.getTotal(); /* * New orders have no items, so the total amount must be zero. */ assertEquals(BigDecimal.ZERO, total); OrderItem aGoodBook = new OrderItem(new BigDecimal("30"), 5, "Domain-Driven"); List<OrderItem> items = order.getItems(); try { items.add(aGoodBook); } the catch (UnsupportedOperationException e) {/ * * we cannot destroy the encapsulation, because order object to the client not public, its internal state. * The object cares about its own state and ensures that it is legal at all times. */} /* * We must use the object exposed addition method */ order.addItem(aGoodBook); /* * After adding OrderItem. The object is still legal. */ BigDecimal totalAfterItemAdd = order.getTotal(); BigDecimal expectedTotal = new BigDecimal("150"); assertEquals(expectedTotal, totalAfterItemAdd); }}Copy the code
Which model to use
Applications should use object-oriented approaches as much as possible. The advantage of object-oriented programming is that the object is guaranteed to be legal at all times. If you want to ensure that the entire application is always in a legitimate (and expected) state, you need to use the congestion model.
The common requirement in most jobs is CRUD, poJOs need to be modified frequently, and data flows through the modules through JSON. In this context, application extensions tend to be horizontal, with field additions or joins of tables rather than extensions of abstract relationships, and the anaemic model is recommended.
Congestion models may be better suited to complex and stable business areas.
Each methodology has its own limitations and scope of application. It may be easy to start with an anaemic model, but later refactoring an application into a congested domain model architecture can be cumbersome and prone to failure.
conclusion
The basis of DDD is the congestion model. In this article, we will first understand how to use the congestion model, and then we will gradually analyze the concepts of DDD and give code practices.
The resources
- Domain-driven Design: Addressing complexity at the Heart of Software. By Eric Evans
- Anemic vs. Rich Domain Models