Have you seen classes with a method
isValid() or something similar? You most definitely have seen and even used such classes. Maybe you have even written such a method yourself. But is it a good idea to allow objects with invalid state?
What does valid or invalid mean?
I can think at least of two meanings of validity of an object:
- It is invalid for a certain purpose, e.g. a credit card that is past the validity date or a string that can not be used as ISBN or a key that does not fit into the lock.
- A broken object that can not be used at all, at least not in the current state.
Invalid for a specific purpose
The first form of validity usually does not really belong to the objects that need to be tested. It depends on the purpose, or, in programmer speak, on the algorithm that uses the object, whether or not an object is valid for its purpose. Therefore, the algorithm itself or some functionality related to it should do the testing, not the object under test.
Often enough, there is more than one form of purpose a thing can serve, and there can be more of one form of validity. An extreme example is the string that can be used as ISBN number. You’d never write a
string::isValid() method, simply because it does not mean anything. You also would not write a
string::isISBN() method, because strings are used in so many contexts that are in no way related to ISBNs, and because you would also have to write methods like
isGPSCoordinate()… you get my point.
The objects may always be valid objects, but they do not necessarily belong to the subdomain we need for our application. Often enough, we use different types for such subdomains, so consider for example a class
ISBNString. It’s interface will differ from that of the more general string class, because for example you can not simply append more characters to a valid ISBN number.
Validity of an object for an algorithm is part of the algorithm, not of the object. Consider creating a separate type for the more constrained set of valid objects.
When we have an object that is invalid because it is in a broke, unusable state, we have to ask three questions:
- Can we un-break it?
- What happens if we just try to use it?
- How were we able to break it in the first place?
The first question mostly depends on the circumstances and the individual implementation of each class. You can for example repair a
std::istream if it’s fail bit is set, but not if it’s bad bit is set. But before you start to design your repair mechanisms, read on about the breaking mechanisms.
“What happens if we try to use it?” also depends on the implementation, but there are actually no answers that I’d consider really satisfying. We could design our class to simply do nothing if it has a bad day. We will use it, it will pretend to work just fine, but it will remain silent and foll us until we debug why we don’t get any results. Not satisfying.
We could just ignore the bad state our object is in and try to do our best. If we really are in a bad state and don’t check it, trying to do our best will result in either partial results without actual errors followed by head scratching and debugging sessions, or it will result in real problems e.g. crashes. Crashes are not satisfying either, I’d say.
The last option is to actually check for bad state and throw an exception. Actually we have a chance to get exceptions, if we just pretend and try, but the outcome is the same – we get the exception after we broke the object. Which is probably the best option but it’s not really satisfying either, especially if we can’t repair the object. Which leads us to the third question:
How could we break the object in the first place? If we are able to tell that the object is broken, we often are able to tell that we are about to break it. So just check early and throw that exception, leaving the object in a sane state.
Better safe than sorry: Avoid the possibility of bad state by checking before state transitions.
An alternative would be making the transition itself revertible: Execute a possibly breaking action, and if it actually broke the object (you have to check either way), revert it and throw the exception.
Yes, there’s that. There are times where performance actually is a thing. Then you don’t want to write code that checks if the input you get will break the object. But you won’t want to write the code that checks if you are in a bad state inside each function, either, so you will pick the “pretend and crash” option I mentioned above.
However, if you have such a need for performance, the callers of your class don’t want to check for
isValid() either, so all that is left is mandating that they won’t pass input that breaks your class. That way you don’t have to care about the cost of bad state checks. You won’t have to care about bad state at all, and
isValid() is not needed.
Don’t allow invalid objects. The possibility of bad state adds to the complexity of your program, so it’s easier to exclude it from the start.
The alert reader will notice that sometimes you create objects that don’t transition to a bad state but are broken from the beginning. I’ll write about constructor failures next time.