Better Software - April 2009 - (Page 15) Code Craft I’ve often seen people override toString() to do something other than return a simple representation of the object, however. This flaw could be annoying but benign. If toString() returned an elaborate HTML or SQL representation of the object, for example, logging that representation would be annoyingly useless, but it wouldn’t break the program. However, a toString() override can be actively evil. For example, toString() could throw an exception that you weren’t expecting and didn’t catch, thereby bringing down the server with what you thought of as a simple logging request. The main practical problem with derivation is something called the “fragile-base-class problem.” It’s possible for someone to modify a base class in a way that looks perfectly safe but nonetheless breaks the derived classes. Since the base-class modifications will pass all the old regression tests, this sort of problem is particularly difficult to find. Moreover, the derived classes might not be available to the person who modified the base class, so they can’t be tested. They may be in another application, or they may be using a library that includes the base class. Java provides a classic example of a fragile base class in its InputStream class. InputStream has three methods for getting input: An abstract read() method reads a single byte of input; two additional methods (such as read(byte[]) ) get an entire array of bytes. InputStream itself doesn’t actually define read()—it’s abstract—but it does define versions of the read-array methods. If you look at the source code, you’ll see that these multiple-byte read methods just call the singlebyte read() multiple times to get their bytes. That fact isn’t documented, but it’s pretty obvious since you can create a fully functional subclass of InputStream simply by overriding the single-byte read method. class NetworkInputStream extends InputStream { public int read() { class InstrumentedInputStream extends NetworkInputStream { private int charactersPerMinute = 0 ; // public int read() { ++charactersPerMinute; // return super.read(); } read(byte[]) is inherited from InputStream } Listing 4 InstrumentedInputStream in; // byte[] buffer = new byte[1024]; read( buffer ); // load 1024 characters into buffer. Listing 5 class NetworkInputStream // version 2 { public void read() { // same as before } public void read( char[] buffer ) { // Do an efficient read from the network // directly into the buffer. DO NOT // use read() to read characters. } } Listing 6 Now, somebody else decides that he needs a version of NetworkInputStream that measures bandwidth with a charac- // Get a byte of input from the network // and return it. } read(byte[]) is inherited from InputStream } Listing 3 Now, imagine that someone extends InputStream to get input from somewhere special, say a NetworkInputStream that gets data from over the network. Further, let’s say that the person who does this work is lazy and decides to leverage the fact that the array version of read() calls the one-byte-at-a-time version of read and thus provides a version of the single-byte method only, as shown in listing 3. So far, so good. Since all the methods inherited from InputStream use the version we just created in NetworkInputStream, things will work just fine. ters-read-per-minute count. Listing 4 shows a simple version. Code like that in listing 5 works just fine because the inherited read(byte[]) calls read(), which we’ve overridden to increment our charactersPerMinute count. A year later, some poor maintenance programmer who doesn’t even know that the InstrumentedInputStream exists looks at NetworkInputStream and says, “Geez! Reading an entire packet one byte at a time is too inefficient. I’ll fix it.” So, he rewrites NetworkInputStream with an explicit override of read(char[]) that doesn’t call read(), as shown in listing 6. Our intrepid maintenance programmer runs all the unit tests (which pass), then pats himself on the back for a job well done. He’s improved the class by making it faster. The only problem is that InstrumentedInputStream doesn’t work anymore because it assumes that read(byte[]) uses read(), which is no longer the case. The characters PerMinute count in InstrumentedInputStream is no longer modified when read(byte[]) is called because the method inherited from NetworkInputStream no longer calls read(). APRIL 2009 BETTER SOFTWARE www.StickyMinds.com 15 http://www.StickyMinds.com
For optimal viewing of this digital publication, please enable JavaScript and then refresh the page. If you would like to try to load the digital publication without using Flash Player detection, please click here.