Domain-driven design in Android — Part 2
In part 1 we learned the need for a model. But how do we arrive at a model?
To tackle the complexity at the heart of software, we can’t just sit in a room by ourselves and write code. “Crunching knowledge” means talking to stakeholders and domain experts to distill a model. Evans gives the following example of the following conversation in the context of printed circuit board (PCB) design:
Expert 1: It isn’t enough to say a signal arrives at a ref-des, we have to know the pin.
Expert 2: Same thing as a component instance. Ref-des is what it’s called in a particular tool we use.
Expert 1: Anyhow, a net connects a particular pin of one instance to particular pin of another.
Developer: Are you saying that a pin belongs to only one component instance and to only one net?
Expert 1: Yes that’s right.
Expert 2: Also, every net has a topology, an arrangement that determines the way the elements of the net connect.
Developer: OK, how about this?
The back and forward eventually results in the following diagram:
Or if we sketch in Kotlin:
The point is to engage in a dialectic with experts around the business in order to distill a model that both can understand. The real code will be bound to this model, but this is a two-way binding: when we implement and we uncover an impreciseness in our model, we go back and rework together with domain experts.
Beyond just nouns
The kind of knowledge captured in a model such as the PCB example goes beyond “find the nouns”
While we have ended up with a model that describes the system, this is not just a series of names for columns in the database. Notice that most classes in our sketch have member functions (instance methods in Java). These functions are important for fleshing out business rules — in the above example, the member functions show how users apply the concept of a signal to calculate the number of hops to avoid long signal delays in their PCB design. A long signal delay means that the signal may not arrive within a clock cycle, and so we imagine a signal being pushed through component to pin to net and incrementing as it passes through a net.
One sign we have gone awry in our modelling is if we end up with an “anemic domain model.” According to Vaughn Vernon in Implementing Domain Driven Design, this is when we end up with software components that are supposed to be our domain model that hold values but hold no business logic. The business logic is instead contained in “manager classes” and their like — software components that manipulate these bare classes. More about this later.
Make business rules explicit
Domain-Driven Design elaborates in some detail. Imagine an app for booking cargo onto a ship voyage. We could easily write a method like this:
But what about overbooking? There are always last minute cancellations and so it’s wasteful to just book capacity. We could always implement overbooking of 110% as the following:
But, as DDD explains:
Now an important business rule is hidden as a guard clause in an application method
In writing the rule as guard clause, we have pursued an implementation that is not bound to the model and started to tug at the threads that bind model and implementation. As DDD explains, a business expert could not read this above code to verify the rules for overbooking and any documents or areas in the business where overbooking rules are set remain disconnected from the code.
Instead, we might want to revisit the model and adjust. Imagine instead an
OverbookingPolicy in the model. This parallels a real policy within the business set by a specific department and maintained in a document. Now the code looks like this:
OverbookingPolicy class has the function:
This is not to say every trivial conditional should become a class. Rather, it shows that we need to think of the model and how it distills business information before we rush in to write code. The code should reflect a model, the closer the better, and the model is the means of communication.
Communication and the use of language
Domain experts live in a world rich in a meaning independent of implementation. Developers are often not interested in the outer meaning, instead focusing on pure implementation. If we are not careful, a wasteful chasm develops. As DDD explains:
Across this linguistic divide, the domain experts vaguely describe what they want. Developers, struggling to understand a domain new to them, vaguely understand. A few members of the team manage to become bilingual, but they become bottlenecks of information flow, and their translations are inexact.
The wastes from translation are obvious to anyone who has experienced arguments with product owners or testers about the meaning of a requirement.
Instead of the chasm, DDD proposes an ubiquitous language. It is called “ubiquitous” because it is present in every aspect of the team’s work. Because of our two-way binding between model and implementation, the ubiquitous language can contain names of classes and prominent operations. Just as domain experts may need to learn to use names of classes and operations that are informed by the model, developers may have to understand business concepts that transcend mere computation. This is in order to have a language for the team to communicate without the need for wasteful translation.
At the start this may seem daunting and unnatural. Especially since the limits of a model have not been discovered. But with knowledge crunching, implementation revealing inadequacies of a model, returning to the model, and refactoring the implementation we continually refine a language to work with.
Use the model as the backbone of a language. Commit the team to exercising that language relentlessly in all communication within the team and in the code. Use the same language in diagrams, writing, and especially speech.
Continued in part 3