Thursday, February 14, 2008

Object Oriented Design Note: Constructors and Exceptions

Background. I've done a fair bit of Object Oriented programming professionally -- around three years. I've read about OO languages, and I've studied Design Patterns, and I've studied The Pragmatic Programmer and other books. But I still have a lot of questions about OO design. I know how to build classes, interfaces, methods, exceptions, etc. I'm just not sure I know when and how to use them all. Should the method be in this class, or in that class? Should this method just "throws" the exception it gets internally, or it should it re-throw it as a new type of exception? I only find hints here and there at the answers.

This note is about my hope to find those answers. Here's one topic that my good friend Chris Vaughan and I discussed today.

Should a method throw an Exception? 

Proposal: Only a class constructor, or static methods, should throw exceptions.

Exceptions are a nice idea, because they force you to handle error conditions. With exceptions, it's hard to just call a method and ignore errors that occur. With traditional C-style programming, you could call a function like socket() and just ignore the return response if there's an error.

But once you've successfully instantiated an object, should its methods be able to return errors? For example, let's say you have a Report class, and it has method toHtml() that provides an HTML-formatted version of the report. Should toHtml() be able to throw exceptions?  This proposal says that no, it shouldn't. If there would be such exceptions, the constructor itself should fail.

This is analogous to a rule of database design: every row should be meaningful. The implication is that we refuse to allow a row to exist that isn't meaningful. We do this with constraints; for example, if it makes no sense for a row in the "students" table to lack a "student ID", then we make the "student ID" field mandatory.

If we say that every method should be callable without the risk of exceptions, then we're saying that our constructor guarantees us that we have a meaningful -- and functional -- object. As Chris said, this forces some structure on us. In our example, this rule would require that we guarantee in the constructor that toHtml() will work when we call it because we're not allowed to let it throw an Exception.

A weakness here is that this may require the constructor to basically pre-calculate the html rendering -- or else otherwise guarantee it'll work. If the underlying report is encoded as XML somewhere, then this might mean parsing that XML to confirm it's valid before returning the object. This removes some of our options for lazy execution; we might have preferred to wait on that XML parsing until it's actually needed.

A serious weakness is this rule's ability to deal with nondeterminism. For example, let's say we have a Connection object that represents some network connection and functions as a two-way communication channel, and we have a read() method that reads in data received from that channel. What if the channel fails? Perhaps the network has stopped working. Or perhaps the other side of the connection decides to forcefully terminate the connection. In this case, we might like the read() method to throw an exception.

But should the read() method throw an exception? Perhaps no;  maybe read() should simply return null -- i.e., nothing was read. In that case, if you want to know about the status of the Connection object, you'll have to ask somebody else. Or perhaps the Connection class isn't a good abstraction anyway; perhaps it's just an accident of history and the way we all think of the BSD Sockets implementation. Perhaps each time we want to read something, we're really instantiating a new object -- i.e., whatever was read in. Perhaps that constructor should fail if nothing can be read in.

Thanks to Chris Vaughan for this idea.

No comments: