One of the primary design goals of a domain model is to maintain the integrity of the model data, and to do so at a higher level than simple database constraints. A good domain model should be able to guarantee semantic consistency with respect to the business domain.
Validation is an important tool for consistency guarantees, but something that is often overlooked is the role of object design. Many validation rules can be replaced by designing objects so as to make it impossible to get into an invalid state in the first place. This post is about a simple example of doing just that.
The section of the model we’re concerned with looks like this:
We have a Company object, with references to Country, State, and Region objects. Country, State and Region are related in a strict hierarchy. If we knew that all countries had states and all states had regions, Company could just store a reference to Region and the rest would be implied. But we don’t have that luxury, so we need all three references. Obviously, there are some quite strong constraints on what can be considered consistent:
- A company’s state, if it exists, must belong the the company’s country
- A company’s region, if it exists, must belong to the company’s state
It’s simple to write validation rules to enforce these constraints, but we can more elegantly enforce them by embodying the rules in the behaviour of the domain objects. Here are the setters for country, state and region within the Company object:
public void setCountry(Country country) { if (this.country == null || !country.equals(this.country)) { this.country = country; setState(country.getStates().getDefault()); } } public void setState(State state) { if (this.state == null || !this.state.equals(state)) { this.state= state; setCountry(state.getCountry()); setRegion(state.getRegions().getDefault()); } } public void setRegion(Region region) { if (this.region == null || !this.region.equals(region)) { this.region = region; if (region != null) { setState(region.getState()); } } }
If we set the company’s region, that setter automatically takes care of setting the company’s state and country to match. If we change the company’s country, on the other hand, we don’t know what state or region were intended. However, we set them to defaults that are at least consistent. The calling module can make a more considered choice at its leisure.
So, with a little model support from the country and state – that is, the provision of a “default” option for state and region respectively – it is now completely impossible for our company to be in an inconsistent state, without ever needing to validate any inputs.
An aside about normalization
In this example, company.region is nullable, state and country are not. Obviously this example is a little denormalized – country is completely specified by specifying the state. But many models have this sort of wrinkle, especially when the underlying database can’t be refactored. We can reduce the impact of the denormalized database schema on the model by changing the setter for country to this:
private void setCountry(Country country) { this.country = country; }
Now we can only set the country by specifying a state. This more nearly matches the conceptual model, while retaining a country field in the company object for ORM purposes.
Conclusion
This is a very trivial example, but the principle is extremely powerful. A domain model often can enforce complex domain constraints simply by its built-in behaviour, either by internally adjusting its state or by simply making invalid operations unavailable. When possible, this approach is greatly preferable to reactive validation, which can tend to require either complex dirty checking, or endless revalidation of unchanging data.