These reviews

The previous article introduced the second part of the book, “Building Blocks for Model-driven Design,” which distilled some of the core best practices in object-oriented domain modeling into a set of basic building blocks. We learned about the REPOSITORY that provides lookup and object persistence.

This time we continue to study the second part of the book. We will put the modeling techniques we learned earlier to practical use in a simulated scenario by addressing hypothetical requirements and implementation problems.

Actual combat scene

Suppose we are developing new software for a shipping company. The initial requirements included three basic features:

  1. Follow up the main processing of customer’s goods;
  2. Advance booking of goods;
  3. Invoices are automatically sent to customers when goods reach a certain state in their processing.

The basic model

Suppose we get the following basic conceptual model through a period of analysis.

A Cargo involves multiple customers, each with a different role. Cargo destination has been specified. Transport objectives are accomplished by a series of carrier movements that meet the Specification.

A HandlingEvent is a different action taken on Cargo, such as loading it onto a ship or clearing customs. This class can be refined into a hierarchy of different kinds of events, such as loading, unloading, or picking up by the consignee.

The DeliverySpecification defines the destination of the delivery, including at least the destination and the date of arrival.

The part that Customer undertakes in transportation is distinguished according to roles, such as shipper, receiver, payer, etc. Since a Cargo can only be filled by one Customer for a given role, their association is a qualified many-to-one relationship, not many-to-many. A role can be implemented simply as a string, or as a class when additional behavior is required.

A CarrierMovement represents a journey from one Location to another performed by a Carrier, such as a truck or a ship. Once Cargo is loaded on a Carrier, it can be moved between locations through one or more Carrier movements.

DeliveryHistory reflects what actually happened to the Cargo, as opposed to the DeliverySpecification, which describes the target. The DeliveryHistory object can calculate the current position of the goods by analyzing the destination of the last loading and unloading and the corresponding CarrierMovement. Successful shipping will result in a DeliveryHistory that meets the goals of the DeliverySpecification.

Isolation domain layer

We used LAYERED ARCHITECTURE mentioned above to separate the domain layer. We can identify three user-level application capabilities that can be assigned to three application layer classes.

  1. TrackingQuery, which accesses past and present processing of a Cargo.
  2. BookingApplication (BookingApplication), which allows you to register a new Cargo and prepare the system to handle it.
  3. IncidentLoggingApplication (application event log), it records every time of Cargo handling, for TrackingQuery lookup.

These application-layer classes are the coordinators; they simply ask questions, not answers, which are the domain layer’s job.

Distinguish between ENTITY and VALUE OBJECT

Cargo

Two identical containers must be separated, so the Cargo object is an ENTITY.

Customer

The Customer object represents a person or a company and is an entity in the general sense. The Customer object obviously has an identity that is important to the user, so it is an ENTITY in the model.

HandlingEvent and CarrierMovement

We care about these individual events because they keep track of what’s going on. They reflect real-world events that are generally not interchangeable, making them ENTITY.

DeliveryHistory

DeliveryHistory is not interchangeable, so it is ENTITY. DeliveryHistory has a one-to-one relationship with Cargo, so it doesn’t actually have its own logo. Its logo comes from Cargo, which owns it.

Location

Two locations with the same name are not the same location, and it is not appropriate to use latitude and longitude as a unique identifier. Latitude and longitude are not often used and can be uniquely distinguished by automatically generated internal arbitrary identifier, so it is an ENTITY.

DeliverySpecification

It represents Cargo’s goals, but this abstraction does not depend on Cargo. It actually represents the final state of some DeliveryHistory. Shipping the Cargo is essentially making the Cargo’s DeliveryHistory finally meet the Cargo’s DeliverySpecification. If you have two Cargo going to the same location, they can use the same DeliverySpecification. Therefore, the DeliverySpecification is the VALUE OBJECT.

Roles and other attributes

A Role represents some information about the association it restricts, but it has no history or continuity. Therefore, it is a VALUE OBJECT like other properties (such as time and name).

Design the associations of objects

We mentioned earlier that object associations should be directional, and that bidirectional associations should be avoided as much as possible. In this spirit, we can draw the following correlations:

If our application were to track a series of cargo ships, traversal from CarrierMovement to HandlingEvent would be important. But our business only needs to track Cargo, so just walking from HandlingEvent to CarrierMovement can meet our business needs.

There is a circular reference in the model: Cargo knows its DeliveryHistory, which holds a series of handlingevents, which in turn point to Cargo. Many domains logically have circular references, and circular references are sometimes necessary in design, but they are complex to maintain. When choosing an implementation, you should avoid keeping information that must be synchronized in two different places.

Define the boundary of AGGREGATE

Cargo AGGREGATE can include everything that exists because of Cargo, including DeliveryHistory, DeliverySpecification and HandlingEvent. This works well for DeliveryHistory, as no one would look up DeliveryHistory without knowing about Cargo. The DeliverySpecification is a VALUE OBJECT, so it is not complicated to include it in Cargo AGGREGATE.

HandlingEvent is another matter. If the business needs to find all the operations that take place when loading and preparing for a CarrierMovement, it also makes sense to think of it separately from Cargo itself, so HandlingEvent should be the root of its own AGGREGATE.

Customer, Location, and CarrierMovement each have their own identity and are shared by many Cargo, so they must be roots in their AGGREGATE.

Select the REPOSITORY

In the above design, we delineated five aggregate roots, so you can only choose from these five entities when designing a REPOSITORY.

To determine which of these five entities really needs REPOSITORY, you must look back at the requirements of your application. To book through BookingApplication, users need to select customers who assume different roles (shipper, consignee, and so on). Therefore, a CustomerRepository is required.

You also need a Location to specify the destination of the goods, so you also need to create a LocationRepository. The user needs to find the loading by ActivityLoggingApplication CarrierMovement, therefore need a CarrierMovementRepository. The user must also tell the system which Cargo has finished loading, so a CargoRepository is also required,

In the first iteration we decided to implement the association of HandlingEvent with DeliveryHistory as a collection, and the application did not need to find out what goods were loaded in a CarrierMovement. For these two reasons, we did not create HandlingEventRepositoty. Of course, you can add a REPOSITORY if the situation changes.

Create an object

Even if we design a FACTORY for some ENTITY, we still need the basic constructor. As ENTITY, we want its identity to remain the same. You can add the following methods to FACTORY:

publicCargo newTrackingId (Cargo prototype, String newTrackingId)Copy the code

It is also possible to encapsulate the process of generating a new ID in FACTORY, so that only one parameter is required:

publicCargo newCargo (Cargo prototype)Copy the code

The two-way relationship between Cargo and DeliveryHistory means that they have to point at each other to be complete, so they have to be created together. Cargo is the aggregation root, so we can create DeliveryHistory using Cargo’s constructor or FACTORY.

public Cargo(String id) {
    trackingId = id;
    deliveryHistory = new DeliveryHistory(this);
    customerRoles = new HashMap();
}
Copy the code

Handlingevents are uniquely identified by a combination of Cargo’s ID, completion time, and event type. The remaining property of the HandlingEvent is the association with the CarrierMovement, which is not required. For an ENTITY, these non-identifying attributes can usually be added later.

public HandlingEvent(Cargo cargo, String eventType, Date timeStamp) {
    handled = cargo;
    type = eventType;
    completionTime = timeStamp;
}
Copy the code

It is convenient to add a factory method for each EventType, along with the necessary parameters, to make the code more expressive. For example, a loadingEvent does involve a CarrierMovement.

public static HandlingEvent newLoading(Cargo cargo, CarrierMovement loadedOnto, Date timeStamp) {
    HandlingEvent res = new HandlingEvent(cargo, LOADING_EVENT, timestamp);
    res.setCarrierMovement(loadedOnto);
    return res;
}
Copy the code

The circular references encountered earlier: Cargo→DeliveryHistory→HistoryEvent→Cargo make instance creation complicated. DeliveryHistory holds the HandlingEvent collection associated with its Cargo, and new HandlingEvent objects must be added to the collection as part of the transaction. To avoid inconsistencies between objects, we might need a reverse pointer (HandlingEvent→DeliveryHistory), but doing so like the following is awkward.

refactoring

Since adding a HandlingEvent requires updating the DeliveryHistory, updating the DeliveryHistory involves Cargo AGGREGATE in the transaction. Therefore, if other users are modifying Cargo at the same time, the HandlingEvent transaction will fail or be delayed. Entering a HandlingEvent is a simple operation that needs to be done quickly, so being able to enter a HandlingEvent without contention is an important application requirement. This led us to consider a different design.

We can implement the HandlingEvent collection in DeliveryHistory as a query, which makes it easy to insert handlingEvents without competing with Cargo AGGREGATE. Therefore, we need to add a REPOSITORY for HandlingEvent to implement the query.

By doing so, DeliveryHistory no longer has a persistent state, so it can be created whenever a question needs to be answered with DeliveryHistory. The CargoFactory will be simplified, eliminating the need to create an empty DeliveryHistory for new Cargo instances.

The impact of these design changes has been greatly reduced by modeling VALUE, ENTITY, and their aggregates. For example, we only need to add a HandlingEventRepository this time, but we don’t need to redesign the HandlingEvent itself.

Module is divided into

The previous model is very simple. Modules are not a problem. Let’s look at a more complete model and see how it divides modules.

The following figure is divided by PATTERN, which obviously does not transfer domain knowledge.

Our business can be summarized as follows: The company charges customers bills for Shipping goods. The sales and marketing personnel of the company negotiate and sign an agreement with the Customer. The operation personnel is responsible for Shipping goods to the designated destination, and the back office personnel is responsible for Billing. Issue invoice according to Customer agreement. According to this domain concept, it can be divided into the following modules.

New requirement: Quota checking

As it stands today, sales departments use other software to manage customer relationships, sales plans, and so on. One of the yieldmanagement features is yieldManagement, which allows a company to create quotas for different types of goods based on the type of goods, origin and destination, or any other factor that can be entered as a classification name. These quotas constitute volume targets for all types of cargo, so that less profitable cargo does not fill the cargo hold and thus cannot move more profitable cargo, while avoiding underbooking (underutilization of capacity) or overbooking (resulting in frequent cargo collisions that ultimately damage customer relationships).

The new requirement is to integrate this quota check into the reservation system to determine if a customer’s reservation can be accepted. BookingApplication uses information from both the SalesManagementSystem and our own domain, REPOSITORY.

If BookingApplication interacts directly with the sales management system, we have to adapt to the design of another system, which makes it difficult to maintain a clear model. Instead, we can create a class that acts as a translator between our model and the language of our sales management system, and it only translates the features we need.

We define a SERVICE for each quota function that needs to be obtained from another system. We implement these services with a class called AllocationChecker.

The problem is that quotas are managed according to Cargo, which is not defined in our system. A simple approach is to represent the type with a list of strings consistent with the sales management system. But by doing so we did not model Cargo, and the knowledge of the Cargo category was missing from the domain model. To enrich the model, we can borrow the ENTERPRISE SEGMENT PATTERN (ENTERPRISE sector unit) from Fowler’s book Analysis Patterns. ENTERPRISESEGMENT is a set of dimensions that define a way to partition a business. This adds a class called EnterpriseSegment to our domain model and design, which is a VALUE OBJECT.

Next, about this business rule: “If the quota of Enterprise Sement is greater than the sum of the ordered quantity and the new Cargo quantity, the Cargo is accepted.” We need to think about where to do that. And we need to figure out how the EnterpriseSegment parameter in the Allocation method called by BookingApplication comes from.

These things should be implemented in the domain layer and handed over to the AllocationChecker. So you can optimize the interface to separate the two functions and make the interaction clearer and easier to understand.

In our design, CargoRepository only deals with enterprise repository, and changes in the sales system only affect the AllocationChecker, which can be seen as a FACADE.

One last question: why not give Cargo the responsibility of acquiring enterprise sement? If all of the EnterpriseSegment data is retrieved from Cargo, then at first glance it might seem like a good choice to make it a derived property of Cargo.

Unfortunately, it’s not that simple. To partition in a dimension that favors business policy, we need to define an arbitrary EnterpriseSegment. Different divisions may be required for the same ENTITY for different purposes. For booking quota purposes, we need to divide according to specific Cargo; However, if it is for tax accounting purposes, a completely different kind of EnterpriseSegment may be adopted.

If Cargo were to do it, Cargo would have to understand the logic of the AllocationChecker, which is completely outside of its conceptual responsibilities. And the method used to figure out what a particular type of EnterpriseSegment needs to do adds to Cargo’s burden. Therefore, the right thing to do is to give the responsibility for obtaining this value to objects that know the partitioning rules, rather than to impose this responsibility on objects that contain specific data on which the rules apply.