TrainingConferencesAbout UsContact UsAdvertiseSQE.comRSS Feed

StickyMinds.com: brain food for building better software

Log In
 Clarify Your Search Criteria

Tips on Using Our Search Feature(s)
 
StickyMinds.com Home
ResourcesTopicsCommunityPowerPassBlogs
Home  >  Topics  >  Test & Evaluation  >  Detail: The Trouble with Derivation



The Trouble with Derivation

By Allen I. Holub

Send This Content to a FriendGet a Short Link to This ContentPrint This Content

Summary: This article discusses the dark underbelly of derivation: the fragile base class. It's possible to modify a base class in such a way that, even though you've improved its implementation and all your tests work just fine, you've nonetheless damaged the derived classes, perhaps fatally.

Jama Software
The notion of polymorphism, or method overriding, is fundamental to object-oriented thinking. That said, the practice of derivation, or subclassing, is vastly overused in most program designs. Subclassing can actually introduce more problems than it solves. I'm talking specifically about implementation inheritance—the "extends" relationships in Java. There's almost no downside to interface inheritance—"implements" in Java—where you override an abstract method in a base class. In fact, you could look at the Gang of Four design patterns [1] as ways to replace implementation inheritance with interface inheritance. Once you exclude the patterns that are hardly ever used, like Interpreter, only one pattern, Class Adapter, requires an "extends." The rest all use interfaces. This meta-pattern isn't an accident.

From a theory point of view, the problem with derivation is coupling. The derived classes are very tightly coupled to their base classes, and a change in either could affect the other.

That's bad, of course. A class should be isolated from the rest of the program so that any internal changes to the class won't impact anybody that uses objects of that class. This same reasoning applies to base-class/derived-class relationship.

There are a lot of places where that too-tight coupling relationship causes problems. Consider the Java example of the Template Method design pattern shown in listing 1.



I've opened the door for the subclass method to wreak real havoc here. First, since I was foolish enough to define someValue as protected, the derived class can (and does) modify it. I'll be really surprised, for example, when someValue has magically changed its value after the prepare() call. Moreover, I now can't touch someValue as I maintain the code or the derived-class code will break. A minor change to the superclass could potentially impact every subclass. Fields should always be private, unless they're actual constants.

Even worse, the prepare() call—or any method that’s structured like prepare()—can do pretty much anything it wants, even things that are dangerous. What if prepare() throws an unexpected exception, for example?

I see this problem constantly. Consider Java's lowly toString() method. In Java, the Object class, which is every other class's superclass, defines a toString() that's supposed to return a String representation of the object. The toString() method lets you write code like that in listing 2 to help with your debugging.



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 single-byte 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.

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.

Now, somebody else decides that he needs a version of NetworkInputStream that measures bandwidth with a characters-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().

Note that the code still runs—it just doesn't work any more. No recompile is required, so the compiler won't help you find the bug. More to the point, it's often not possible to find every derived class and test it every time the base class is modified.

This is a fragile base class. You should be very scared.

So, what's the lesson here? The main lesson—since it's almost impossible to second-guess every situation where a fragile base class can hurt you—is to avoid implementation inheritance ("extends" relationships) altogether. Avoid derivation-based frameworks that require you to extend a base class and override base-class methods to make the framework useful. The real solution to this problem, however, is to use interfaces rather than implementation inheritance—and that will be the subject of my next column. {end}

References
[1] Gamma, Erich, Richard Helm, Ralph Johnson, and John M. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional Computing Series, 1994.
[2] Allen Holub, Holub on Patterns. Apress, 2004.


Do you have any examples of fragile base classes in your own work?

Join the conversation below or start a new one in the Member Comments section.


About the Author
Allen I. Holub has worked in the computer industry since 1979. Focusing on object-oriented technology and agile process, he excels at helping companies fix broken software development processes and adopt new ones by providing CEO/CTO coaching, staff training, project management, and programming services. Allen has authored nine books, most recently Holub on Patterns, and more than 150 magazine articles, writing as contributing editor for Dr. Dobb's Journal, JavaWorld, and SD Times among others. Allen has worked on projects ranging from operating systems and compilers to Ajax Web applications, and he teaches for the University of California, Berkeley, Extension. Allen is the security-track chair for the Software Development conference and sits on the board of advisors of Ascenium Corp. and Ontometrics Corp. Contact Allen at www.holub.com/allen.html.

Back to Top
 
 
Member Comments
Add Your Comment

May We Suggest...
Show All

Articles & Papers

Templates

Links

Books

Tools

Related Products
Testing Training Courses
Software Testing Certification, Systematic Software Testing, Test Management, Mastering Test Design, Just-in-time Testing

Software Engineering Training
Mastering the Requirements Process, Requirements Modeling, Introduction to the Capability Maturity Model Integration, Business-Driven Software Measurement

Agile Software Development Training
Scrum Master Implementation Workshop, User Stories and Estimation in Agile Development, Design Patterns Explained, Practical Test-Driven Development

 
Ads By Google
What's This?
 
 



Home   |   Resources   |   Topics   |   Community   |   PowerPass



© 2010 StickyMinds.com. All rights reserved.
StickyMinds.com is a division of Software Quality Engineering.
Privacy Policy    Terms & Conditions    Link to StickyMinds.com    Feedback


Rally Software

HP Software


STAREAST 2010 


Better Software Conference