JavaScript Jems - The Inheritance Tax
Written by Mike James   
Tuesday, 09 April 2024
Article Index
JavaScript Jems - The Inheritance Tax
Hierarchical Typing
Inheritance As The Model
Inheritance and Extends

Inheritance As The Model

Why do we use inheritance at all, and why is it related to the idea of subtype? This is a complicated question and one that leads to arguments. The original idea of using objects was to model the real world. In the real world things are objects with properties, and even methods, that allow the object to do something. Introducing objects into programming was to make it more like an exercise in simulation. Indeed the first object-oriented language was Simula, a language for simulation. The idea is that in the real world objects are related to one another. The problem is that they are related in complex ways.

As we have already stated, in programming the most basic intent of inheritance is to allow code reuse. Code reuse doesn’t have much to say about any type relationships. It is tempting to take the next step and to say that if objectB inherits from objectA then it is an objectA as well as being an objectB. A square is a square, but it is also a rectangle.

The Liskov substitution principle is the best known embodiment of this idea. It says that anywhere you can use an instance of a class, you can use an instance of any subclass. The reasoning is that the subclass has all of the methods of the base class and more. This is often true, but it isn’t always true. For example, by our previous reasoning, a square is a subclass of a rectangle, but you can’t use a square everywhere you can use a rectangle. The reason is that you cannot set the sides of a square to different values. There is a restriction on modifying a rectangle to make a square.

Restrictions and specializations spoil the neat idea that subtypes can be used in place of their supertype. What this means is that the Liskov substitution principle is more a theoretical simplification than a reflection of the world. This also makes strong typing an arbitrary theoretical decision when it come to rules for how class instances can be used. You can find ways to make subclasses always work as subtypes. For example, if you implement a square as a rectangle that still has two sides specified, you can retain all of the quadrilateral methods and enforce the equality some other way. This is far from natural.

There is also the problem that, in the real world, objects are related to multiple other objects. A square is a special case of a quadrilateral and it is an n-sided equilateral shape. You could try to model this by using multiple inheritance, but this is usually much more difficult than it seems when you first start to try it. This is the reason most other languages restrict themselves to single inheritance. Other languages add the idea of interfaces – essentially class declarations with no implementation. This allows for a limited form of multiple inheritance, but does nothing for code reuse, forcing programmers to return to copy-and-paste code reuse.

The problem is that the real world is often not modeled well by a single inheritance hierarchy, whether used with or without strong typing. This is the main reason that you will hear advice such as “prefer composition over inheritance”. The idea that one object contains another object is in many ways an easier concept to work with. So, for example, a car object might contain a steering wheel object and four road wheel objects, which in turn contain wheel objects. However, this doesn’t always fit, how does a square contain a rectangle object? Add to this the fact that current languages provide poor support for composition and it isn't so attractive.

It is claimed that the hierarchy of classes and object types is good because it mimics the way the world is organized. Things in the real world are supposed to be structured in the same way, i.e. hierarchical, and our code should follow this lead and be hierarchical. Of course, this isn't the case - the relationships between things in the real world are much more complicated than a simple hierarchy. In many cases hierarchical typing means that you have to force your model of the world into an inappropriate form. There are examples where the hierarchy fits. Many code libraries are built starting off from a primitive example of the type which is then refined in different ways, leading to a hierarchy that represents the range of objects that are available. However, most programmers don't create libraries and spend more time working with objects that have messy complex relationships and perhaps no relationships at all.

Inheritance or Composition

It is often difficult to work out the best relationships between objects. For example, is a circle a subclass of ellipse or vice versa? At first it seems obvious that an ellipse is a subclass of circle because a circle has one radius and an ellipse has two, so an ellipse is an extended circle class. However, you can think of a circle as a special case of an ellipse when the two radii are equal. If you choose to implement the circle as the base class (using Java) then the result is something like:

public class Circle {
    int radius;
    void draw() {
        draw a circle using radius
    }
}
public class Ellipse extends Circle{
    int radius2;
    void draw() {
        draw an ellipse using radius and radius2
    }
}

As drawing an ellipse is more complicated than drawing a circle, we need to override the circle's draw function so as to draw an ellipse. So really all we get from the Circle class via inheritance is a single property, radius, which is not a rich inheritance.

What is more, with virtual inheritance, the norm in most languages, the Liskov substitution principle is broken. If you were to write:

Circle myShape=new Ellipse();
myShape.radius=10;
myShape.draw();

then, despite myShape being a Circle, it is the draw method of Ellipse that is used which uses radius and radius2 to draw the ellipse. Of course, radius2 hasn't been set, and cannot be set because myShape is a Circle and doesn't have a radius2 property. We could write the ellipse draw method to conform to the Liskov substitution principle, but this is another complication. In general, overriding any method potentially breaks the Liskov substitution principle and invalidates hierarchical typing.

Now consider implementing things the other way around. Starting from an implementation of Ellipse let's derive an implementation of Circle in JavaScript that doesn't even hint at inheritance and uses class so you can compare the two. First the Ellipse:

class Ellipse { 
   constructor() {
     this.radius1 = 1;
     this.radius2 = 1;
   }
   draw() { 
     draw an ellipse using radius1 and radius 2
   }
 }

As a circle is a special case, a restriction, of an Ellipse we can implement Circle very easily:

class Circle { 
   constructor() { 
     this.ellipse = new Ellipse();
   } 
   draw() { 
     this.ellipse.draw();
   }
   set radius(value){ 
     this.ellipse.radius1=value;
     this.ellipse.radius2=value;
   } 
   get radius(){ 
     return this.radius1;
   }
}

Notice that in place of inheritance we have composition - now the Circle object contains an Ellipse object. Also notice that we need get and set to enforce the restriction to a circle.

There are lots of cases where composition is much more natural than inheritance which is why the advice "prefer composition over inheritance" has started to gain ground.

The theory of inheritance and hierarchical typing is satisfying, but not a good fit with the real world and thus is one of the areas of object-oriented programming that has come under attack. All of this is an argument that is slightly beyond the scope of this discussion, but the key point is that inheritance and hierarchical typing have not been proved to be better than the alternatives.

kindlecover

 



Last Updated ( Wednesday, 10 April 2024 )