Domain-Driven Design
Page 33
It is very hard to maintain the level of communication needed to develop a unified system of any size. We need ways of increasing communication and reducing complexity. We also need safety nets that prevent overcautious behavior, such as developers duplicating functionality because they are afraid they will break existing code.
It is in this environment that Extreme Programming (XP) really comes into its own. Many XP practices are aimed at this specific problem of maintaining a coherent design that is being constantly changed by many people. XP in its purest form is a nice fit for maintaining model integrity within a single BOUNDED CONTEXT. However, whether or not XP is being used, it is essential to have some process of CONTINUOUS INTEGRATION.
CONTINUOUS INTEGRATION means that all work within the context is being merged and made consistent frequently enough that when splinters happen they are caught and corrected quickly. CONTINUOUS INTEGRATION, like everything else in domain-driven design, operates at two levels: (1) the integration of model concepts and (2) the integration of the implementation.
Concepts are integrated by constant communication among team members. The team must cultivate a shared understanding of the ever-changing model. Many practices help, but the most fundamental is constantly hammering out the UBIQUITOUS LANGUAGE. Meanwhile, the implementation artifacts are being integrated by a systematic merge/build/test process that exposes model splinters early. Many processes for integration are used, but most of the effective ones share these characteristics:
• A step-by-step, reproducible merge/build technique;
• Automated test suites; and
• Rules that set some reasonably small upper limit on the lifetime of unintegrated changes.
The other side of the coin in effective processes, although it is seldom formally included, is conceptual integration.
• Constant exercise of the UBIQUITOUS LANGUAGE in discussions of the model and application
Most Agile projects have at least daily merges of each developer’s code changes. The frequency can be adjusted to the pace of change, as long as any unintegrated change would be merged before a significant amount of incompatible work could be done by other team members.
In a MODEL-DRIVEN DESIGN, the integration of concepts smooths the way for the integration of the implementation, while the integration of the implementation proves the validity and consistency of the model and exposes splinters.
Therefore:
Institute a process of merging all code and other implementation artifacts frequently, with automated tests to flag fragmentation quickly. Relentlessly exercise the UBIQUITOUS LANGUAGE to hammer out a shared view of the model as the concepts evolve in different people’s heads.
Finally, do not make the job any bigger than it has to be. CONTINUOUS INTEGRATION is essential only within a BOUNDED CONTEXT. Design issues involving neighboring CONTEXTS, including translation, don’t have to be dealt with at the same pace.
CONTINUOUS INTEGRATION would be applied within any individual BOUNDED CONTEXT that is larger than a two-person task. It maintains the integrity of that single model. When multiple BOUNDED CONTEXTS coexist, you have to decide on their relationships and design any necessary interfaces. . . .
Context Map
An individual BOUNDED CONTEXT still does not provide a global view. The context of other models may still be vague and in flux.
People on other teams won’t be very aware of the CONTEXT bounds and will unknowingly make changes that blur the edges or complicate the interconnections. When connections must be made between different contexts, they tend to bleed into each other.
Code reuse between BOUNDED CONTEXTS is a hazard to be avoided. Integration of functionality and data must go through a translation. You can reduce confusion by defining the relationship between the different contexts and creating a global view of all the model contexts on the project.
A CONTEXT MAP is in the overlap between project management and software design. The natural course of events is for the boundaries to follow the contours of team organization. People who work closely will naturally share a model context. People on different teams, or those that don’t talk, even if they are on the same team, will split off into different contexts. Physical office space can have an impact too, as team members on opposite ends of a building—not to mention different cities—will probably diverge without extra integration effort. Most project managers intuitively recognize these factors and broadly organize teams around subsystems. But the interrelationship between team organization and software model and design is still not prominent enough. Both managers and team members need a clear view into the ongoing conceptual subdivision of the software model and design.
Therefore:
Identify each model in play on the project and define its BOUNDED CONTEXT. This includes the implicit models of non-object-oriented subsystems. Name each BOUNDED CONTEXT, and make the names part of the UBIQUITOUS LANGUAGE.
Describe the points of contact between the models, outlining explicit translation for any communication and highlighting any sharing.
Map the existing terrain. Take up transformations later.
Within each BOUNDED CONTEXT, you will have a coherent dialect of the UBIQUITOUS LANGUAGE. The names of the BOUNDED CONTEXTS will themselves enter that LANGUAGE so that you can speak unambiguously about the model of any part of the design by making your CONTEXT clear.
The MAP does not have to be documented in any particular form. I find diagrams like the ones in this chapter to be helpful in visualizing and communicating the map. Others may prefer a more textual description or a different graphical representation. In some situations, discussion among teammates may be sufficient. The level of detail can vary according to need. Whatever form the MAP takes, it must be shared and understood by everyone on the project. It must provide a clear name for each BOUNDED CONTEXT, and it must make the points of contact and their natures clear.
The relationships between BOUNDED CONTEXTS take many forms depending on both design issues and project organizational issues. Later, this chapter will lay out various patterns of relationships between CONTEXTS that are effective in different situations, and that can provide terms to describe the relationships you find in your own MAP. Keeping in mind that the CONTEXT MAP always represents the situation as it stands, the relationships you find may not fit these patterns initially. If they fall close, you may wish to use the pattern name, but don’t force it. Just describe the relationships you find. Later you can begin to migrate toward more standardized relationships.
So, what do you do if you’ve discovered a splinter—a model that is completely entangled but contains inconsistencies? Put a dragon on the map and finish describing everything. Then, with an accurate global view, address the points of confusion. A minor splinter can be repaired, and processes can be put in place to shore it up. If a relationship is vague, you can choose the nearest pattern and move toward it. Your first order of business is to arrive at a clear CONTEXT MAP, and this may mean fixing real problems you have found. But don’t let this necessary repair lead to wholesale reorganization. Until you have an unambiguous CONTEXT MAP that places all your work into some BOUNDED CONTEXT, with explicit relationships between all connected models, change only the outright contradictions.
Once you have a coherent CONTEXT MAP, you’ll see things you want to change. You can make considered changes to the organization of teams or to the design. Remember, don’t change the map until the change in reality is done.
Example: Two CONTEXTS in a Shipping Application
We return again to the shipping system. One of the application’s major features was to be the automatic routing of cargos at booking time. The model was something like this:
Figure 14.2
The Routing Service is a SERVICE that encapsulates a mechanism behind an INTENTION-REVEALING INTERFACE made up of SIDEEFFECT-FREE FUNCTIONS. The results of those functions are characterized with ASSERTIONS.
1. The interface declares that when a Route Spe
cification is passed in, an Itinerary will be returned.
2. The ASSERTION states that the returned Itinerary will satisfy the Route Specification that was passed in.
Nothing is stated about how this very difficult task is performed. Now let’s go behind the curtain to see the mechanism.
Initially on the project on which this example is based, I was too dogmatic about the internals of the Routing Service. I wanted the actual routing operation to be done with an extended domain model that would represent vessel voyages and directly relate them to the Legs in the Itinerary. But the team working on the routing problem pointed out that, to make it perform well and to draw on well-established algorithms, the solution needed to be implemented as an optimized network, with each leg of a voyage represented as an element in a matrix. They insisted on a distinct model of shipping operations for this purpose.
They were clearly right about the computational demands of the routing process as then designed, and so, lacking any better idea, I yielded. In effect, we created two separate BOUNDED CONTEXTS, each of which had its own conceptual organization of shipping operations. (See Figure 14.3.)
Figure 14.3. Two BOUNDED CONTEXTS formed to allow efficient routing algorithms to be applied
Our requirement was to take a Routing Service request, translate it into terms the Network Traversal Service could understand, then take the result and translate it into the form a Routing Service is expected to give.
This means it was not necessary to map everything in these two models, but only to be able to make two specific translations:
Route Specification → List of location codes
List of Node IDs → Itinerary
To do this, we have to look at the meaning of an element of one model and figure out how to express it in terms of the other.
Starting with the first translation (Route Specification → List of location codes), we have to think about the meaning of the sequence of locations in the list. The first in the list will be the beginning of the path, which will then be forced to pass through each location in turn until it reaches the last location in the list. So the origin and destination are the first and last in the list, with the customs clearance location (if there is one) in the middle.
Figure 14.4. Translation of a query to the Network Traversal Service
(Mercifully, the two teams used the same location codes, so we don’t have to deal with that level of translation.)
Notice that the reverse translation would be ambiguous, because the network traversal input allows any number of intermediate points, not just one specifically designated as customs clearance point. Fortunately, this is no problem because we don’t need to translate in that direction, but it gives a glimpse of why some translations are impossible.
Now, let’s translate the result (List of Node IDs → Itinerary). We’ll assume that we can use a REPOSITORY to look up the Node and Shipping Operation objects based on the Node IDs we receive. So, how do those Nodes map to Legs? Based on the operationType-Code, we can break the list of Nodes into departure/arrival pairs. Each pair then relates to one Leg.
Figure 14.5. Translation of a route found by the Network Traversal Service
The attributes for each Node pair would be mapped as follows:
departureNode.shippingOperation.vesselVoyageId → leg.vesselVoyageId
departureNode.shippingOperation.date → leg.loadDate
departureNode.locationCode → leg.loadLocationCode
arrivalNode.shippingOperation.date → leg.unloadDate
arrivalNode.locationCode → leg.unloadLocationCode
This is the conceptual translation map between these two models. Now we have to implement something that can do the translation for us. In a simple case like this, I typically create an object for the purpose, and then find or create another object to provide the service to the rest of our subsystem.
Figure 14.6. A two-way translator
This is the one object that both teams have to work together to maintain. The design should make it very easy to unit-test, and it would be a particularly good idea for the teams to collaborate on a test suite for it. Other than that, they can go their separate ways.
Figure 14.7
The Routing Service implementation now becomes a matter of delegating to the Translator and the Network Traversal Service. Its single operation would look something like this:
public Itinerary route(RouteSpecification spec) {
Booking_TransportNetwork_Translator translator =
new Booking_TransportNetwork_Translator();
List constraintLocations =
translator.convertConstraints(spec);
// Get access to the NetworkTraversalService
List pathNodes =
traversalService.findPath(constraintLocations);
Itinerary result = translator.convert(pathNodes);
return result;
}
Not bad. The BOUNDED CONTEXTS served to keep each of the models relatively clean, let the teams work largely independently, and if initial assumptions had been correct, would probably have served well. (We’ll return to that later in this chapter.)
The interface between the two contexts is fairly small. The interface of the Routing Service insulates the rest of the Booking CONTEXT’s design from events in the route-finding world. The interface is easy to test because it is made up of SIDE-EFFECT-FREE FUNCTIONS. One of the secrets to comfortable coexistence with other CONTEXTS is to have effective sets of tests for the interfaces. “Trust, but verify,” said President Reagan when negotiating arms reductions.1
It should be easy to devise a set of automated tests that would feed Route Specifications into the Routing Service and check the returned Itinerary.
Model contexts always exist, but without conscious attention they may overlap and fluctuate. By explicitly defining BOUNDED CONTEXTS and a CONTEXT MAP, your team can begin to direct the process of unifying models and connecting distinct ones.
Testing at the CONTEXT Boundaries
Contact points with other BOUNDED CONTEXTS are particularly important to test. Tests help compensate for the subtleties of translation and the lower level of communication that typically exist at boundaries. They can act as a valuable early warning system, especially reassuring in cases where you depend on the details of a model you don’t control.
Organizing and Documenting CONTEXT MAPS
There are only two important points here:
1. The BOUNDED CONTEXTS should have names so that you can talk about them. Those names should enter the UBIQUITOUS LANGUAGE of the team.
2. Everyone has to know where the boundaries lie, and be able to recognize the CONTEXT of any piece of code or any situation.
The second requirement could be met in many ways depending on the culture of the team. Once the BOUNDED CONTEXTS have been defined, it comes naturally to segregate the code of different CONTEXTS into different MODULES, which leaves the question of how to keep track of which MODULE belongs in which CONTEXT. A naming convention might be used to indicate this, or any other mechanism that is easy and avoids confusion.
Equally important is communicating the conceptual boundaries in such a way that everyone on the team understands them the same way. For this communication purpose, I like informal diagrams like the ones in the example. More rigorous diagrams or textual lists could be made, showing all packages in each CONTEXT, along with the points of contact and the mechanisms responsible for connecting and translating. Some teams will be more comfortable with this approach, while others will get by fine based on spoken agreement and lots of discussion.
In any case, working the CONTEXT MAP into discussions is essential if the names are to enter the UBIQUITOUS LANGUAGE. Don’t say, “George’s team’s stuff is changing, so we’re going to have to change our stuff that talks to it.” Say instead, “The Transport Network model is changing, so we’re going to have to change the translator for the Booking context.”
Relationships Between BOUNDED CO
NTEXTS
The following patterns cover a range of strategies for relating two models that can be composed to encompass an entire enterprise. These patterns serve the dual purpose of providing targets for successfully organizing development work, and supplying vocabulary for describing the existing organization.
An existing relationship may, by chance or by design, fall near one of these patterns, in which case you can describe it using that term, variations duly noted. Then, with each small design change, the relationship can be drawn closer to the chosen pattern.
On the other hand, you may find that an existing relationship is muddled or overcomplicated. Some reorganization might be necessary just to make an unambiguous CONTEXT MAP possible. In this situation, or any situation in which you are considering reorganization, these patterns present a range of choices that are favored in different circumstances. Prominent variables include the level of control you have over the other model, the level and type of cooperation between teams, and the degree of integration of features and data.
The following set of patterns covers some of the most common and important cases, which should give you a good idea of how to approach other cases. A crack team working closely on a tightly integrated product can deploy a large unified model. The need to serve different user communities or a limitation on the coordination abilities of the team might lead to a SHARED KERNEL or CUSTOMER/SUPPLIER relationships. Sometimes a good hard look at the requirements reveals that integration is not essential and it is best for systems to go their SEPARATE WAYS. And, of course, most projects have to integrate to some degree with legacy and external systems, which can lead to OPEN HOST SERVICES or ANTICORRUPTION LAYERS.