Domain-driven design focuses on fields, especially in the face of complicated domain logic, which can always help us analyze fields well. Domain-driven design is based on Domain Modeling. Eric believes that it is necessary to cooperate well with experts in the field to find common languages and keywords in the field from conversations. Domain Modeling is an iterative process. You can refine the model based on in-depth domain knowledge. However, domain-driven design does not exclude other analysis technologies, such as analysis models, or guides us to find the domain model of the problem domain through test-driven development.
Domain Modeling is not unique to domain-driven design. In RUP, Domain Modeling is a very important step. It is a case-driven development method. The obtained use cases help analysis and designers find objects and relationships between objects. Based on my personal experience, I like to use two completely different methods to obtain models. One is case-driven, and the other is test-driven. In the initial domain model, I will try to use the concept of domain-driven design to classify objects, find objects, value objects, aggregates, and service objects, and analyze the object lifecycle, create a resource library and a factory object for different types of objects.
This article will take a library management system which is familiar to readers as the field for analysis, and try to explain how to apply the field-driven design in project development. I will select a case-driven approach to obtain my initial domain model. For the sake of simplicity, I will first give examples and Use Cases of the analysis field.
Borrowing: the reader carries the books to borrow to the borrowing place. The Library staff first scanned the reader's debit card, obtained the reader's information, and then scanned the book's barcode. If you borrow more books, scan more books. During scanning, You need to determine the type of the current Reader and obtain the number of books that the reader can borrow. If the number of books borrowed exceeds the limit, a message is displayed. If the scan fails, allow the staff to manually enter the number. The borrow period is one month.
Returning books: The Reader carries the books to the returning place. Library staff scan the bar code of books and return books. If the borrowed books expire, the system prompts and calculates the amount of fines to be paid. If the scan fails, allow the staff to manually enter the number.
I used the abstract method to describe the use case. I like this simple method, which is actually equivalent to a user story in XP. This method can be used to compile use cases when the requirements are not complex or when the requirements for documents are not strict.
The following is an example diagram showing the two use cases:
You can first use the noun/Verb method to find the domain objects in the model. Although this method is extremely simple and low-level, it is very effective at the beginning of establishing a domain model. Through case analysis, you can obtain the following objects: Reader, Administrator, Book, Library Card, and readers. There may also be areas that we have not discovered, which can be further achieved through in-depth areas or conversations with customers. We can try to obtain the simplest domain model first, as shown below.
We found that the Administrator object is an isolated object, which has no relationship with objects in other fields. At least in the case of borrowing and Returning books, we do not need to manage this object. We can consider deleting it. The sequence object in the model is very special. On the surface, it performs operations on Book and LibraryCard. However, for the sequence object, it does not care about what object to operate, but only needs to scan the bar code, returns a string. This is an embodiment of behavior. In the entire system, the consumer object can have only one, with no attributes and status. It only provides the scanning function or service, so you can consider defining it as a service object.
The relationship between Reader and Book is very direct, but after LibraryCard is introduced, this relationship is a bit embarrassing. After carefully reading the use cases, we find that the reader's information is obtained through the debit card. You can use the debit card to obtain the books currently borrowed by the readers, whether by borrowing or returning books. At this time, there is no relation between the reader and the book, and it has been passed on. Since the debit card has managed the relationship between books, do we need to keep the Reader object? When reading the use case, we know that the reader's information will be obtained when scanning the debit card. Although we can retain this information in the debit card, it is still necessary to encapsulate it as an object based on the single responsibility principle (SRP.
Currently, the debit card only maintains the books currently borrowed by readers. Do you still need to maintain the history of borrowing and returning? This function is not available in the case description. We are confused because retaining history is essential to most systems. At this time, the customer's answer is particularly important. "Oh, yes, we need to check the history !" This is a positive response from the customer. Obviously, viewing history is another use case, and it may even belong to another Context, for example, the Context about "query. However, the source of this information comes from borrowing and returning use cases. We should identify this information. If other use cases need to be used, I think this object needs to be shared. The detailed domain model is as follows:
Through the analysis of scanning behavior, I think that the scanning behavior provided by tracing is not related to the domain, but an infrastructure, so I define it as a service at the infrastructure layer. The FineCalculator object is added to the model to calculate the penalty amount for expired readers. Obviously, it is a service object. Note that BorrowingHistory has a one-to-one relationship with Book, because we need to create a lending history for each Book.
Now, we need to identify the entities and value objects in the domain model and the possible aggregation. We need a unique identifier to differentiate readers, and this identifier is continuous. Therefore, Reader is an object. Similarly, the Book object is an object because we need a unique identifier to track books. Note that the Book entity in this model represents a specific Book rather than the same Book. Because the library may purchase multiple books for the same type, and the readers borrow real books, not just the attributes of books. In this case, the ID of the Book is particularly important, and it cannot be identified by the ISBN of the Book.
On the surface, BorrowingHistory is also an object, and each record of BorrowingHistory is unique. Even if two historical records exist, it has the same reader ID and book ID, we still regard it as a different record because they have different borrowing times. However, for system callers, they generally do not focus on all borrowing records, but query the borrowing records of a Reader. Therefore, we can aggregate the records together with Reader. However, with the in-depth analysis of the demand, we found that there is a problem with the definition of such aggregation, because we may also need to query the borrowing records of a book (for example, to know which book is the most popular, tracking the borrowing situation of each book ). Since the Reader and Book should be classified into different aggregates, BorrowingHistory has the problem of undefined aggregation. In this case, we should separate it as a separate aggregation root.
What is confusing is the LibraryCard object. On the one hand, its ID comes from Reader and has a one-to-one relationship. Therefore, it can be used as a part of Reader aggregation. According to the model diagram, it actually records the relationship between the reader and the book. After careful analysis, the relationship between readers and books maintained by LibraryCard is actually a reflection of BorrowingHistory. The difference is that it only records the current borrowing information, it also contains information about past borrowing books. BorrowingHistory can be used for information persistence, while LibraryCard can maintain a set of currently borrowed information in the memory. Therefore, you can define LibraryCard in Reader aggregation. In this way, the association between objects can be reduced and the consistency between objects can be ensured.
We also need to analyze the ID of the Reader object and the Book object in depth, because the ID of both is obtained through the infrastructure's metadata service. Pipeline does not have the ability to know the difference between the two. When borrowing a book, you must first scan the Borrowing Card to obtain the reader's information and then scan the book according to the required procedures. In addition, when an error occurs during a scan, the system must support manual input by the operator. Therefore, ID verification is also required for manual input. We need an object that specifically verifies the ID.
We also need to consider many business rules, such as whether to allow readers to borrow books, whether the rules are out of date, and the rules for calculating the amount of fines. If these rules are extremely simple and do not change, they can be placed in domain objects. However, once the rules become complex, they will seriously interfere with the responsibilities of the objects in the relevant fields. Based on the separation of duties principle, we can provide specialized rule objects, that is, the application of specification patterns in the design of the domain-driven. If possible, we can even introduce policy patterns to abstract these rules. The domain model obtained after analysis is as follows:
The Reader object and the LibraryCard object are in the same aggregation, and the Reader is the aggregation root. BorrowingSpecification and ReturningSepecification are both value objects and placed in Reader aggregation. FineCalculator is a service object. It calls the FineRule value object to obtain the penalty rule. After calculation, the value of the Money value object is returned. Due to aggregation, the relationship between FineCalculator and LibraryCard has been changed to a fine for calculating Reader.
BorrowingHistory and Book are both object objects, while IdentityValidator is the service object, which verifies the scan code.
Next, you need to select a resource library (Repository) for the domain object ). In the domain model, only three entities, Reader, BorrowingHistory, and Book, are aggregate root objects. Therefore, you only need to create a resource library object for these three objects.
Because the requirements are relatively simple and the domain model has been well established, we can start coding and verify these models. This article does not consider limiting the context. I hope that future articles will express this in real cases. On the whole, based on this case, we have been able to get a preliminary understanding of the basic steps of field-driven design.