‘Design by Contract’ is good, right? It’s supposed to be, but you always have to deal with that one question: what do you do when a precondition fails in production? In some systems you can throw an exception, or abort processing. But, what do you do when you’re committed? When failure is unacceptable?
There are many answers to this question, but the one that I don’t hear often enough is that you can, in many cases, design away preconditions. Here’s a simplified example. Suppose that we need to do work using a date range. We can define a DateRange class and give it a constructor which takes two dates: the starting and ending dates of the interval:
new DateRange(start, end);
That works, but it lets us fumble our arguments like this:
new DateRange(end, start);
Sure, we can have a precondition that tells us that the start must be less than or equal to the end, but it’s still a shame that our interface allows the error. Can we make a better interface? We could have a DateRange constructor that accepts a starting date and an extent in days:
new DateRange(start, 4);
That’s better, but we still have a precondition. The number of days should be positive. Is there any way around that? Well, we could create a date range at a particular date and “inflate” it afterward:
DateRange vacation = new DateRange(startingDate);
vacation.addWeek();
vacation.addDay();
No more preconditions.
Is the interface a little funky? In this case, definitely. But the point is we don’t have to take preconditions as given. They aren’t always inevitable. In many cases, we can systematically reduce them with a bit of redesign.
Are there preconditions you can remove by redesigning your interfaces? Is the interface change worth it?
"""
DateRange vacation = new DateRange(startingDate);
vacation.addWeek();
vacation.addDay();
"""
You've actually just moved the preconditions. You still need to check that addWeek() and addDay() only take positive arguments. Sure, you can abs() them, but that breaks expectations. What if you want to be able to reduce the range as well? You're back to checking preconditions. If you're requiring a positive range, you could just take the first and abs() the difference between the two dates.
I'm not disagreeing with your general point, just your example. It isn't as easy as it looks...
Posted by: jerith | September 05, 2006 at 12:39 AM
If you want strong static guarantees in your code, you should really look into a more expressive statically-typed language. Ocaml and Haskell are likely candidates.
Posted by: Alex | September 05, 2006 at 01:05 AM
A shame Java doesn't have unsigned ints, eh?
I agree with the principle. The same can be said for the design of file formats.
Posted by: shaurz | September 05, 2006 at 04:33 AM
> You've actually just moved the preconditions. You still need to check that addWeek() and addDay() only take positive arguments.
No, they don't take any arguments at all; addWeek just adds one week. So yes, it's possible to remove the precondition entirely in this case, but like I said in the blog, it would be funky.
Posted by: Michael Feathers | September 05, 2006 at 05:17 AM
Actually, now someone can construct date ranges that are not really date ranges anymore, just starting dates. So, it is not only funky, but it still begs the question of what do you do when you're committed.
Perhaps a static creation method, along the lines of createDateRangeFromTo(start, end)?
Then you can say,
DateRange vacation = DateRange.createDateRangeFromTo("Aug/01/2006", "Aug/15/2006");
Posted by: Bob Evans | September 05, 2006 at 11:59 AM
Bob: "Actually, now someone can construct date ranges that are not really date ranges anymore, just starting dates."
Michael: I guess that gets down to the question of whether an empty range is a range. It could be defined that way, and I've seen that in problems similar to this one.
Bob: "So, it is not only funky, but it still begs the question of what do you do when you're committed. Perhaps a static creation method, along the lines of createDateRangeFromTo(start, end)?"
Michael: I like the name. It definitely helps, but the precondition is still there. It's still possible to pass an end that occurs before the start, and when that happens at runtime you have to decide what to do. I've seen systems which treat intervals with (end < start) as empty intervals. They allow an error just to keep the system running. Not pretty, and definitely domain-specific. In any case, I just wanted to show how preconditions can be eliminated at times. Many people just assume that they are fixed.
Posted by: Michael Feathers | September 05, 2006 at 12:47 PM
Make the normal "constructor" actually be a factory function in Date. Yeah, I know Java doesn't allow adding methods to existing classes, but Objective-C, Smalltalk, Ruby, and some other languages do.
DateRange range2daysInJan = Date( "Jan 01, 2006" ).rangeTo( Date( "Jan 02, 2006" ) );
Seems like in this case, we could also not care what order the dates come in:
DateRange same2daysInJan = Date( "Jan 02, 2006" ).rangeTo( Date( "Jan 01, 2006" ) );
assertEqual( range2daysInJan, same2daysInJan );
Posted by: keith ray | September 18, 2006 at 07:45 AM
I think you should have worked a little harder and made a working example rather than a clearly silly example.
Posted by: wonk | September 19, 2006 at 08:10 AM
I'm confused, Michael. The code in your example appears to be doing something different.
Posted by: Jason Gorman | September 22, 2006 at 12:52 AM
This article is spot on, because Java is imperative the constraint/relationship between a start and an end date is specified as a pre-condition. I the concept of removing pre-conditions makes sense, but its still impossible to remove null arguments.
Try the following:
TimeDuration duration = New TimeDuration(unitsOftime);
DateRange vacation = New DateRange(startingDate, duration);
as a duration is a size we don't care if it is positive or negative (we just make it positive), and by creating the duration prior to the daterange we don't need to "inflate" it.
Posted by: Mark Grant | December 02, 2006 at 01:57 PM