Suppose it's the early 1990's and you're James Gosling implementing String.substring(int, int)
for the first time. What should happen when the index arguments are out-of-range? Should these tests pass? Or throw?
public void testSubstring() {
assertEquals("class", "superclass".substring(5, 32));
assertEquals("super", "superclass".substring(-2, 5));
assertEquals("", "superclass".substring(20, 24));
assertEquals("superclass", "superclass".substring(10, 0));
}
Forgiving APIs
In a forgiving API, these tests pass. The implementation would recognize the out-of-range indices and correct for them. Benefits of forgiving APIs:
- Fault-tolerant. An off-by-one mistake won't bring a production system to its knees.
- Easier to code against. If you don't know what to use for a given argument, just pass
null
and the implementation will do something reasonable.
Strict APIs
In a strict APIs, the out-of-range arguments to substring
are forbidden and the method throws an IllegalArgumentException
. Benefits of strict APIs:
- Fail-fast. An off-by-one mistake will be caught in unit tests, if they exist.
- Easier to maintain. By limiting the number of valid inputs, there's less behaviour to maintain and test.
- More Predictable. Mapping invalid inputs to behaviour is an artform. In the example, should
substring(10, 0)
return the empty string? Or "superclass"? What would the caller expect?
For maintainability, I almost always prefer strict APIs. I like to think of the classes in my code as the gears in a fine Swiss watch. Everything fits together tightly, with firm constraints on both the inputs and the outputs. I can refactor with confidence because the system simply won't work if I've introduced problems into it. With a forgiving API, I could introduce bugs and not find out about them until much later.