Inheritance is a unique aspect of object orientation. It’s seen as both a defining condition and something to be avoided whenever possible (according to James Gosling, inventor of Java). The Gang of Four, in their seminal work “Design Patterns: Elements of Reusable Object-Oriented Software”, referred to inheritance as a threat to encapsulation and stated that object composition should be preferred to it.
There are many reasons for the conflicted relationship. While inheritance can provide a powerful and simple mechanism for code reuse and extension, it has spawned a collection of anti-patterns and issues. From the yo-yo problem (bouncing up and down through an inheritance hierarchy to trace behavior) to the fragile base class problem (where apparently trivial changes to the base class break derived classes), there are many reasons to avoid using inheritance. Both the .Net framework and Java even provide the ability to prevent inheritance (via the sealed keyword in C#, NotInheritable in VB.Net, and Final in Java).
In spite of the many potential pitfalls with inheritance, it remains a powerful and useful feature. The keys to using inheritance successfully are planning and discipline on the part of the designer of the base class and responsibility on the part of the consumer.
In designing a class for reuse, thought must be given to how a derived class will use (and possibly abuse) the base class and its members. This will help determine which members should be virtual, abstract (pure virtual), and sealed. Maintaining as much encapsulation as possible (by limiting the use of protected members) is likewise critical to providing robust base classes. Documentation will allow the consumer to make better assumptions regarding the way their derivations work. Lastly, the more widely used the base class will be, the more important adherence to the open/closed principle becomes.
The consumer of a base class also plays a crucial part in ensuring that it is used successfully. Consider an example of the fragile base class problem:
Suppose that a class Bag is provided by some object-oriented system, for example, an extensible container framework. In an extensible framework user extensions can be called by both the user application and the framework. The class Bag has an instance variable b : bag of char, which is initialized with an empty bag. It also has methods add inserting a new element into b, addAll invoking the add method to add a group of elements to the bag simultaneously, and cardinality returning the number of elements in b.
Suppose now that a user of the framework decides to extend it. To do so, the user derives a class CountingBag, which introduces an instance variable n, and overrides add to increment n every time a new element is added to the bag. The user also overrides the cardinality method to return the value of n which should be equal to the number of elements in the bag. Note that the user is obliged to verify that CountingBag is substitutable for Bag to be safely used by the framework.
After some time a system developer decides to improve the efficiency of the class Bag and releases a new version of the system. An “improved” Bag1 implements addAll without invoking add. Naturally, the system developer claims that the new version of the system is fully compatible with the previous one. It definitely appears to be so if considered in separation of the extensions. However, when trying to use Bag1 instead of Bag as the base class for CountingBag, the framework extender suddenly discovers that the resulting class returns the incorrect number of elements in the bag.
(From “A Study of The Fragile Base Class Problem”, Leonid Mikhajlov1 and Emil Sekerinski, November 1998)
Unless there was a documented contract that addAll would always use add, the break detailed above was due to the extender making an assumption that the internals of Bag would never change. Likewise, the Liskov Substitution Principle requires that subclasses represent an “Is A” relationship (any code expecting an instance of the superclass could receive an instance of the subclass without breaking). Extending the base class by adding new members to the subclass is acceptable. Attempting to restrict it, such as by overriding a method and throwing a NotImplemented exception, violates the Liskov Substitution Principle. This “Is Kinda Sorta A” relationship is almost guaranteed to lead to problems.
It’s common sense that good design does not happen by accident. Accordingly, inheritance should be by design rather than by default. Liberal use of the sealed keyword can certainly help head off problems with unintended inheritance.
I will note that this isn’t a universal opinion. Brian Button, of MS Patterns and Practices fame, has been reported to dislike the sealed classes in the .Net Framework:
Brian shares my frustration about sealed classes in the .Net Framework. He has encountered parts of the framework that are sealed, and when he needs to extend them, he can’t. Sealed classes are hard to write testable code against. He made a good point that I hadn’t thought of before: If you seal a class, you are saying “I can predict the future, and this is all that this class will ever need to do.”
In defense of my opinion, however, I will note this: a sealed class can be unsealed at some point in the future without breaking existing code. Once unsealed, the genie cannot be put back in the bottle without breaking derived classes. In this respect, an unsealed class is much more a prediction of the future than the sealed one.