Book Read Free

Domain-Driven Design

Page 25

by Eric Evans


  // ending with the assignment of new r, b, and y values.

  }

  OK, so it looks like this method combines two Paints together, the result having a larger volume and a mixed color.

  To shift our perspective, let’s write a test for this method. (This code is based on the JUnit test framework.)

  public void testPaint() {

  // Create a pure yellow paint with volume=100

  Paint yellow = new Paint(100.0, 0, 50, 0);

  // Create a pure blue paint with volume=100

  Paint blue = new Paint(100.0, 0, 0, 50);

  // Mix the blue into the yellow

  yellow.paint(blue);

  // Result should be volume of 200.0 of green paint

  assertEquals(200.0, yellow.getV(), 0.01);

  assertEquals(25, yellow.getB());

  assertEquals(25, yellow.getY());

  assertEquals(0, yellow.getR());

  }

  The passing test is the starting point. It is unsatisfying at this point because the code in the test doesn’t tell us what it is doing. Let’s rewrite the test to reflect the way we would like to use the Paint objects if we were writing a client application. Initially, this test will fail. In fact, it won’t even compile. We are writing it to explore the interface design of the Paint object from the client developer’s point of view.

  public void testPaint() {

  // Start with a pure yellow paint with volume=100

  Paint ourPaint = new Paint(100.0, 0, 50, 0);

  // Take a pure blue paint with volume=100

  Paint blue = new Paint(100.0, 0, 0, 50);

  // Mix the blue into the yellow

  ourPaint.mixIn(blue);

  // Result should be volume of 200.0 of green paint

  assertEquals(200.0, ourPaint.getVolume(), 0.01);

  assertEquals(25, ourPaint.getBlue());

  assertEquals(25, ourPaint.getYellow());

  assertEquals(0, ourPaint.getRed());

  }

  We should take our time to write a test that reflects the way we would like to talk to these objects. After that, we refactor the Paint class to make the test pass.

  Figure 10.3

  The new method name may not tell the reader everything about the effect of “mixing in” another Paint (for that we’ll need ASSERTIONS, coming up in a few pages). But it will clue the reader in enough to get started using the class, especially with the example the test provides. And it will allow the reader of the client code to interpret the client’s intent. In the next few examples in this chapter, we’ll refactor this class again to make it even clearer.

  Entire subdomains can be carved off into separate modules and encapsulated behind INTENTION-REVEALING INTERFACES. Using such whittling to focus a project and manage the complexity of a large system will be discussed more in Chapter 15, “Distillation,” with COHESIVE MECHANISMS and GENERIC SUBDOMAINS.

  But in the next two patterns, we’ll set out to make the consequences of using a method very predictable. Complex logic can be done safely in SIDE-EFFECT-FREE FUNCTIONS. Methods that change system state can be characterized with ASSERTIONS.

  Side-Effect-Free Functions

  Operations can be broadly divided into two categories, commands and queries. Queries obtain information from the system, possibly by simply accessing data in a variable, possibly performing a calculation based on that data. Commands (also known as modifiers) are operations that affect some change to the systems (for a simple example, by setting a variable). In standard English, the term side effect implies an unintended consequence, but in computer science, it means any effect on the state of the system. For our purposes, let’s narrow that meaning to any change in the state of the system that will affect future operations.

  Why was the term side effect adopted and applied to quite intentional changes affected by operations? I assume this was based on experience with complex systems. Most operations call on other operations, and those called invoke still other operations. As soon as this arbitrarily deep nesting is involved, it becomes very hard to anticipate all the consequences of invoking an operation. The developer of the client may not have intended the effects of the second-tier and third-tier operations—they’ve become side effects in every sense of the phrase. Elements of a complex design interact in other ways that are likely to produce the same unpredictability. The use of the term side effect underlines the inevitability of that interaction.

  Interactions of multiple rules or compositions of calculations become extremely difficult to predict. The developer calling an operation must understand its implementation and the implementation of all its delegations in order to anticipate the result. The usefulness of any abstraction of interfaces is limited if the developers are forced to pierce the veil. Without safely predictable abstractions, the developers must limit the combinatory explosion, placing a low ceiling on the richness of behavior that is feasible to build.

  Operations that return results without producing side effects are called functions. A function can be called multiple times and return the same value each time. A function can call on other functions without worrying about the depth of nesting. Functions are much easier to test than operations that have side effects. For these reasons, functions lower risk.

  Obviously, you can’t avoid commands in most software systems, but the problem can be mitigated in two ways. First, you can keep the commands and queries strictly segregated in different operations. Ensure that the methods that cause changes do not return domain data and are kept as simple as possible. Perform all queries and calculations in methods that cause no observable side effects (Meyer 1988).

  Second, there are often alternative models and designs that do not call for an existing object to be modified at all. Instead, a new VALUE OBJECT, representing the result of the computation, is created and returned. This is a common technique, which will be illustrated in the example that follows. A VALUE OBJECT can be created in answer to a query, handed off, and forgotten—unlike an ENTITY, whose life cycle is carefully regulated.

  VALUE OBJECTS are immutable, which implies that, apart from initializers called only during creation, all their operations are functions. VALUE OBJECTS, like functions, are safer to use and easier to test. An operation that mixes logic or calculations with state change should be refactored into two separate operations (Fowler 1999, p. 279). But by definition, this segregation of side effects into simple command methods only applies to ENTITIES. After completing the refactoring to separate modification from querying, consider a second refactoring to move the responsibility for the complex calculations into a VALUE OBJECT. The side effect often can be completely eliminated by deriving a VALUE OBJECT instead of changing existing state, or by moving the entire responsibility into a VALUE OBJECT.

  Therefore:

  Place as much of the logic of the program as possible into functions, operations that return results with no observable side effects. Strictly segregate commands (methods that result in modifications to observable state) into very simple operations that do not return domain information. Further control side effects by moving complex logic into VALUE OBJECTS when a concept fitting the responsibility presents itself.

  SIDE-EFFECT-FREE FUNCTIONS, especially in immutable VALUE OBJECTS, allow safe combination of operations. When a FUNCTION is presented through an INTENTION-REVEALING INTERFACE, a developer can use it without understanding the detail of its implementation.

  Example: Refactoring the Paint-Mixing Application Again

  A program for paint stores can show a customer the result of mixing standard paints. Picking up where we left off in the last example, here is the single domain class.

  Figure 10.4

  public void mixIn(Paint other) {

  volume = volume.plus(other.getVolume());

  // Many lines of complicated color-mixing logic

  // ending with the assignment of new red, blue,

  // and yellow values.

  }

  Figure 10.5. T
he side effects of the mixIn() method

  A lot is happening in the mixIn() method, but this design does follow the rule of separating modification from querying. One concern, which we’ll take up later, is that the volume of the paint 2 object, the argument of the mixIn() method, has been left in limbo. Paint 2’s volume is unchanged by the operation, which doesn’t seem quite logical in the context of this conceptual model. This was not a problem for the original developers because, as near as we can tell, they had no interest in the paint 2 object after the operation, but it is hard to anticipate the consequences of side effects or their absence. We’ll return to this question soon in the discussion of ASSERTIONS. For now, let’s look at color.

  Color is an important concept in this domain. Let’s try the experiment of making it an explicit object. What should it be called? “Color” comes to mind first, but earlier knowledge crunching had already yielded the important insight that color mixing is different for paint than it is for the more familiar RGB light display. The name needs to reflect this.

  Figure 10.6

  Factoring out Pigment Color does communicate more than the earlier version, but the computation is the same, still in the mixIn() method. When we moved out the color data, we should have taken related behavior with it. Before we do, note that Pigment Color is a VALUE OBJECT. Therefore, it should be treated as immutable. When we mixed paint, the Paint object itself was changed. It was an ENTITY with an ongoing life story. In contrast, a Pigment Color representing a particular shade of yellow is always exactly that. Instead, mixing will result in a new Pigment Color object representing the new color.

  Figure 10.7

  public class PigmentColor {

  public PigmentColor mixedWith(PigmentColor other,

  double ratio) {

  // Many lines of complicated color-mixing logic

  // ending with the creation of a new PigmentColor object

  // with appropriate new red, blue, and yellow values.

  }

  }

  public class Paint {

  public void mixIn(Paint other) {

  volume = volume + other.getVolume();

  double ratio = other.getVolume() / volume;

  pigmentColor =

  pigmentColor.mixedWith(other.pigmentColor(), ratio);

  }

  }

  Figure 10.8

  Now the modification code in Paint is as simple as possible. The new Pigment Color class captures knowledge and communicates it explicitly, and it provides a SIDE-EFFECT-FREE FUNCTION whose result is easy to understand, easy to test, and safe to use or combine with other operations. Because it is so safe, the complex logic of color mixing is truly encapsulated. Developers using this class don’t have to understand the implementation.

  Assertions

  Separating complex computations into SIDE-EFFECT-FREE FUNCTIONS cuts the problem down to size, but there is still a residue of commands on the ENTITIES that produce side effects, and anyone using them must understand their consequences. ASSERTIONS make side effects explicit and easier to deal with.

  True, a command containing no complex computations may be fairly easy to interpret by inspection. But in a design where larger parts are built of smaller ones, a command may invoke other commands. The developer using the high-level command must understand the consequences of each underlying command. So much for encapsulation. And because object interfaces do not restrict side effects, two subclasses that implement the same interface can have different side effects. The developer using them will want to know which is which to anticipate the consequences. So much for abstraction and polymorphism.

  When the side effects of operations are only defined implicitly by their implementation, designs with a lot of delegation become a tangle of cause and effect. The only way to understand a program is to trace execution through branching paths. The value of encapsulation is lost. The necessity of tracing concrete execution defeats abstraction.

  We need a way of understanding the meaning of a design element and the consequences of executing an operation without delving into its internals. INTENTION-REVEALING INTERFACES carry us part of the way there, but informal suggestions of intentions are not always enough. The “design by contract” school goes the next step, making “assertions” about classes and methods that the developer guarantees will be true. This style is discussed in detail in Meyer 1988. Briefly, “post-conditions” describe the side effects of an operation, the guaranteed outcome of calling a method. “Preconditions” are like the fine print on the contract, the conditions that must be satisfied in order for the post-condition guarantee to hold. Class invariants make assertions about the state of an object at the end of any operation. Invariants can also be declared for entire AGGREGATES, rigorously defining integrity rules.

  All these assertions describe state, not procedures, so they are easier to analyze. Class invariants help characterize the meaning of a class, and simplify the client developer’s job by making the objects more predictable. If you trust the guarantee of a post-condition, you don’t have to worry about how a method works. The effects of delegations should already be incorporated into the assertions.

  Therefore:

  State post-conditions of operations and invariants of classes and AGGREGATES. If ASSERTIONS cannot be coded directly in your programming language, write automated unit tests for them. Write them into documentation or diagrams where it fits the style of the project’s development process.

  Seek models with coherent sets of concepts, which lead a developer to infer the intended ASSERTIONS, accelerating the learning curve and reducing the risk of contradictory code.

  Even though many object-oriented languages don’t currently support ASSERTIONS directly, ASSERTIONS are still a powerful way of thinking about a design. Automated unit tests can partially compensate for the lack of language support. Because ASSERTIONS are all in terms of states, rather than procedures, they make tests easy to write. The test setup puts the preconditions in place; then, after execution, the test checks to see if the post-conditions hold.

  Clearly stated invariants and pre- and post-conditions allow a developer to understand the consequences of using an operation or object. Theoretically, any noncontradictory set of assertions would work. But humans don’t just compile predicates in their heads. They will be extrapolating and interpolating the concepts of the model, so it is important to find models that make sense to people as well as satisfying the needs of the application.

  Example: Back to Paint Mixing

  Recall that in the previous example I was concerned about the ambiguity of what happens to the argument of the mixIn(Paint) operation on the Paint class.

  Figure 10.9

  The receiver’s volume is increased by the amount of the argument’s volume. Drawing on our general understanding of physical paint, this mixing process should deplete the other paint by the same amount, draining it to zero volume, or eliminating it completely. The current implementation does not modify the argument, and modifying arguments is a particularly risky kind of side effect anyway.

  To start on a solid footing, let’s state the post-condition of the mixIn() method as it is:

  After p1.mixIn(p2):

  p1.volume is increased by amount of p2.volume.

  p2.volume is unchanged.

  The trouble is, developers are going to make mistakes, because these properties don’t fit the concepts we have invited them to think about. The straightforward fix would be change the volume of the other paint to zero. Changing an argument is a bad practice, but it would be easy and intuitive. We could state an invariant:

  Total volume of paint is unchanged by mixing.

  But wait! While developers were pondering this option, they made a discovery. It turns out that there was a compelling reason the original designers made it this way. At the end, the program reports the list of unmixed paints that were added. After all, the ultimate purpose of this application is to help a user figure out which paints to put into a mixture. />
  So, to make the volume model logically consistent would make it unsuitable for its application requirements. There seems to be a dilemma. Are we stuck with documenting the weird post-condition and trying to compensate with good communication? Not everything in this world is intuitive, and sometimes that is the best answer. But in this case, the awkwardness seems to point to missing concepts. Let’s look for a new model.

  We Can See Clearly Now

  As we search for a better model, we have significant advantages over the original designers, because of the knowledge crunching and refactoring to deeper insight that has happened in the interim. For example, we compute color using a SIDE-EFFECT-FREE FUNCTION on a VALUE OBJECT. This means we can repeat the calculation any time we need to. We should take advantage of that.

  We seem to be giving Paint two different basic responsibilities. Let’s try splitting them.

  Now there is only one command, mixIn(). It just adds an object to a collection, an effect apparent from an intuitive understanding of the model. All other operations are SIDE-EFFECT-FREE FUNCTIONS.

  A test method confirming one of the ASSERTIONS listed in Figure 10.10 could look something like this (using the JUnit test framework):

  public void testMixingVolume {

  PigmentColor yellow = new PigmentColor(0, 50, 0);

  PigmentColor blue = new PigmentColor(0, 0, 50);

  StockPaint paint1 = new StockPaint(1.0, yellow);

  StockPaint paint2 = new StockPaint(1.5, blue);

  MixedPaint mix = new MixedPaint();

  mix.mixIn(paint1);

  mix.mixIn(paint2);

  assertEquals(2.5, mix.getVolume(), 0.01);

  }

  Figure 10.10

  This model captures and communicates more of the domain. The invariants and post-conditions make common sense, which will make them easier to maintain and use.

 

‹ Prev