eyt*
Sep 02, 2004

Abusing Inheritance...

Object Oriented Software Development is a completely different way of looking at software development, and developers that are coming from other paradigms generally are introduced to many new concepts, which takes time to understand the difference between them. Many such developers will generally start by creating applications by a monolithic class, a few highly coupled classes, or what I call “Object-Oriented Spaghetti,” which has a single class's functionality distributed over multiple classes (As an editor in an issue of Dr. Dobbs once said, “You can write Fortran in any language”). In all of these cases, people are not realizing the potential of object oriented design. This is problem is also quite popular when RAD tools are used and the MVC Pattern is not used.

One example of a beginners mistakes is the misuse of inheritance, such as the following example:

ValidatedList derived from Vector, but not overriding all entry points

The ValidatedList class is intended to perform some data integrity or validation when entries are added and removed from the list. In the above diagram, the ValidatedList class is derived from Vector, however, the Vector class has several ways of adding and removing elements, whereas the derived class only defines two methods. In other words, the ValidatedList class only protects two methods, but all four are available to the user of the class.

A naive solution would be to simply reimplement the class by providing the four signatures, but what if the Vector interface changes? The same problem could occur. The above example was actually taken from C++, where the Vector class is really the std::vector template class, which is even worst, since the methods are not marked virtual, indicating that the method can be overridden (in Java, all methods can be overridden, unless the static or final keywords are used in the method signature).

But in reality, let us think about what we are doing here. The key is to think about inheritance as an IS-A relationship, implying that the subclass is a type of the parent class and that the subclass specializes the parent class in some way. In other words, the client of a class can be changed to a subclass without any code modifications, as shown below, as the interface (and the contract signified by that interface) is what the client uses.

A client uses a Parent class directly, not the Subclass.

In this particular example, it may seem to some that the relationship is warranted, but if you look a little closer, the validation services that the class does are not specific to the Vector container, and could work equally well with any container. Said in such a way, this should be implemented via containment, as demonstrated below. In this case, the container can be changed in future releases, completely protecting the Client of the class from interface changes.

ValidatedList is implemented in terms of a Vector

This example was rather simple, but it is frequently an issue. Another related issue is with multiple inheritance. While it is generally considered poor design to use multiple inheritance, they can be of great value, such as in the Adapter PatternGOF. With this in mind, let us consider the following:

LockedList which derives from Locked and List

Quickly looking at this, some may identify this as the Adapter Pattern, however, the Adapter pattern is meant to adapter a class's interface to another's interface, which the two are usually related. A List and a Lock are not at all related.

Generally people who design these types of classes are thinking that a LockedList is a List that is locked, but this is not what the above communicates. The real meaning of the class must then be that it is a thread-safe list. A thread-safe list, however, is a List, not a Lock.

If we look further into this example, note that the class LockedList does not override any methods from either class; in other words, in order for the class to guarantee thread-safety, the user of the LockedList class is forced to guarantee this property. While this design decision may allow your users to lock the list for multiple operations, this forces generic algorithms to be surrounded by lock() and unlock() code that probably is of a larger scope than required and users must remember to always call these methods; but even this is a small problem in the large view of things. On a larger scope, if the program deadlocks, the problem may appear to be a LockedList problem, and debugging this code will be difficult at best, especially since unit tests will not be able to reproduce a client's problems.

One solution to this problem is to override all the methods of the List interface, and use containment instead to access these members, as demonstrated here:

LockedList derived from List and contains Lock, with Lock heirarchy.

The advantage of breaking the inheritance becomes a little more clear in this diagram, because we can now separate out the Lock class into subclasses. The classes presented as based on operating system implementations, however, this could also include a distributed lock manager, a file-based locking scheme, and many other locking schemes.

At first, this may seem to solve all the problems. A user of the List class can seamlessly update their code to use the LockedList class and now their class is thread-safe. Or is it?

Like the example of the ValidatedList above, this class has a problem in which methods can be added to the List class that could be overlooked in the LockedList class. Such a problem can be dormant in a system for some time before discovered. While the discovery time could be shorten with proper unit testing, many times this will cause intermittent bugs, exposed by unrelated changes, or only exposed when porting to a new platform or compiler. How could we guarantee thread-safety?

There are several ways that one can approach this. One approach is to add some protected lock() and unlock() methods to the List class that by default do nothing, and can be overridden to provide the locking mechanism. This implementation forces the List to be thread-aware without incurring the hit of locking. For this implementation, the LockedList would only override these methods and forward their to its lock attribute, such as follows:

List with lock()/unlock() methods, with a derived LockedList class.

Thinking on these lines, you could also move the Lock reference up-to the parent class, create a NullLock class, and remove the LockedList completely, as demonstrated here:

List with Lock classes.

While this is a workable class structure, there is still a minimal performance impact for classes that do not use locks. More flexibility could be obtained by refactoring the above class design into following:

Abstract List, with the List and LockedList subclasses, the latter using both an AbstractList and a lock.

In this design, the LockedList becomes a proper Adapter class. Each method, therefore, would be implemented by locking the class, calling the adapted list's method, and unlocking the class (A C++-based implementation would need to use the Scoped Locking PatternPOSA2). This is similar to the approach used in Java's java.util.List, although the iterators are inconsistent with this abstraction.

The decision for either approach would really depends on your exact requirements. The former design would be optimal in cases where you rarely need to specialize the List class, where as the latter case allows for greater flexibility for specialization of lists.

Using inheritance is great, however, it must be used only in contexts that truly illustrates the IS-A relationship. To illustrate the difference, Peter Coad and Mark Mayfield recommend in Java Design that inheritance should be used when the subclass is a special kind of the parent class, and not a role played by the class, that this relationship always exists for the lifetime of the subclass (for example, the subclass cannot be transformed into another type), and that the subclass extends the behaviour of the parent class, instead of simply using or nullifying the parent class.

Containment, on the other hand, allows a class's implementation to change. In the above example of the LockedList deriving from the Lock class, changing the Lock class could be difficult, as some users of the class may prefer one type of lock over another; this flexibility could not be realized directly. On the other hand, by delegating the lock behaviour to a member, the extensibility of the class was more flexible and adaptable to the changing needs of its users.

This is not to say that inheritance is bad. The point of this is to only use inheritance when inheritance is warranted, and to carefully choose when you derive classes; changing this later may not be an easy task, but with containment, it is very easy.

Filed In

Navigation

eyt*