Making Implicit Concepts Explicit
- Sometimes concepts are present implictly in design or hinted during dicussions
- Transformations of the model and code, are when developers represent the implicit concepts explicitly in the model
- This transformation is a breakthrough that leads to a deeper model
Digging Out Concepts
- Implict concepts can be discovered by:
- Proactively searching
- Listening to the language used during discussions
- Experimenting
Listen to Language
- Sometimes domain experts or users might say something the developers don't understand and often is brushed off, thinking it's not important.
- When domain experts and developers are using vocabulary that is not in the design, it is often a warning sign that something is missing in the model.
- Opportunity to improve the model and design by including absent term.
- Specific terms or phrases may include concepts that can be included to improve the model
Hearing a Missing Concept in the Shipping Model Example
- Existing application can book a cargo
- Building an operations support application to help manage work orders for loading and unloading cargoes
- The application uses a routing engine to plan the trip of a cargo, each leg of the journey stored as a row in the database
- During the discussion between developer and shipping expert, the developer says a keyword "iternary", which the expert says is the main thing
- The developer realises that the routing service can return an iternary object instead of putting it into the database
- The concept of the iternary opened up opportunities for the model
- The team refactored the code to include the iternary concept and this had multiple benefits:
- Routing Service interface was more expressive
- Routing Service decoupled from database
- Removing domain logic from booking report and placing into isolated domain layer
- Expanding ubiquitous language to allow precise discussion of the model and design between developers and domain experts
Scrutinize Awkwardness
- Sometimes concepts are not obvious, you would need to dig and invent these concepts
- Places in the design that gets more complex as more requirements are added, are good places to dig for implict concepts
- Hard to recognise if there is a missing concept
- Sometimes domain experts are willing to experiment with the model,
- and other times the developers need to be the one to come up with new ideas using the expert as a validator
Earning Interest the Hard Way Example
- Hypothetical finance company that invests in commercial loans and other interest bearing assets
- Application that tracks investments and earnings from them
- Each night, 1 component runs a script to calculate all interest and fees for the day
- An Awkward Model
- With this model, the developer was struggling with increasing complexity of calculating interest.
- After a few discussions a deeper model was found, because the developer directly asked for help from domain expert
- Now the nightly script tells each Asset to calculateAccrualsThroughDate instead of the interest calculator
- Benfits of this model:
- Implements a new concept "accrual"
- Moves domain knowledge from script into domain layer
- Eliminate duplication by bringing fees and interest together
- In this example, developer found the interest calculator to be awkward and decided to dig for new concepts, by asking the domain expert for knowledge.
Contemplate Contradictions
- Different experts inteprete things in different ways
- The same person can provide information that is inconsistent after deep analysis
- Contradictions can act as clues to deeper models
Read the Book
- Don't overlook obvious concepts as they may play a big part in the model
- You can read books for fundamental concepts, but will still need to work with domain experts to distil the relevant parts.
Earning Interest by the Book
- Similar to the example of Earning Interest the Hard Way, except without the help of a domain expert
- Developer uses an introductory accounting book for help instead
- Model different from previous examples because the developers has no insight on Assets are income generators
- It also good to have different sources for information, books can fill the gaps of foundations which sometimes domain experts will gloss over
- Another option would be to read something written by another software professional who has worked in the same domain
- This could create a different starting point which may or may not have a different solution
Try, Try Again
- The examples omit the amount of trial and error
- Often follows lots of leads before finding the most clear and useful one
- Experimenting is a way to see what works and what doesn't
- Trying to avoid missteps in design results in lower quality because of less experiments
- Which is often longer than quick experiments
- Better to make mistakes and learn from them which will help developer understand
How to model less obvious concepts
Object orientation leads us to look for particular kinds of objects - nouns and verbs. But other kinds of categories may be less obvious, for example
- Constraints
- Processes
- Specifications
Constraints
Often emerge implicitly but greatly improve the model if made explicit.
class bucket {
private float capacity;
private float contents;
public void pourIn(float addedVolume) {
if (contents + addedVolume > capacity) {
contents = capacity;
}
else {
contents = contents + addedVolume
}
}
}
This bucket enforces the constraint that it can't contain more than its capacity, we can make it explicit.
class bucket {
private float capacity;
private float contents;
public void pourIn(float addedVolume) {
contents = constrainedToCapacity(contents + addedVolume)
}
private float constrainedToCapacity(float potentialVolume) {
if (potentialVolume > capacity) return capacity;
return potentialVolume;
}
}
This has a more obvious relationship to the model. Factoring the constraint into its own method allows us to give it a more intention revealing name which can be discussed. It also separates concerns from the caller. In more complex cases the constraint may not really fit in the object itself, for example:
- Evaluating the constraint requires information that does not fit with the object
- Related rules appear across multiple objects
- A lot of design conversation revolves around constraints but the implementation is hidden in procedural code
In these cases you can factor out the constraint into an explicit object, or model it as objects and relationships.
Review
In chapter 1 we added a constraint between Voyage and Cargo in the shipping domain with an overbooking policy constraint, which specified that 10% more cargo will be booked than capacity in order to account for cancellations.
Processes
- Processes that exist in the domain, not procedures, for example a routing process for shipping.
- A service is one way of expressing this. If there are multiple ways to carry out a process the algorithms can be made objects which represent different strategies.
- A process should be made explicit if it's discussed by the domain experts, rather than an implementation detail.
Specifications
An invoice could check if it's overdue. It could also check if it is delinquent, this could involve
- Checking if the invoice is overdue
- Past a grace period to pay back - Which may depend on the customer's account status
- The invoice may result in a second notice, or if that is passed it could be sent to a collection agency
The idea of an invoice as a representation of a request for payment could easily get lost in the rules. These rules will also pull in other dependencies on objects like customer accounts. A developer could attempt to fix this by writing the rule in the application layer (e.g. bill collection application), but the rules need to stay in the domain layer.
In the logic paradigm rules are expressed as boolean predicates which can be combined using operators such as "and" and "or". People have attempted to implement this with objects, but this is a major undertaking. We don't need to fully implement the logic paradigm - most rules fall into a few special cases and a more specialised implementation can better communicate intent.
Create specialised objects that evaluate to a boolean - testing methods become separate value objects. The new object is a specification - a constraint on the state of another object. It can test any object to see if it meets particular criteria. When the rules are complex multiple specifications can be combined.
A delinquent invoice can be modelled as a specification that states that it means to be delinquent.
A factory can create a specification using information form other sources - like a policy database. This should not be coupled to the domain entity.
Applying and implementing specification
There are a few purposes you may need to specify the state of an object
- To validate it
- To select it
- To create it
Validation
class DelinquentInvoiceSpecification extends InvoiceSpecification {
private Date currentDate;
public DelinquentInvoiceSpecification(Date currentDate) {
this.currentDate = currentDate;
}
public boolean isSatisfiedBy(Invoice candidate) {
int gracePreiod = candidate.customer().getPaymentGracePeriod();
Date firmDeadline = DateUtility.addDaysToDate(candidate.dueDate(), gracePeriod);
return currentDate.after(firmDeadline);
}
}
If we need to display a red flag when a salesperson brings up a customer with delinquent bills.
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
Validation tests a single object to see if it meets some criteria, the same concept can be used to find objects that satisfy the criteria. If we want to find all the customers with delinquent invoices, we could reuse the above specification, but there may be additional issues.
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;
}
Can get all the delinquent invoices by
Set delinquentInvoices = invoiceRepository.selectSatisfying(new DelinquentInvoiceSpecification(currentDate));
The data may not be in memory though, more likely a database - should reuse the database search functionality. We need to do this while keeping the model.
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 work well with repositories, but the details of the table structure have leaked into the domain - should be isolated in a mapping layer. With sql in the specification object, changes to the customer and invoice need to be tracked here - or the query could fail. Some ORMs would allow you to build a query in terms of the model objects. Otherwise, you can refactor the SQL objects by adding a method to the repository.
public class InvoiceRepository {
public Set selectWhereGracePeriodPast(Date aDate) {
String sql = whereGracePeriodPast_SQL(Date 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(currentDate);
}
public Set satisfyingElementsFrom(InvoiceSpecification spec) {
return spec.satisfyingElementsFrom(this);
}
}
public class DelinquentInvoiceSpecification {
public Set satisfyingElementsFrom(InvoiceRepository repository) {
return repository.selectWhereGracePeriodPast(currentDate);
}
}
Now the SQL is in the repository and the specification decides what query to execute. The rules aren't in the specification, but you can still read the definition (past grace period). The repository now only has a specialized query only used in one place, we could make it more generic potentially at the cost of performance due to pulling more invoices out of the database to process.
public class InvoiceRepository {
public Set selectWhereDueDateIsBefore(Date aDate) {
String sql = selectWhereDueDateIsBefore_SQL(Date aDate);
ResultSet queryResultSet = SQLDatabaseInterface.instance().executeQuery(sql);
return buildInvoicesFromResultSet(queryResultSet);
}
public String selectWhereDueDateIsBefore_SQL(Date aDate) {
return
"SELECT * FROM INVOICE, CUSTOMER" +
" WHERE INVOICE.DUE_DATE"
" < " + SQLUtility.dateAsSQL(currentDate);
}
public Set satisfyingElementsFrom(InvoiceSpecification spec) {
return spec.satisfyingElementsFrom(this);
}
}
public class DelinquentInvoiceSpecification {
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;
}
}
Creation
A specification is not a design - there are multiple designs that can satisfy a specification.
An interface of a generator that is defined in terms of a specification explicitly constrains the output.
- The generators implementation is decoupled from the specification
- The specification communicates rules explicitly
- The interface is more flexible
- Easier to test - the specification can be used to validate the output
Example - Chemical Warehouse Packer
Chemicals are stored in stacks of drums in large containers, most can be stored anywhere but some need special containers:
- volatile - ventilated containers
- explosive - armored containers
Also rules about combinations in containers.
- Biological sample - can't be in a container with explosives
Each chemical has a spec of what containers it can be in
- TNT - armored container
- Sand - anything
- Biological samples - not in a container with explosives
- Ammonia - Ventilated container
Start with validation instead of a procedure - write an explicit spec.
An isSatisfied method on the container specification could check for needed container features.
public class ContainerSpecification {
private ContainerFeature requiredFeature;
public ContainerSpecification(ContainerFeature required) {
requiredFeature = required;
}
boolean isSatisfiedBy(Container aContainer) {
return aContainer.getFeatures().contains(requiredFeature);
}
}
Usage
tnt.setContainerSpecification(new ContainerSpecification(ARMORED));
A method on the container can check that all the features required by its chemical drums
boolean isSafelyPacked() {
while (it.hasNext()) {
Drum drum = (Drum) it.next();
if (!drum.containerSpecification().isSatisfiedBy(this))
return false;
}
return true;
}
Could write a monitoring application that reports unsafe situations
Iterator it = containers.iterator();
while (it.hasNext()) {
Container container = (Container) it.next();
if (!container.isSafelyPacked())
unsafeContainers.add(container)
}
But this isn't what we were supposed to write - we were supposed to pack the containers, but this can be used to test the packer. Now with the specification we can write a service that takes collections of Drums and Containers and pack them following the rules.
public interface WarehousePacker {
public void pack(Collection containersToFill, Collection drumsToPack) throws NoAnswerFoundException;
/* ASSSERTION: At the end of pack() the ContainerSpecification of each Drum should be satisfied by its Container. */
}
Now designing an optimised constraint solver for the WarehousePacker has been decoupled from the rest of the application through the specification - but the rules stay in the domain objects where they belong. Each object declares how it should be packed.
Writing the optimized logic for the packer is taking a while, in the meantime another team is trying to build the UI and database integration. Instead of being blocked the other team can write a very simple Packer that can be swapped out when the real one is ready.
public class Container {
private double capacity;
private Set contents; //Drums
public boolean hasSpaceFor(Drum aDrum) {
return remainingSpace() >= aDrum.getSize();
}
public double remainingSpace() {
double totalContentSize = 0.0;
Iterator it = contents.iterator();
while (it.hasNext()) {
Drum aDrum = (Drum) it.next();
totalContentSize = totalContentSize + aDrum.getSize();
}
return capacity – totalContentSize;
}
public boolean canAccommodate(Drum aDrum) {
return hasSpaceFor(aDrum) &&
aDrum.getContainerSpecification().isSatisfiedBy(this);
}
}
public class PrototypePacker implements WarehousePacker {
public void pack(Collection containers, Collection drums)
throws NoAnswerFoundException {
/* This method fulfills the ASSERTION as written. However,
when an exception is thrown, Containers' contents may
have changed. Rollback must be handled at a higher
level. */
Iterator it = drums.iterator();
while (it.hasNext()) {
Drum drum = (Drum) it.next();
Container container =
findContainerFor(containers, drum);
container.add(drum);
}
}
public Container findContainerFor(
Collection containers, Drum drum)
throws NoAnswerFoundException {
Iterator it = containers.iterator();
while (it.hasNext()) {
Container container = (Container) it.next();
if (container.canAccommodate(drum))
return container;
}
throw new NoAnswerFoundException();
}
}
This packer may pack all the specialty containers with sand and run out of those containers before it gets to the explosives and volatiles. But it allows the second team to move forward with integrating the external systems. The domain experts can also interact with the early version and give feedback that clarifies the requirements.
Decoupling an implementation from the interface allows different parts of a project to be developed in parallel.