leave a note____
_newest entry___
__entry index___
___diaryland___

2012-10-23 - 7:38 a.m.

Inheritance versus composition
23rd October, 2012

Ranty intro

the first page of Google results for ��inheritance versus composition�� leads to an apparently popular conclusion that composition is preferable. In all cases, the impression given by the authors of these conclusions is that they believe their own private example adds weight to their argument. Their argument however is the equivalent of saying ��the compiler has a bug�� when in fact they need to be more modest and associate the problem with themselves instead of the notion of inheritance.

There are also mentions of the ��diamond problem��. I propose that in answer to any example of the ��diamond problem��, the design error that is the root cause may be identified, vindicating the mechanism of multiple inheritance.

Proof

When object oriented design is discussed, it is essential to consider the responsibilities of the various objects. Correctly identifying the responsibilities of the resulting system in operation, and the accurate classification of these into categories is critical to the development of the correct hierarchy of classes.

A common mistake made by people bemoaning the error-prone nature of inheritance-based designs is that of putting implementation of responsibility into derived classes. By definition, a base class exists because of, and takes on, the responsibilities common to the collection of subclasses it supports. That is to say, the implementation of the responsibility must be wholly contained within it and no part of this responsibility must fall on a derived class. This seems counter-intuitive, but at all times in these discussions it must be remembered why the object-oriented design philosophy exists at all�: as a tool to be used when approaching solutions of truly complex systems.

We did just fine getting code to work with machine language. Well, maybe assembler. The C language did little more that simplify the use of library code which in the assembler model is a lot of work. C consists therefore in a clarification in terms of legibility and a simplification in terms of function invocation compared to assembler. Assembler was already infinitely powerful in terms of what it could be used to produce. As any computer today can be proved to be the equivalent of a universal Turing machine, so any program written in any program could always have been written in assembly language.

So, the existance of C builds on an already infinitely capable base, simply making it easier and less error-prone to develop software. The reason we went further was that even with the power of C, as a project becomes increasingly complex, the effort needed to add new functionality usually increases exponentially. The main reason, if not the only one, is the coupling inherent in imperative, side-effect-heavy programming in a language which encourages direct access to all program memory from any point in code.

Programmers learning C and coming from a C background (including assembler) are used to thinking in terms of ��ok, I'm here, I have these variables available, and I want this result. What step do I need to do to make this result happen.��. the ��I'm here�� part is critical. Imperative programmers are conditioned to look at a line of code or a block of code as their current point of focus. The fact that C permits basically any modification of state at any point in code leads to coupling between disparate code locations which incur prohibitive penalities on code modification.
The improvement proposed by object-oriented design is that of encapsulation. The notion that the behaviour and state associated with the solution of a part of the problem posed by a system's specification may be kept separate from the behaviours and states of some of the other parts lets us � in theory � freely modify the code of one part of the solution without affecting the other parts. The use of the name object and the division of code into classes is secondary. The important work is done by the rules of the compiler, which forces the programmer to comply with restrictions in order to avoid coupling whenever possible. Thus, the programmer changing a line of code in one module may in some cases be unable to break the behaviour of another module, if the rules suggested by the object-oriented philosophy are upheld.

C++ is not a perfect language. It should however be possible to use it to achieve perfect code. With proper use of the notion of private state, variable and function constness, and references, proper encapsulation is possible.

Furthermore, inheritance is proposed as part of the overall object-oriented philosophy. However, at this stage the compiler loses its control over the programmer and it is impossible by any permutation of base class design to prevent a programmer from breaking base-class functionality in a derived class.

Which leads back to the opening rant. When a modification to code breaks the functionality of the system, or worse, a point is reached where no possible way exists to move the implementation forward, we have two choices, blame the artist or blame the tools. Without proposing a true ��proof��, here are some concrete examples supporting the view that the tools at least allow a proper system to be designed, admittedly only when the programmer uses them correctly.

Implementation

As stated earlier, a central tenet of the object-oriented design method is the identification of the responsibilities of the end result system. Once identified, responsibilities may be assigned to objects in an infinite number of ways. There is no direct method available to find a set of objects that once implemented will satisfy all the responsibilities, nor is there necessarily a perfect class hierarchy to find. There is an art to deciding into which categories the responsibilities must be divided. We must agree, however, that once these decisions are made, we have bound ourselves to a particular implementation. Any critical failure of the implementation will imply returning to the original decision process of identifying responsibilities and categorising them.

Once decided upon, the implementation path should be straightforward to code. Many programmers have difficulty at this step without ever considering the need to re-evaluate the analysis that lead them down this particular path. Lack of knowledge of the reasons behind the path chosen and also of how to choose one path over another are probably the reasons for this. The division of labor between analyst and programmer are harmful in two ways�: the programmer has a harder time seeing a flaw in design without knowledge of how to analyse, and it is easier to see an error in design during the implementation phase. Let's consider some flawed approaches to highlight common errors in analysis and implementation, then suggest some rules to follow to avoid these problems.


Consider the following design for a graphical user interface�:

responsibilities to implement�:

draw, accept click, maintain state, provide state, accept state

class hierarchy�:

GUIElement - Label

- Button - DropDownList

- List - DropDownList
- Grid

- Checkbox - RadioButton


concentrating on the draw responsibility, consider this definition.

Draw � convey all elements necessary for representation of state to the user.

Once fulfilled, this responsibility will provide the correct operation.

Now consider this proposed implementation path�:

class GUIElement�:
virtual void Draw()�; {}�;

class Label�:
virtual void Draw()�; { /* draw text at location */ }�;

class Button�:
virtual void Draw()�; { /* draw button at location depending on state */ }�;

class List�:
virtual void Draw()�; { /* draw rows of data in a vertically oriented repeating fashion */ }�;

class DropDownList�:
virtual void Draw()�; { /* draw the currently selected list item and a button. When button clicked, toggle between display of only selected item or a window for selection of items from list */ }�;

etc.

This is a likely popular implementation. The analyst has correctly identified and detailed the types of drawing to be done. The programmer may receive a small task that is the implementation of one Draw method. The code particular to the display of one object has a place to go.

This is a typically incorrect design that leads many to proclaim inheritance as a flawed mechanism. Why�? For instance, if DropDownList should naturally inherit from Button and List, then it cannot call on its parent's Draw() method to fulfill the responsibilities of drawing a button and a list. It might try using internal state to conditionnaly call either Button::Draw or List::Draw but that is not an efficient use of polymorphism. This seems to make multiple inheritance the culprit. What else�?

If we want to display text on a button, we are forced to repeat the code that is found in the Label class. We can't say a button IS-A label because that isn't true. Trying to do that to reduce code duplication would be a rude fix that might work in this simple case... or might not. This case is worse if these two methods are implemented by two programmers and they each chose a different approach to the rendering of text.

What is going on�? If inheritance is supposed to save work and neaten the separation of code, why are there two major problems with this seemingly obviously correct implementation path�?

The reason is that the basic tenet of object-oriented design was ignored at the very start. To correctly compose a class hierarchy, identify the responsibilities to be implemented, then create groupings of classes that share responsibilities. These groups must derive from parent classes who provide the implementation. So what is the design error exactly�? Simply put, GUIElement has not defined the Draw method.

That's right. Consider the ramifications. It was decided that one of the system's responsibilities was to convey all elements necessary for representation of state to the user. It was then decided that many objects shared this responsibility, and that a category of objects implementing this functionality should be created, having as base class GUIElement.

So far so good, but when happened next is that the very base object supposed to be implementing the responsibility said ��pass��. No C++ mechanism exists to force a base class to implement a responsibility, yet the whole idea behind the benefits of inheritance is that since a derived class IS-A GUIElement, it already knows how to draw itself.

Right, right, but how is it possible perform the actual drawing code in the base class without ever knowing what the expected appearance of the derived objects will be�? And another question�: how much time was spent coming up with this ��implementation plan�� anyway�? The answer is not enough because a correct implementation path is not only possible but obvious once the rules for correct object-oriented design are followed. Instead of seeing the impossible, see the solution. Stay on principle and work.

So, we know Draw is a responsibility shared by all descendants of GUIElement, so in essence there is to be no re-implementation or override of Draw by a child. This gives�:

class GUIElement�:
void Draw()�; { /* convey all elements necessary for representation of state to the user */ }�;

class Button�: public GUIElement�:
// no Draw()

Button myButton()�;
// initialise myButton
myButton.Draw()�;

Is this impossible�? No. Going over the definition of Draw in the base class, ��convey X to the user��. This means for example drawing on the screen. Obviously it is not the responsibility of GUIElement to know what a screen is or how to draw on it, so GUIElement will only deal in terms of some abstract visible object provided by another part of the system. GUIElement then is responsible for conveying a set of GraphicsPrimitives to the Renderer. This implies the following�:

class GUIElement�:
set visibleElements�;

This is called seeing the solution. It is a logic problem, start from the most restricted definition of what must be done, then slowly add the next logical piece needed to satisfy the definition.

So, the definition of Draw states ��convey all graphics primitives necessary for representation of state��. This means a GUI element has state, and that the declaration and holding of state is a responsibility shared by all objects in this category. Further, the base class will handle this responsibility so all derived classes inherit the implementation. Nowhere is it implied that a GUIElement base class must know what particular state composes a derived class. Neither is it implied what changes in state will result from data assigned or user interaction. These details will be provided in derived classes so nothing prevents us from completing the base class implementation of the Draw responsibility�:

class GUIElement�:
set stateElements�;
set visibleElements�;
void Draw()�; { /* convert state primitives to graphics primitives and pass to renderer */�}�;

The rules necessary for converting state primitives to graphics primitives also form part of the implementation of the Draw responsibility, and are not presented here. The point of this example is clear, however, provide generic implementation details and rules at the base class level. Derived classes do not implement base class responsibilities, they control their state so that base class code operates correctly.

A Label class deriving from GUIElement specifies in its initialisation routine that it contains a string StatePrimitive. When the base class Draw code is invoked, the child class's behaviour is state-defined locally but behaviour-implemented in the base class.

Further, the Button class also specifies that it contains a string StatePrimitive. The single copy of the code needed to draw this string is not necessary in its implementation. Consider the absurdity of the responsibility that would imply�: ��convert a string StatePrimitive to a text GraphicsPrimitive��. Why on earth should a Button GUI element (and the children of the category that it represents) be responsible for that�? In a correct object-oriented design, the Button class is responsible for the least possible number of responsibilities. In terms of drawing, one of these is ��define a GUI Button as possibly containing a string StatePrimitive��. It is worth noting that this responsibility is implemented using data only�! There is no new method necessary to achieve this goal.

A possible reproach to this proposed design is that it is overly complex. The implementation is overly generic, burdened by complex rules to convert one thing into another and it is difficult to be sure what the outcome will be without studying all elements in detail. Instead of providing concise obvious lines of code to do a concrete thing like draw a character, we are providing obscure data-driven definitions to conform to a strange interface that will eventually cause a character to be drawn. The reply to this reproach leads back to the introductory explanation in this essay, where C provided an immense advantage over assembly language, but fell short in the long run and was replaced for one reason�: the need to approach the implementation of truly complex projects.

Diamonds crumble quickly

What about the other flaw pointed out in the design proposed above�? An instance of the diamond problem is present in the confusion that arises when the DropDownList::Draw method is invoked. Without needing much explanation, it is obvious that a design which proposes implementing a base-class, category-defining responsibility in more than one class, then deriving a child from both those classes, is incorrect. By definition it would be like saying the child belonged to two categories that each shared the same responsibility. (and in which it so happened that the way to implement the responsibility is different, as if that matters.) If two categories really share a responsibility, then they are sub-categories, and the responsibility is to be implemented by the class which defines the parent category.

Conclusion

Code and data are two separate things. Behaviour arises as a result of the combination of the two. Confusion often arises as to how each is to be used to arrive at the correct result, and also as to where the blame lies when use of a design method leads to failure to provide the correct result.

At all times we may, with the proper application (and if necessary, creation) of rules, create a design method which may be followed to come up with a correct design. In the absence of a counter-proof, inheritance and multiple-inheritance are viable design concepts to be used when designing truly complex systems.

previous - next