Avoid Invalid State

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:

  1. 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.
  2. 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 isPostalAddress(), isEmailAddress(), 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.

Broken objects

When we have an object that is invalid because it is in a broke, unusable state, we have to ask three questions:

  1. Can we un-break it?
  2. What happens if we just try to use it?
  3. 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.

“But performance…!”

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.

Conclusion

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.

Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

2 Comments

  1. Thomas

    Sometimes we need a default constructed object. It only need to be assignable / destructable. That may be a good reason for an invalid object Sometime setting up an object is tricky and requires work beyond construction. Such work is often more suitable to some form of initialization function. We can change valid to initialized, but it’s often the same thing.

    Reply
    1. Arne Mertz

      Of course there are always exceptions to any rule. However, if we need a default constructed object, we should make sure that the default constructed state is a valid state, for the reasons mentioned in this and the last post.
      Since the need for a default constructible object often stems from the usage of some other dependency, we should also evaluate if we really need to do it the way we are doing it or if we have alternatives. Among the alternatives could be the use of (smart)pointers or boost::optional (std::optional from C++17)
      The “setting up an object is tricky” needs not lead us to invalid objects. There are two approaches to deal with complex initialization: The first is to construct helper objects that get passed to the object’s constructor. Often these are simple aggregates (structs) that carry whatever a data-intensive construction needs. The other approach is the builder pattern: have a function or class that does the complex assembly and initialization of the object in question and then releases it into the wild. That way you can technically have an invalid object, but only temporarily inside the builder which takes care that nobody can access the incomplete object.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *