CSC/ECE 517 Summer 2008/wiki2 8 jb
This wiki will follow the debate over inheritance vs. delegation, with the goal of showing the strengths and weaknesses of each approach, and where each approach is preferred.
Background
A brief description of Inheritance and Delegation will help get us started:
Inheritance
Inheritance is one of the fundamental tenets of object oriented programming. Inheritance refers to the ability to model hierarchies of classes that are related to each other through the is-a relationship. It is commonly agreed upon that inheritance done correctly must conform to the Liskov substitution principle. Inheritance is closely related to the topics of dynamic binding and polymorphism. Dynamic binding refers to a language's support for virtual methods, allowing the determination of the actual method invoked on a particular object to be determined at runtime. Polymorphism refers to the ability to store a reference to a derived class within a variable declared as a base class type.
Delegation
Delegation, sometimes referred to as aggregation, is the concept that one class may contain an instance of another class, and delegate some responsibility to that class. This is also referred to as the has-a relationship. Aggregation is closely related to composition. Both aggregation and composition are used to describe one object containing another object, but composition implies ownership [1]. Aggregation is more general and doesn't imply any responsibilities for memory management. A class which contains other classes is called a composite class, while a class being contained is called a composited or composed class [2].
Design Pattern: Replace Inheritance with Delegation
Before we discuss the main points of the inheritance vs. delegation debate, it is worthwhile discussing a design pattern that describes how to replace delegation with inheritance. The idea behind the pattern is extremely simple: instead of deriving a new class from an existing class, make your new class contain an instance of the existing class. An example of the pattern being applied is shown below in the graphic. In this case a Stack was implemented as being derived from a Vector. You could imagine that the stack would add the push and pop methods, but use the vector for the actual storage. Instead of deriving the Stack from Vector, the pattern dictates that the Stack should contain an instance of a Vector.
It is useful to discuss the pattern prior to discussing the debate so that you can keep the example of the Stack in mind while thinking about the main points of the debate. The Stack example is simplistic, but nicely shows the equivalence of the two approaches. The major points in the debate assume this equivalence in order to argue their case. After we go over the debate, we will look at some cases where the two approaches are not equivalent in order to show where each approach is preferred.
The Debate
Much of the debate is focussed on arguments against using inheritance. The most notable and widely accepted statement on the subject came from the original GoF, or Gang of Four, in their book titled Design Patterns: Elements of Reusable Object-Oriented Software. The book stated to "Favor object composition over class inheritance" as a design principle. This position is backed up by many strong arguments.
Arguments against Inheritance
Inheritance breaks encapsulation
- Critics of inheritance argue that the public and protected access modifiers, when used for attributes, break encapsulation by letting a derived class peer inside of its base class [3].
- The point about breaking encapsulation also comes up when talking specifically about implementation inheritance. Implementation inheritance refers to the practice of inheriting from an actual class as opposed to an interface. The idea behind implementation inheritance is to re-use code from the base class and add or customize some desired behavior. The argument against implementation inheritance is that it breaks encapsulation by making the functionality of the subclass dependent on the implementation of the base class [4].
- Interface inheritance, on the other hand, is when a class either derived from a pure virtual class in c++, or implements an interface in Java. Interface inheritance does not break encapsulation, and is the idea behind the GoF's design principle of programming to an interface.
Inheritance increases coupling
- Critics of inheritance argue that inheritance can lead to brittle code due to coupling between base and derived classes [5]. When using delegation there is a single, explicit dependency between the composite and composed classes; the interface of the composed class. With inheritance the dependencies become blurred with multiple factors leading to close coupling. Access to base class members, unanticipated consequences of executing virtual methods, etc... all contribute to a more brittle relationship and runtime behavior that can be difficult to predict.
- The coupling introduced by inheritance introduces compile time dependencies that can lead to increased build times [6]. In C++, the header file containing the base class declaration must be pulled in when compiling a derived class header. There is the obvious penalty of reading the file from disk, but the bigger problem is that the changes high up in a class hierarchy can cause a large number of objects to be rebuilt. This can be contrasted to delegation, where a composited class can be declared with a forward declaration to avoid the compile time penalty.
- Another aspect of coupling is described by the yo-yo_problem. The yo-yo problem describes the continuous bouncing up and down through an object hierarchy in order to understand the features and behavior of an individual class.
Inheritance is static and determined at compile time
- In statically typed languages such as c++ and java, the class hierarchy is determined at compile time. Delegation has the advantage in that the composite object can swap out the composed object at runtime. Ruby, a dynamically typed language, avoids this compile time restriction and allows changing a class' inheritance at runtime using run-time mixins.
Multiple inheritance and the diamond problem
- Languages that support multiple inheritance, such as C++, are exposed to the diamond problem. The diamond problem arises when a class derives from two base classes that each have a common ancestor. This leads to several problems, including ambiguous virtual method resolution, and potential duplication of data.
- Virtual method resolution is problematic since a class experiencing the diamond problem can potentially have multiple virtual method implementations available. This forces the language runtime designer to define a heuristic for picking one, which is essentially an arbitrary determination. Data duplication is also an issue, since a class experiencing the diamond problem will end up with multiple copies of the data fields at the root of the hierarchy.
- Language designers have introduced the concept of virtual inheritance to help programmers avoid the data duplication aspects of the diamond problem. Virtual inheritance works by adding a vftable pointer to each class that inherits virtually from another. This allows the determination of the base class' location to be determined at runtime, but comes with a penalty. Programmers employing virtual inheritance on extremely large object hierarchies have run into memory problems due to carrying these extra vftables [7].
Arguments against delegation
Delegation leads to bloated Code
- The main point used against delegation is that it increases code size[8]. The argument is that delegation doesn't allow any of the code re-use that inheritance does. Inheritance allows the programmer to only define additional, or specialized functionality within derived classes. Using delegation as a direct replacement for inheritance requires more code, since in order to program to the interface, the entire interface must be supported by the composed object.
- These arguments fail to consider that in some cases coding with inheritance can lead to bloated code too. Consider the stack example that Skrien talks about on page 76. If you wanted to implement MyStack in such a way that you excluded all of the functionality that you didn't want to carry, the only way to do "void out" these methods would be to implement them and either do nothing or throw an exception [Skrien 76].
Delegation incurs performance overhead
- The process of delegation involves an extra method invocation when compared to inheritance. The performance penalty is most likely negligible. [Skrien, pg 77]. Skrien doesn't mention the vftable overhead involved with inheritance, so one could make an argument that Skrien should have called this one a draw.
Design Considerations
When faced with the choice of using inheritance or delegation, one of the first things the programmer may do is try to define whether the case represents the is-a or has-a relationship. The is-a relationship maps to inheritance and the has-a relationship maps to delegation. But as we have seen, there isn't always an obvious way to determine which is the correct approach to take [9].
We will conclude by offering some advice to help you choose between inheritance or delegation.
Polymorphism
Only use inheritance if you're going to make use of polymorphism. "Inheritance is not for code reuse". There are many cases where it makes perfect sense to incorporate inheritance and polymorphism into your design. The example below from Skrien, and also included the notes from lecture 17, illustrates this well. In this program the main draw loop is able to only deal with drawing instances of the Polygon class. Each class derived from Polygon overrides the virtual Draw method to implement the custom draw behavior.
Roles
If you find yourself designing an object hierarchy around roles, choose delegation. It is tempting to think of roles using the is-a relationship. In the left-most UML diagram below, you can see that the Student and Employee roles and deriving from a common Person class. The problem with this is that using this hierarchy an individual cannot support both roles. Changing roles in the future would also not be allowed. Delegation solves this problem by letting the Role classes aggregate the Person class. In the right-most UML diagram below you can see that both the Student and Employee classes can aggregate the Person class. Using this scheme, both roles can reference the same instance of a Person, and roles can be added or deleted at runtime, allowing a Person to change roles within the system.
Resources
Object Oriented Design Using Java, Skrien - link to Skrien book on Amazon
Dr. Dobbs: Composition vs. Inheritance - good article on composition vs. inheritance
Design pattern for replacing inheritance with delegation - high level overview of the pattern for replacing inheritance with delegation
Game Object Structure: Inheritance vs. Aggregation - excellent article on inheritance vs. aggregation, tons of info!
Classes, Structs, and Objects—Delegation and Composition vs. Inheritance - great article with a few very good code samples
Composition vs. Inheritance, A Comparative Look at Two Fundamental Ways to Relate Classes - basic, but concise description of the differences between the two approaches
Section on Composition vs. Inheritance from Design Principles article, with input from Erich Gamma, one the Gang of Four - good interview with lots of background on the subject
Composition vs. Inheritance in Java - another great article with a few really good code samples
Wikipedia: Inheritance
Wikipedia: Delegation
Wikipedia: Composition