Domain-Driven Design

Home > Other > Domain-Driven Design > Page 23
Domain-Driven Design Page 23

by Eric Evans


  Logic programming provides the concept of separate, combinable, rule objects called “predicates,” but full implementation of this concept with objects is cumbersome. It is also so general that it doesn’t communicate intent as much as more specialized designs.

  Fortunately, we don’t really need to fully implement logic programming to get a large benefit. Most of our rules fall into a few special cases. We can borrow the concept of predicates and create specialized objects that evaluate to a Boolean. Those testing methods that get out of hand will neatly expand into objects of their own. They are little truth tests that can be factored out into a separate VALUE OBJECT. This new object can evaluate another object to see if the predicate is true for that object.

  Figure 9.12

  To put it another way, the new object is a specification. A SPECIFICATION states a constraint on the state of another object, which may or may not be present. It has multiple uses, but one that conveys the most basic concept is that a SPECIFICATION can test any object to see if it satisfies the specified criteria.

  Therefore:

  Create explicit predicate-like VALUE OBJECTS for specialized purposes. A SPECIFICATION is a predicate that determines if an object does or does not satisfy some criteria.

  Many SPECIFICATIONS are simple, special-purpose tests, as in the delinquent invoice example. In cases where the rules are complex, the concept can be extended to allow simple specifications to be combined, just as predicates are combined with logical operators. (This technique will be discussed in the next chapter.) The fundamental pattern stays the same and provides a path from the simpler to more complex models.

  The case of the delinquent invoice can be modeled using a SPECIFICATION that states what it means to be delinquent and that can evaluate any Invoice and make the determination.

  Figure 9.13. A more elaborate delinquency rule factored out as a SPECIFICATION

  The SPECIFICATION keeps the rule in the domain layer. Because the rule is a full-fledged object, the design can be a more explicit reflection of the model. A FACTORY can configure a SPECIFICATION using information from other sources, such as the customer’s account or the corporate policy database. Providing direct access to these sources from the Invoice would couple the objects in a way that does not relate to the request for payment (the basic responsibility of Invoice). In this case, the Delinquent Invoice Specification was to be created, used to evaluate some Invoices, and then discarded, so a specific evaluation date was built right in—a nice simplification. A SPECIFICATION can be given the information it will need to do its job in a simple, straightforward way.

  The basic concept of SPECIFICATION is very simple and helps us think about a domain modeling problem. But a MODEL-DRIVEN DESIGN requires an effective implementation that also expresses the concept. To pull that off requires digging a little deeper into how the pattern will be applied. A domain pattern is not just a neat idea for a UML diagram; it is a solution to a programming problem that retains a MODEL-DRIVEN DESIGN.

  When you apply a pattern appropriately, you can tap into a whole body of thought about how to approach a class of domain modeling problem, and you can benefit from years of experience in finding effective implementations. There is a lot of detail in the discussion of SPECIFICATION that follows: many options for features and approaches to implementation. A pattern is not a cookbook. It lets you start from a base of experience to develop your solution, and it gives you some language to talk about what you are doing.

  You may want to skim the key concepts when first reading. Later, when you run into the situation, you can come back and draw on the experience captured in the detailed discussion. Then you can go and figure out a solution to your problem.

  Applying and Implementing SPECIFICATION

  Much of the value of SPECIFICATION is that it unifies application functionality that may seem quite different. We might need to specify the state of an object for one or more of these three purposes.

  1. To validate an object to see if it fulfills some need or is ready for some purpose

  2. To select an object from a collection (as in the case of querying for overdue invoices)

  3. To specify the creation of a new object to fit some need

  These three uses—validation, selection, and building to order—are the same on a conceptual level. Without a pattern such as SPECIFICATION, the same rule may show up in different guises, and possibly contradictory forms. The conceptual unity can be lost. Applying the SPECIFICATION pattern allows a consistent model to be used, even when the implementation may have to diverge.

  Validation

  The simplest use of a SPECIFICATION is validation, and it is the use that demonstrates the concept most straightforwardly.

  Figure 9.14. A model applying a SPECIFICATION for validation

  class DelinquentInvoiceSpecification extends

  InvoiceSpecification {

  private Date currentDate;

  // An instance is used and discarded on a single date

  public DelinquentInvoiceSpecification(Date currentDate) {

  this.currentDate = currentDate;

  }

  public boolean isSatisfiedBy(Invoice candidate) {

  int gracePeriod =

  candidate.customer().getPaymentGracePeriod();

  Date firmDeadline =

  DateUtility.addDaysToDate(candidate.dueDate(),

  gracePeriod);

  return currentDate.after(firmDeadline);

  }

  }

  Now, suppose we need to display a red flag whenever a salesperson brings up a customer with delinquent bills. We just have to write a method in a client class, something like this.

  public boolean accountIsDelinquent(Customer customer) {

  Date today = new Date();

  Specification delinquentSpec =

  new DelinquentInvoiceSpecification(today);

  Iterator it = customer.getInvoices().iterator();

  while (it.hasNext()) {

  Invoice candidate = (Invoice) it.next();

  if (delinquentSpec.isSatisfiedBy(candidate)) return true;

  }

  return false;

  }

  Selection (or Querying)

  Validation tests an individual object to see if it meets some criteria, presumably so that the client can act on the conclusion. Another common need is to select a subset of a collection of objects based on some criteria. The same concept of SPECIFICATION can be applied here, but implementation issues are different.

  Suppose there was an application requirement to list all customers with delinquent Invoices. In theory, the Delinquent Invoice Specification that we defined before will still serve, but in practice its implementation would probably have to change. To demonstrate that the concept is the same, let’s assume first that the number of Invoices is small, maybe already in memory. In this case, the straightforward implementation developed for validation still serves. The Invoice Repository could have a generalized method to select Invoices based on a SPECIFICATION:

  public Set selectSatisfying(InvoiceSpecification spec) {

  Set results = new HashSet();

  Iterator it = invoices.iterator();

  while (it.hasNext()) {

  Invoice candidate = (Invoice) it.next();

  if (spec.isSatisfiedBy(candidate)) results.add(candidate);

  }

  return results;

  }

  So a client could obtain a collection of all delinquent Invoices with a single code statement:

  Set delinquentInvoices = invoiceRepository.selectSatisfying(

  new DelinquentInvoiceSpecification(currentDate));

  That line of code establishes the concept behind the operation. Of course, the Invoice objects probably aren’t in memory. There may be thousands of them. In a typical business system, the data is probably in a relational database. And, as pointed out in earlier chapters, the model focus tends to get lost at these intersections with other technologies.

  Relational
databases have powerful search capabilities. How can we take advantage of that power to solve this problem efficiently while retaining the model of a SPECIFICATION? MODEL-DRIVEN DESIGN demands that the model stay in lockstep with the implementation, but it allows freedom to choose any implementation that faithfully captures the meaning of the model. Lucky for us, SQL is a very natural way to write SPECIFICATIONS.

  Here is a simple example, in which the query is encapsulated in the same class as the validation rule. A single method is added to the Invoice Specification and is implemented in the Delinquent Invoice Specification subclass:

  public String asSQL() {

  return

  "SELECT * FROM INVOICE, CUSTOMER" +

  " WHERE INVOICE.CUST_ID = CUSTOMER.ID" +

  " AND INVOICE.DUE_DATE + CUSTOMER.GRACE_PERIOD" +

  " < " + SQLUtility.dateAsSQL(currentDate);

  }

  SPECIFICATIONS mesh smoothly with REPOSITORIES, which are the building-block mechanisms for providing query access to domain objects and encapsulating the interface to the database (see Figure 9.15).

  Figure 9.15. The interaction between REPOSITORY and SPECIFICATION

  Now this design has some problems. Most important, the details of the table structure have leaked into the DOMAIN LAYER; they should be isolated in a mapping layer that relates the domain objects to the relational tables. Implicitly duplicating that information here could hurt the modifiability and maintainability of the Invoice and Customer objects, because any change to their mappings now have to be tracked in more than one place. But this example is a simple illustration of how to keep the rule in just one place. Some object-relational mapping frameworks provide the means to express such a query in terms of the model objects and attributes, generating the actual SQL in the infrastructure layer. This would let us have our cake and eat it too.

  When the infrastructure doesn’t come to the rescue, we can refactor the SQL out of the expressive domain objects by adding a specialized query method to the Invoice Repository. To avoid embedding the rule into the REPOSITORY, we have to express the query in a more generic way, one that doesn’t capture the rule but can be combined or placed in context to work the rule out (in this example, by using a double dispatch).

  public class InvoiceRepository {

  public Set selectWhereGracePeriodPast(Date aDate){

  //This is not a rule, just a specialized query

  String sql = whereGracePeriodPast_SQL(aDate);

  ResultSet queryResultSet =

  SQLDatabaseInterface.instance().executeQuery(sql);

  return buildInvoicesFromResultSet(queryResultSet);

  }

  public String whereGracePeriodPast_SQL(Date aDate) {

  return

  "SELECT * FROM INVOICE, CUSTOMER" +

  " WHERE INVOICE.CUST_ID = CUSTOMER.ID" +

  " AND INVOICE.DUE_DATE + CUSTOMER.GRACE_PERIOD" +

  " < " + SQLUtility.dateAsSQL(aDate);

  }

  public Set selectSatisfying(InvoiceSpecification spec) {

  return spec.satisfyingElementsFrom(this);

  }

  }

  The asSql() method on Invoice Specification is replaced with satisfyingElementsFrom(InvoiceRepository), which Delinquent Invoice Specification implements as:

  public class DelinquentInvoiceSpecification {

  // Basic DelinquentInvoiceSpecification code here

  public Set satisfyingElementsFrom(

  InvoiceRepository repository) {

  //Delinquency rule is defined as:

  // "grace period past as of current date"

  return repository.selectWhereGracePeriodPast(currentDate);

  }

  }

  This puts the SQL in the REPOSITORY, while the SPECIFICATION controls what query should be used. The rules aren’t as neatly collected into the SPECIFICATION, but the essential declaration is there of what constitutes delinquency (that is, past grace period).

  The REPOSITORY now has a very specialized query that most likely will be used only in this case. That is acceptable, but depending on the relative numbers of Invoices that are overdue compared to those that are delinquent, an intermediate solution that leaves the REPOSITORY methods more generic may still give good performance, while keeping the SPECIFICATION more self-explanatory.

  public class InvoiceRepository {

  public Set selectWhereDueDateIsBefore(Date aDate) {

  String sql = whereDueDateIsBefore_SQL(aDate);

  ResultSet queryResultSet =

  SQLDatabaseInterface.instance().executeQuery(sql);

  return buildInvoicesFromResultSet(queryResultSet);

  }

  public String whereDueDateIsBefore_SQL(Date aDate) {

  return

  "SELECT * FROM INVOICE" +

  " WHERE INVOICE.DUE_DATE" +

  " < " + SQLUtility.dateAsSQL(aDate);

  }

  public Set selectSatisfying(InvoiceSpecification spec) {

  return spec.satisfyingElementsFrom(this);

  }

  }

  public class DelinquentInvoiceSpecification {

  //Basic DelinquentInvoiceSpecification code here

  public Set satisfyingElementsFrom(

  InvoiceRepository repository) {

  Collection pastDueInvoices =

  repository.selectWhereDueDateIsBefore(currentDate);

  Set delinquentInvoices = new HashSet();

  Iterator it = pastDueInvoices.iterator();

  while (it.hasNext()) {

  Invoice anInvoice = (Invoice) it.next();

  if (this.isSatisfiedBy(anInvoice))

  delinquentInvoices.add(anInvoice);

  }

  return delinquentInvoices;

  }

  }

  We’ll take a performance hit with this code, because we pull out more Invoices and then have to select from them in memory. Whether this is an acceptable cost for the better factoring of responsibility depends entirely on circumstances. There are many ways to implement the interactions between SPECIFICATIONS and REPOSITORIES, to take advantage of the development platform, while keeping the basic responsibilities in place.

  Sometimes, to improve performance, or more likely to tighten security, queries may be implemented on the server as stored procedures. In that case, the SPECIFICATION could carry only the parameters allowed by the stored procedure. For all that, there is no difference in the model between these various implementations. The choice of implementation is free except where specifically constrained by the model. The price comes in a more cumbersome way of writing and maintaining queries.

  This discussion barely scratches the surface of the challenges of combining SPECIFICATIONS with databases, and I’ll make no attempt to cover all the considerations that may arise. I just want to give a taste of the kind of choices that have to be made. Mee and Hieatt discuss a few of the technical issues involved in designing REPOSITORIES with SPECIFICATIONS in Fowler 2003.

  Building to Order (Generating)

  When the Pentagon wants a new fighter jet, officials write a specification. This specification may require that the jet reach Mach 2, that it have a range of 1800 miles, that it cost no more than $50 million, and so on. But however detailed it is, the specification is not a design for a plane, much less a plane. An aerospace engineering company will take the specification and create one or more designs based on it. Competing companies may produce different designs, all of which presumably satisfy the original spec.

  Many computer programs generate things, and those things have to be specified. When you place a picture into a word-processing document, the text flows around it. You have specified the location of the picture, and perhaps the style of text flow. The exact placement of the words on the page is then worked out by the word processor in such a way that it meets your specification.

  Although it may not be apparent at first, this is the same concept of a SPECIFICATION that was applied to validation and selection. We are specifying cri
teria for objects that are not yet present. The implementation will be quite different, however. This SPECIFICATION is not a filter for preexisting objects, as with querying. It is not a test for an existing object, as with validation. This time, a whole new object or set of objects will be made or reconfigured to satisfy the SPECIFICATION.

  Without using SPECIFICATION, a generator can be written that has procedures or a set of instructions that create the needed objects. This code implicitly defines the behavior of the generator.

  Instead, an interface of the generator that is defined in terms of a descriptive SPECIFICATION explicitly constrains the generator’s products. This approach has several advantages.

  • The generator’s implementation is decoupled from its interface. The SPECIFICATION declares the requirements for the output but does not define how that result is reached.

  • The interface communicates its rules explicitly, so developers can know what to expect from the generator without understanding all details of its operation. The only way to predict the behavior of a procedurally defined generator is to run cases or to understand every line of code.

  • The interface is more flexible, or can be enhanced with more flexibility, because the statement of the request is in the hands of the client, while the generator is only obligated to fulfill the letter of the SPECIFICATION.

  • Last, but not least, this kind of interface is easier to test, because the model contains an explicit way to define input into the generator that is also a validation of the output. That is, the same SPECIFICATION that is passed into the generator’s interface to constrain the creation process can also be used, in its validation role (if the implementation supports it) to confirm that the created object is correct. (This is an example of an ASSERTION, discussed in Chapter 10.)

 

‹ Prev