Discovering new concepts
Gaining deeper insight into the domain
In the last post, we looked at how we could represent a business rule using types.
The rule was: "A contact must have an email or a postal address".
And the type we designed was:
Now let's say that the business decides that phone numbers need to be supported as well. The new business rule is: "A contact must have at least one of the following: an email, a postal address, a home phone, or a work phone".
How can we represent this now?
A little thought reveals that there are 15 possible combinations of these four contact methods. Surely we don't want to create a union case with 15 choices? Is there a better way?
Let's hold that thought and look at a different but related problem.
Forcing breaking changes when requirements change
Here's the problem. Say that you have a contact structure which contains a list of email addresses and also a list of postal addresses, like so:
And, also let's say that you have created a printReport
function that loops through the information and prints it out in a report:
Crude, but simple and understandable.
Now if the new business rule comes into effect, we might decide to change the structure to have some new lists for the phone numbers. The updated structure will now look something like this:
If you make this change, you also want to make sure that all the functions that process the contact infomation are updated to handle the new phone cases as well.
Certainly, you will be forced to fix any pattern matches that break. But in many cases, you would not be forced to handle the new cases.
For example, here's printReport
updated to work with the new lists:
Can you see the deliberate mistake? Yes, I forgot to change the function to handle the phones. The new fields in the record have not caused the code to break at all. There is no guarantee that you will remember to handle the new cases. It would be all too easy to forget.
Again, we have the challenge: can we design types such that these situations cannot easily happen?
Deeper insight into the domain
If you think about this example a bit more deeply, you will realize that we have missed the forest for the trees.
Our initial concept was: "to contact a customer, there will be a list of possible emails, and a list of possible addresses, etc".
But really, this is all wrong. A much better concept is: "To contact a customer, there will be a list of contact methods. Each contact method could be an email OR a postal address OR a phone number".
This is a key insight into how the domain should be modelled. It creates a whole new type, a "ContactMethod", which resolves our problems in one stroke.
We can immediately refactor the types to use this new concept:
And the reporting code must now be changed to handle the new type as well:
These changes have a number of benefits.
First, from a modelling point of view, the new types represent the domain much better, and are more adaptable to changing requitements.
And from a development point of view, changing the type to be a union means that any new cases that we add (or remove) will break the code in a very obvious way, and it will be much harder to accidentally forget to handle all the cases.
Back to the business rule with 15 possible combinations
So now back to the original example. We left it thinking that, in order to encode the business rule, we might have to create 15 possible combinations of various contact methods.
But the new insight from the reporting problem also affects our understanding of the business rule.
With the "contact method" concept in our heads, we can rephase the requirement as: "A customer must have at least one contact method. A contact method could be an email OR a postal addresses OR a phone number".
So let's redesign the Contact
type to have a list of contact methods:
But this is still not quite right. The list could be empty. How can we enforce the rule that there must be at least one contact method?
The simplest way is to create a new field that is required, like this:
In this design, the PrimaryContactMethod
is required, and the secondary contact methods are optional, which is exactly what the business rule requires!
And this refactoring too, has given us some insight. It may be that the concepts of "primary" and "secondary" contact methods might, in turn, clarify code in other areas, creating a cascading change of insight and refactoring.
Summary
In this post, we've seen how using types to model business rules can actually help you to understand the domain at a deeper level.
In the next post, we'll see how types can help with representing fine-grained states.
Last updated
Was this helpful?