Inheritance is one of the fundamental facets of object-oriented programming. In this article,
Steve looks at inheritance, and in particular some of the mistakes that many developers make when applying inheritance.
I'm hearing a lot about inheritance these days. With the recent announcements that Visual Basic 7 will support inheritance, I thought this might be a good time to quickly review inheritance, and to focus on some avoidable pitfalls.
According to the Unified Modeling Language (UML) Reference Manual [1], inheritance is defined as a mechanism by which more specific elements incorporate structure and behavior defined by more general elements.
The UML also prescribes the related terms “generalization” and “specialization” to complement “inheritance”. The UML uses “generalization” for all relationships between a more specific element and a more general element. “Specialization”, on the other hand, applies when we produce a more specific description of a model element by adding children. So, a general class is a “parent” class, and a specialized class is a “child” class or subclass. Thus, a specialized class inherits from a general class.
To use inheritance is to take advantage of behavior in existing classes. Inheritance lets you model “Is-A”, “Is-Like” and “Is-Kind-Of” relationships. These are very broad categories, leading to one of the main problems with inheritance: It's so general that it's easy to apply recklessly. For many developers, inheritance is the first thing that comes to mind - “Let's just subclass this class, and ...”
Restrain yourself. You should not make class B
inherit from class A
unless you can somehow make the argument that every instance of B
is also an instance of A
. This possibly goes without saying, but it's helpful to be reminded of it.
Here are a few more pitfalls to consider when considering inheritance.
Pitfalls of Inheritance Modeling
You are thinking of creating or modifying a class hierarchy. You're contemplating the class hierarchy, possibly using a class diagram. Here are some things to keep in mind:
Inheritance is purely static in nature, not dynamic at all. Inter-object dynamics - the actual relationships between object instances - aren't easily specified in class diagrams. In other words, an inheritance hierarchy gives few clues about how objects actually work together. Therefore, if you primarily view an object-oriented problem in terms of class hierarchies, you risk seriously under-assessing the object dynamics.
The dynamic views of a system are at least as important as the static ones. Object dynamics can be modeled with UML sequence, interaction, state, and action diagrams. It takes a fair degree of object-oriented maturity to apply each type of view in more or less correct measure.
Inheritance casts behavior at design-time, and that's inherently less flexible than composition, which can be applied at design-time as well as at run-time. In Design Patterns [2], 11 behavioral patterns are described. Of these, only one pattern (Template Method) is a purely inheritance-based way of controlling behavior. The other ten patterns (Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, and Visitor) all involve composition. More to the point, the behavioral design patterns mostly use specialized objects to flexibly provide elements of behavior.
The best object-oriented systems use a combination of inheritance and composition. Typically, inheritance is used to define interfaces and the most general core behaviors, whereas composition is used to separate interfaces and core behaviors from concrete implementations.
Inheritance hierarchies should be as flat as possible*,* because deep class hierarchies are more difficult to work with and to maintain than flat ones. A rule of thumb suggested by Riel [3] is: inheritance hierarchies should be no deeper than an average person can keep in his or her short-term memory.
If a class hierarchy is deeper than, say, five or six levels, consider it a candidate for refactoring. It's quite possible that the hierarchy is encapsulating behavior that should be handled by two or more classes. It's also possible that the class diversity can be reduced by abstracting some of the behavior into role classes that can be flexibly attached with composition, and reused by a variety of clients. [4]
Factor common behavior and data as high as possible in the hierarchy, so that as more derived classes can take advantage of a common abstraction [3]. Don't be shy about pushing a method or property up the class hierarchy. The cost is negligible, and the benefits of a wider common interface cannot be overstated. Documentation is simplified, and it's much simpler to use the classes when coding.
Separate interfaces from implementations, so that each may vary separately. Separating form (interface) from function (implementation) means that these independent elements can be more freely and flexibly altered, and potentially reused separately. For a complete discussion of this concept, see the Bridge pattern in Design Patterns [2].
The separation of interface from implementation is contrary to what is suggested by most of today's visual development environments. When you double-click on a control, you are presented with a code editor, where you can immediately couple the implementation code within the interface. This is usually a mistake. The only code that belongs here is a call to a method somewhere (anywhere!) else. This leads to our next point.
Events should always call methods in an adjacent layer or, if practical, in an adjacent tier. This reinforces the separation between interfaces and implementations, and makes this distinction physical as well as logical. If we accept the notion that interfaces include the events they generate, then it's a mistake to code event handling (the implementation details) directly in the code snippets provided by user interface controls.
In environments with explicit containership hierarchies (like Visual FoxPro), user interface events should call methods of the container in which they live. These container methods should delegate to their containers, and so on. Ultimately this event bubbling could lead to methods of non-visual objects, where the behavior triggered by the presentation layer events can be invoked programmatically and independently. Thus, the RightClick
event of a particular control should bubble upwards to call RightClick handler methods of the containers in which it is nested.
In Visual Basic, where interface code is centralized in form objects, event code should delegate to functions in modules, or to methods of objects that reside outside the form. This is one of the easiest things you can do in VB to make your code more reusable.
Pitfalls of Adaptive Inheritance
Adaptive inheritance involves using a subclass to change the preexisting properties and methods of a parent class. We don't change the type, or interface, of the class. We're simply using a subclass to make changes to the parent class implementation.
So, say you're working with the subclass of a class, and you don't intend to change the class interface by adding properties or methods. You're just coding within the existing interface. Here are some things to keep in mind:
Don't forget the call-up in the methods you code, or you won't inherit the behavior of the general class. If your design suppresses the call-up statement, you are completely overriding the general behavior, which is a sign that your derived class probably fails the “is-a” test.
After all, overriding the behavior of a general class eliminates part of its abstraction from the derived class. If you find yourself completely overriding general behavior, consider that you may have a poorly designed inheritance hierarchy, or one that's inverted.
Avoid changing method prototypes, since changing the number or sequence of parameters is a change to the class interface. Once you change the interface of a particular method, you lose the ability to use classes polymorphically. More to the point, changing method prototypes is a needless source of confusion for future developers and maintainers, who must remember different parameter requirements for different classes in the same hierarchy.
Pitfalls of Extensibility Inheritance
Extensibility inheritance involves adding new properties and methods to support new functionality not found in the parent class. In this case, you are extending the interface of the parent class.
So, assume now that you're working with a subclass of some other class, and you are adding new properties or methods to the interface. Here are some things to keep in mind:
Consider the new members' visibility, and strive to not make everything you add PUBLIC. Consider that the class should, as much as possible, encapsulate and hide the data and services it provides.
Member visibility is especially important when creating COM components for use in environments that provide intellisense technology. As you code in an Intellisense environment, the members of the class pop up, sometimes irritatingly, in a dropdown list. Making only the bare essential members public, makes for tidy Intellisense dropdown lists.
Keep related things together, because organizing work and making later changes is always easier when things are centralized. This means that you should try to place the properties with the methods that need them, within the same class.
If you do not keep data and code together, you may end up with increased object coupling and maintenance complexity. Also, consider the increased messaging load necessary to provide the method with the required data.
Beware of overboard accessor methods, which are also known as SET_()
and GET_()
methods, because their overuse implies that related things are not being kept together (see the previous paragraph). If you have many accessor methods, ask yourself, “Why do related objects need this data, and why isn't this class providing those services?”
Remember the Operand Principle [5], which says that the arguments of a method should include only operands, not options. An operand argument to a routine represents an object upon which the routine will operate. An option argument represents a mode of operation. An argument is an option if, assuming the client has not supplied its value, it can be provided with a reasonable default.
Thus, avoid this cluttered interface:
X.Print( v, w, x, y, z)
...and prefer this interface:
X.Printer= [\\Svr\LPT2]
X.Copies= 2
X.bin= 3
X.Print("SomeDoc.Doc")
In the example above, options that can have reasonable default values, are not parameters. This looks like more code, but it's a lot less code in simple cases, and it's more literate programming in complex cases.
However, it's worth noting that, when working with COM components, setting object properties like this causes a trip to the server for each executed statement. Thus, the code above is clearly not optimal in COM. In this case, it's best to have an interface whereby you can set multiple properties in one statement. While the long parameter list is one way to do this, consider an alternative that accepts a single parameter (such as an XML package), containing the desired state of the server and the method to execute.
Conclusion
There are many ways to misuse inheritance, and this article covers but a few of them. I've found that the works of Gamma [2], Riel [3] and Fowler [4] are indispensable guides about good static design.
Object-oriented inheritance mechanisms are easy to grasp and, on the surface, easy to apply. A classic rookie mistake is to try to use inheritance for all sorts of “Is-A”, “Is-Like” and “Is-Kind-Of” relationships.
When modeling, consider that inheritance is a fundamentally static and inflexible way to structure your applications. Tinkering with inheritance hierarchies has little to do with object dynamics, which is at least as important as class hierarchy to most good object-oriented systems.
When adapting methods within an inheritance hierarchy, don't forget to properly invoke the general behavior defined in the general classes. Don't recklessly change method prototypes, and strive to always separate interfaces from their implementations.
When extending classes within an existing hierarchy, quash the visibility of new members as much as possible, try to keep related things together, and strive for clean interfaces for new methods.
Steven Black
Bibliography
[1] Rumbaugh, James, Jacobson, Ivar, Booch, Grady (1999), The Unified Modeling Language Reference Manual, Addison Wesley, Reading, MA, ISBN 0-201-30998-X.
[2] Gamma, E., Helm, R., Johnson, R, and Vlissides, J. (1994), Design Patterns, Elements of Object Oriented Software, Addison Wesley, Reading, MA, ISBN 0-201-63361-2.
[3] Riel, A (1996), Object Oriented Design Heuristics, Addison Wesley, Reading, MA, ISBN 0-201-63385-X.
[4] Fowler, Martin (1999), Refactoring, Addison Wesley, Reading, MA, ISBN 0-201-48567-2.
[5] Meyer, Bertrand (1997), Object-Oriented Software Construction, Prentice Hall, Englewood Cliffs, NJ, ISBN 0-13-629155-4.