Deconstructing Inheritance

Deconstructing Inheritance

By Lucian Radu Teodorescu

Overload, 28(156):14-18, April 2020


Inheritance can be overused. Lucian Radu Teodorescu considers how it can go wrong and the alternatives.

After glancing at the title, the reader might accuse me of trying to destroy inheritance; probably by arguing that it should be replaced by some other mechanism. But that is not the case; that is not my intent. According to Merriam-Webster [ MW ], deconstruction is defined as:

: a philosophical or critical method which asserts that meanings, metaphysical constructs, and hierarchical oppositions (as between key terms in a philosophical or literary work) are always rendered unstable by their dependence on ultimately arbitrary signifiers

: the analytic examination of something (such as a theory) often in order to reveal its inadequacy

My intent here is to reveal inheritance’s actual meanings versus the meanings that most Object-Oriented programmers will infuse it with; to show hidden oppositions in its structure, to show that some signifiers are somehow arbitrary, and finally to reveal inner inadequacies. The main point is to test the limits of inheritance, and how far we can go until our beliefs about inheritance break.

One of the main topics of the article will be the relation between inheritance and the is-a relationship, and how this connects to the principle of correspondence (the common design belief that modelling OOP software should maintain a correspondence to the real-world that the software somehow models). Another important topic that is frequently referred to in this article is the Liskov Substitution Principle (LSP) [ Liskov94 ] [ Liskov88 ].

These two topics are a crucial point in analysing inheritance. They both define what inheritance is, but also subversively work against it, creating this amorphous concept that encompasses both good and bad.

Some simple problems are hard

Let’s look at a very simple OOP modelling problem: we want to model the Rectangle and the Square concepts in software. For our problem, we are only interested in dimensions. As the two concepts are closely related in the real-world, we want to relate them with an inheritance in our software. There are 2 main options:

  • make Rectangle inherit from Square
  • make Square inherit from Rectangle

Let us analyse both options.

Rectangle is-a Square

First thing: this is mathematically incorrect. In the real-world, the is-a relationship is reversed. But, let’s ignore this for a moment. Let’s look at the code in Listing 1, which is modelling Rectangle is-a Square .

class Square {
  int size;
public:
  virtual int getSize() const { return size; }
  virtual void setSize(int x) { size = x; }
  virtual int getArea() const {
    return size*size; }
};
class Rectangle: public Square {
  int width;
public:
  virtual int getWidth() const { return width; }
  virtual int getHeight() const {
    return Square::getSize(); }
  virtual void setSize(int x) {
    Square::setSize(x); width = x; }
  virtual void setWidth(int x) { width = x; }
  virtual void setHeight(int x) {
    Square::setSize(x); }
  virtual int getArea() const {
    return width*getSize(); }
};
			
Listing 1

Not only is the mathematical relation broken, but the interface of the Rectangle class is polluted by concerns that it doesn’t have (size is confusing for Rectangle ). Moreover, we can easily find an example (see Listing 2) in which this breaks the LSP test (if you change the type, does the code still functions well?).

void increaseArea(Square& square) {
  auto oldArea = square.getArea();
  square.setSize(square.getSize() * 2);
  auto newArea = square.getArea();
  assert(newArea == 4 * oldArea);
}
			
Listing 2

Passing a Rectangle object to the increaseArea function will make the code break. This variant is definitely not right. Let’s try the other one.

Square is-a Rectangle

Let’s look at the code in Listing 3.

class Rectangle {
  int width, height;
public:
  virtual int getWidth() const { return width; }
  virtual int getHeight() const { return height; }
  virtual void setWidth(int x) { width = x; }
  virtual void setHeight(int x) { height = x; }
  virtual int getArea() const {
    return width*height; }
};
class Square: public Rectangle {
public:
  virtual int getSize() const {
    return Rectangle::getWidth(); }
  virtual void setSize(int x) {
    Rectangle::setWidth(x);
    Rectangle::setHeight(x); }
};
			
Listing 3

Mathematically, this seems to be correct. And the interface of Square is not necessarily polluted with the unneeded stuff (the inherited methods can be hidden). Let’s now try to see if it passes the LSP test (see Listing 4).

void increaseAreaNew(Rectangle& r) {
  auto oldArea = r.getArea();
  r.setWidth(r.getWidth() * 2);
  auto newArea = r.getArea();
  assert(newArea == 2 * oldArea);
}
			
Listing 4

Similarly to the previous test, if we assume that r is a veritable rectangle, doubling the width will double the area. But, if r is a square, then the area will increase by 4 times.

Another problem is with the existence of the setWidth and setHeight functions of the base class. No matter how we override them in the derived class, the existence of these setters will make possible clients of Rectangle break. If we ignore to override them, it’s easy to see that a call to any of these will break the invariants of Square . If we throw exceptions, we may break Rectangle clients that used to work ok. If we change both the width and the height when one setter is called, then we can find examples similar to Listing 4. There is no reasonable override to these methods that cannot be proven to be wrong with the help of LSP.

More discussion

The previous examples showed us that, if we want to force the mathematical relationship between area and the sizes of the square/rectangle, no matter how we do the inheritance, we cannot do it right.

One good observation that will allow us to fix things is to remove the setters; make objects of those two classes immutable. Something similar to the code in Listing 5.

class Rectangle {
protected:
  const int width, height;
public:
  Rectangle(int w, int h) : width(w), height(h) {}
  virtual int getWidth() const { return width; }
  virtual int getHeight() const { return height; }
  virtual int getArea() const {
    return width*height; }
};
class Square: public Rectangle {
public:
  Square(int s) : Rectangle(s, s) {}
  virtual int getSize() const {
    return Rectangle::getWidth(); }
};
			
Listing 5

This code doesn’t break LSP as, once created, the objects cannot be made to break their invariants. However, then the main question that arises is what are actually gaining from the inheritance anyway? We occupy more memory for the Square objects, and we make a few functions virtual, that most probably are not used. The only thing that is reused is the area() method, which, mostly by coincidence, did not need rewritten. Adding inheritance here does not help us.

Let’s look at what others are saying about this problem:

The truth is that Squares and Rectangles, even immutable Squares and Rectangles, ought not be associated by inheritance – Robert C. Martin

The class Square is not a square, it is a program that represents a square. The class Rectangle is not a rectangle, it is a program that represents a rectangle. […] The fact that a square is a rectangle does not mean that their representatives share the ISA relationship. – Robert C. Martin

ISA is useful when trying to model real world relations to make class hierarchies intuitive, but classes are metaphors, and metaphors, if extended too far will break – Bjørn Konestabo

One cannot use inheritance to model a very simple mathematical problem. Even though we have a good insight into what the real-word concepts mean when we place them into code, the metaphor breaks. Inheritance doesn’t work the way the is-a relationship works in mathematics.

Concepts, is-a and inheritance

The previous sections showed us that inheritance cannot always properly model the is-a relationships from the real-world. Let’s look at some more cases in which the analogy with the real-world breaks.

Let’s think of modelling an elevator system. Besides the elevator car, motor or doors, we have a lot of buttons. We have buttons on each floor (up/down), we have buttons inside the elevator, both for floors and for cancelling or alerting. We can model the system as shown in Figure 1. But we can also model it with Figure 2.

Figure 1
Figure 2

Or, we can simply model everything with just one Button class (Figure 3).

Figure 3

To be honest, I would probably go for the last option, but that doesn’t matter too much for this discussion.

If we carefully look at the various methods (and others can easily be found), we realise that the Button concept is probably the only real-world concept. Things like TravelButton , InsideButton , and FloorButton are concepts invented in software modelling, and then somehow look real. Nobody thinks of an inside-button concept in the real world. Yes, we sometimes distinguish between buttons that are inside the lift car and the ones that are fixed to the floors, but that’s a property of the objects themselves; inside-button and outside-button are not strong enough to be concepts by themselves.

Without dwelling too much on the semantics of the concept concept, we observe a discrepancy between concepts inspired between real-life (concepts as building blocks of thoughts) and concepts that are generated through our design process (concepts as sets, that can always be divided into smaller sets). If we want to stick to the classes that should be inspired by real-world concepts, we should probably abandon the second type of concepts.

And now we’ve reached the fun part: what does ‘is-a’ mean? What does it take for a concept to be another concept? Too bad for us that metaphysics has not been able to figure out the answer to this issue in the last 2000+ years. While we wait for the philosophers to figure this out, we can safely assume at least that we cannot say A is-a B if A and B belong to different species. And, in our case, we just argued that InsideButton and Button belong to different species: one in an artificially constructed concept and one is a real-world inspired concept. That means that is improper to say that InsideButton is-a Button (at least, not while considering Button as a real-world concept).

A far more useful relation would be the behaves-like-a relationship. We can safely say that InsideButton behaves-like-a Button , even if the two concepts come from different worlds (e.g., a dolphin can behave like a fish even if it’s not a fish). Moreover, from a software perspective, we are only interested in the behaves-like-a relationship, and we can leave the is-a to metaphysics. When I say D behaves-like-a B , what I mean is that D inherits all the properties of B , that I can use D in all the places that I would use B . But this is exactly what the Liskov Substitution Principle says.

So, in other words, each time we look at inheritance, instead of thinking about is-a relationships, we should think of behaves-like-a relationships, and then immediately think of LSP.

If my digression into semiotics and metaphysics left the users too confused, maybe a quote would do better [ Sutter04 ]:

The “is-a” description of public inheritance is misunderstood when people use it to draw irrelevant real-world analogies: A square “is-a” rectangle (mathematically) but a Square is not a Rectangle (behaviorally). Consequently, instead of “is-a,” we prefer to say “works-like-a” (or, if you prefer, “usable-as-a”) to make the description less prone to misunderstanding.

To prove my point, this quote was taken from the chapter named ‘Public inheritance is substitutability. Inherit, not to reuse, but to be reused’. Public inheritance is substitutability. q.e.d.

On the fine details of LSP

Formally, LSP states the following [ Liskov94 ]:

Subtype requirement: Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T.

The informal principle goes the following way [ Liskov88 ] (see also [ ObjectMentor03 ]):

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

It basically defines what a subtyping relation should be, and by extension, it describes what successful inheritance should be.

Now, if we assume a strict interpretation of the formal principle, we can argue that polymorphism cannot happen under subtyping, rendering inheritance useless.

Let us quickly sketch a proof. Let’s say that we have classes D and B , D being a subtype of B . Then we have a method m , implemented as m1 in B and as m2 in D , using a different implementation. In this case we can define the provable property ϕ ( x ) as being ϕ ( x ) = x.m() has-exactly-the-same-behaviour-as m1 . It is clear that this property holds for any object of type B , but it does not hold for objects of type D . The only way to make LSP work for such properties is to make the methods have identical behaviour, so, therefore, to eliminate polymorphism. q.e.d.

The reader might accuse us of exaggerating the matter by artificially constructing counterexamples; something that cannot happen in practice. But if we consider our statement in the context of the Open/Closed Principle, then we can easily imagine how this is not exaggerating. After all, with the types under the incidence of inheritance can be a closed system, and we should be able to extend this system with functionality that contains our counterexample. And this is not just a theoretical problem. Myself, I’ve encountered violations of LSP built on the same pattern as our proof above multiple times (of course, unintentional).

At this point, some readers might still argue that we should probably not be applying the LSP principle so strictly. We should only consider the properties relevant to the program in question. That is, if we want to make D derive from B , we should consider all the ‘practical’ properties associated with D and B . This idea is similar to the one that Sutter and Alexandrescu argue in their ‘Public inheritance is substitutability. Inherit, not to reuse, but to be reused’ recommendation; they argue that creating inheritance should be the focus on the external code that may be able to use this inheritance relationship.

But there is a great danger if we go in this direction. Inheritance was supposed to be an abstraction feature, one that reduces the complexity of the software – instead of looking at a large number of classes, we should look at fewer classes. But instead, LSP forces us to consider all the visible properties of the code, for all the users of the classes. It’s an anti-abstraction feature.

Let’s take an example. In the previous section, the method to compute the area of a rectangle or a square is a relatively good abstraction. It decouples the implementation details (in this case very simple) from the code that utilises it. We can easily document it and explain it to other engineers. But, suddenly, if we add inheritance to any of these classes, we need to also consider how this method can be used by the callers, coupling it to a possibly large number of implementations. If we are not careful and capture the usage patterns, we might end up with examples that break LSP, and thus render the program invalid.

So, theoretically, LSP cannot be formally applied, and if we are using our practical sense to apply it, we may be creating a larger problem for us to solve. To paraphrase a programmer’s joke: we have one problem to solve; let’s try to throw in inheritance to solve it – now we have two problems to solve, and one of them is hard to solve.

LSP and invariants

Let’s now analyse how LSP applies from a different point of view: that of maintaining invariants. The base class has some invariants. The main question would be for a derived class on how it can change the behaviour of the objects while maintaining the same invariants – after all, invariants are visible properties, and LSP dictates that they should not change.

One can think of invariants as predicates that can be applied to objects. Whenever the predicate returns false for an object, the invariant doesn’t hold. Moreover, it can be chained as a conjunctive form giving a series of conditions C 1 , C 2 , …, C n . The predicate holds if all the conditions are true. Can we add or remove conditions in the derived classes? Let’s look at both cases. Let’s assume that the invariant of the base class is I B = C 1 C 2 C 3 .

First, let’s consider that case in which the derived class removes a constraint, let’s say C 3 . The invariant for the derived class will be I D = C 1 C 2 . Now, all the D objects that properly satisfy I D , but do not satisfy C 3 , will not satisfy I B . LSP will not apply to those objects. Thus, removing constraints in derived classes will break LSP.

In the other case, when we add constraints, things appear to work well. Mathematically we can easily prove that the invariants of the base class are met for the derived objects: I D I B .

But this works well only when all the invariants are known upfront. And, most of the time, as software is in its essence just complexity [ Brooks95 ], not all invariants are known upfront – there are a lot of implicit assumptions. Let’s say that I B = C 1 C 2 C 3 and I D = C 1 C 2 C 3 C 4 , and that C 4 never applies to any object of B . In such cases, a user can accidentally assume that C 4 never happens. This becomes an accidental assumption in B ’s invariant: I B = C 1 C 2 C 3 ∧ ¬ C 4 . If this happens, then again LSP is broken.

So, to make LSP work, we have to survey all usercode to check for hidden assumptions, before we can derive from B ; both when trying to keep the same or when adding new constraints to the invariant of the derived class.

Inheritance and composition

We can start with Robert C. Martin’s quote to set the basis for the discussion in this section:

A pox on the ISA relationship. It’s been misleading and damaging for decades. Inheritance is not ISA. Inheritance is the redeclaration of functions and variables in a sub-scope. No more. No less.

Well, the content misses one big point: inheritance also adds subtyping (allowing us to implicitly convert derived-class objects into base-class objects); which in turn can be used to implement polymorphism. But the rest is true.

If inheritance is just a redeclaration of functions and variables, then we can easily transform it into composition. Instead of making D derive from B , we can make D contain a B .

Therefore, if subtyping is not needed, it’s better to just use composition instead of inheritance as we don’t have to deal with the complications introduced by subtyping.

The same conclusion reaches Sutter and Alexandrescu in their C++ coding standards: 101 rules, guidelines, and best practices book [ Sutter04 ]; see the section called ‘Prefer composition to inheritance’. I won’t repeat the arguments, as the idea is relatively straightforward.

Inheritance is stronger than friendship

Sutter and Alexandrescu argue that inheritance is almost as strong as friendship (same section of [ Sutter04 ]). My opinion is that we should move forward and argue that inheritance is even stronger than friendship.

Making an analogy with real-life, one’s children are closer than one’s friends. The children can be friends, but in general, are more than that. Yes, children, especially young ones, may not have access to all the information their parent shares with friends but may dramatically alter the life of the parent.

The analogy works with classes. Yes, derived classes may not access all the fields of the base class, but they can seriously affect the space of the invariants. Future development in the derived classes may involve changing the invariants in the base class, and therefore affecting all the other derived classes, and all their users.

Friendship can affect private data, the internals of the class. But, if encapsulation is done right, this will not change the public interface of the class, and therefore the behaviour of the clients. Like any abstraction, class-level encapsulation restrains the impact of a change. In contrast, an inheritance that changes the invariants (directly or indirectly) is a public change, and it affects all the clients of the class.

To exemplify the impact of inheritance, let’s look at Figure 4. Let’s assume that the AirplaneUser needs a change to the flying behaviour. This affects the Airplane class, which changes the invariants of FlyingThing , which, in turn, affects Duck and finally DuckUser . In effect, the AirplaneUser and DuckUser classes are indirectly coupled.

Figure 4

This is stronger than friendship. Friendship may change your internal state, but if it’s not done particularly badly, it tends not to break your invariants, and your users are isolated from the change.

Looking only at the difference between protected and private access is missing the larger impact of inheritance. However, if we look at the bigger picture, if we consider LSP and the example above, we can conclude that inheritance implies more coupling than friendship.

Conclusions

In this article, we have tried to cast a critical perspective on inheritance, as one of the most used (or abused) features of OOP. The intent of this critical perspective was not to prove that inheritance should not be used at all, but rather to test its limits. What becomes apparent is that this is not a feature that should be abused, and great care needs to be taken when adding new inheritance to a software system, not to break existing code. In other words, we don’t have local guarantees when adding inheritance.

We started with a classic problem of Rectangle and Square, and have shown that inheritance doesn’t quite make sense in code, even though a square is a rectangle in mathematics. We explored the meaning of the is-a relationship and its relation to the real-world; after all, a common strategy in OOP modelling is to use real-world analogies. We concluded that this analogy works only to a point. Inheritance is not is-a . Furthermore, the term is-a can be confusing (unless we solve a large part of metaphysics). A slightly more appropriate way to think of inheritance is to think of it as a behaves-like-a relationship; i.e., what LSP preaches.

Moving forward, we showed that LSP is hard to apply. If we want to be strictly formal, we cannot apply it. In practice, we can, however, apply it, but not as easily as we may think. We cannot have local reasoning (looking just at the base class and the derived code). We need to look at all the user code and all the implicit assumptions that this code makes. Depending on the software, there may be semantic leaks towards all parts of the code; yes, that may be a badly structured code, but there is no clear algorithm that indicates whether we have such semantic leaks. As much as we would like to put boundaries to the implications of inheritance and LSP, it seems that we can’t.

We also briefly argued that whenever possible composition should be preferred to inheritance. And, to add one more negative aspect to inheritance, we’ve argued that inheritance is a stronger relationship than friendship, relation widely considered harmful.

And, as we are enumerating some negative aspects of inheritance, we should mention the presentation called Inheritance Is The Base Class of Evil , by Sean Parent [ Parent13 ] – the name is too good not to be mentioned.

But again, the purpose of our deconstruction is not to show that inheritance should not be used. The main idea is to better know its limits, to find its weak points, and to find its internal inadequacies; when it can be applied, and where it can generate problems. There are cases in which inheritance is, at least useful, if not more. But this can be the topic of another article.

References

[Brooks95] Frederick P. Brooks Jr. (1995), The Mythical Man-Month: Essays on Software Engineering , 20th Anniversary Edition, Addison-Wesley

[Liskov88] Barbara H. Liskov (1988), Data Abstraction and Hierarchy

[Liskov94] Barbara H. Liskov, Jeannette M. Wing (1994), A behavioral notion of subtyping, ACM Transactions on Programming Languages and Systems

[MW] Merriam-Webster (2020), Definition of ‘deconstruction’, https://www.merriam-webster.com/dictionary/deconstruction

[ObjectMentor03] Object Mentor (2003), ‘The Liskov Substitution Principle’, https://web.archive.org/web/20030403055009/http://www.objectmentor.com:80/resources/articles/lsp.pdf

[Parent13] Sean Parent (2013), ‘Inheritance Is The Base Class of Evil’, GoingNative 2013 , https://channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil

[Sutter04] Herb Sutter, Andrei Alexandrescu (2004), C++ coding standards: 101 rules, guidelines, and best practices , Addison-Wesley Professional

Lucian Radu Teodorescu has a PhD in programming languages and is a Software Architect at Garmin. As hobbies, he is working on his own programming language and he is improving his Chuck Norris debugging skills: staring at the code until all the bugs flee in horror.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.