Inheritance

Inheritance #

Subclass #

In most (class-based) object-oriented languages, a class $A$ can extend another class $B$. In this case, $A$ is called a subclass of $B$.

The intuitive meaning is inclusion between their respective sets of instances, i.e. every instance of $A$ is also an instance of $B$ (but the converse may not hold).

This can be paraphrased in English by “every $A$ is a $B$ “. For instance:

  • every banana is a fruit,
  • every square is a rectangle,
  • every rectangle is a polygon,
  • etc.

Transitivity #

The “extend” relation is transitive, meaning that if $A$ extends $B$ and $B$ extends $C$, then $A$ extends $C$ (for any classes $A$, $B$ and $C$ ).

For instance, from the above examples, one can infer that “every square is a geometric shape”.

Inheritance #

Naturally, if $A$ extends $B$, then it inherits the properties of $B$.

For instance, a rectangle has four right angles. Since every square is a rectangle, a square has four right angles as well.

Factorizing code with a (possibly abstract) superclass #

Inheritance can be used to avoid redundant code.

Direct inheritance: illustration #

Let us model the units of our game as objects.

Each unit has:

  • a type (e.g. mage, unicorn, etc.),
  • a color (at least in the original game), and
  • a certain amount of health.

The behavior and stats of a unit (e.g number of turns before attacking when combined, default health, etc.) are dictated by its type. So it makes sense to group units by type.

For instance, one can create a class Unicorn whose instances are all units of type unicorn. In Java:

public class Unicorn {
    String color;
    int health;
    int attackCountdown;

    public Unicorn(String color) {
      this.color = color;
      health = 1;
      attackCountdown = -1;
    }
}

Note. We used the keyword attackCountdown in this example to indicate the number of turns before the unit attacks (and a special value of -1 when it is not set to attack). But there are of course other ways to model this.

Note. In this example, we used the prefix this. for the attribute color only, because there is no ambiguity for the two other attributes.

We can also create a class Butterfly on the same model

public class Butterfly {
    String color;
    int health;
    int attackCountdown;

    public Butterfly(String color) {
      this.color = color;
      health = 2;
      int attackCountdown = -1;
    }
}

Now consider a method encounter that manages an encounter between two units. Without inheritance, one would need to implement at least three versions of this method (possibly four if encounters are asymmetric):

  • unicorn vs unicorn,
  • unicorn vs butterfly and
  • butterfly vs butterfly.

For instance, the first of these three methods could be implemented as follows:

    void encounter(Unicorn u1, Unicorn u2) {

      // subtract the health of u2 from the health of u1
      u1.health -= u2.health;
      // the health of u2 becomes the inverse of the remaining health of u1
      u2.health = -u1.health;
    }

More generally, if the game has $n$ types of units, then the code will contain $\frac{n(n-1)}{2} + n$ nearly identical encounter methods.

Question. Can we use inheritance in this example to avoid duplicate code (and how)?

Observe that a unicorn and a butterfly (viewed as object) have identical attributes (a.k.a. “keys”), namely String color, int health, and int attackCountdown. So we can create a superclass of Unicorn and Butterfly that carries these attributes, and let the two subclasses inherit it. For instance, this superclass may be called Unit.

However, we may also want every unit in the game to have a concrete type (like “unicorn” or “butterfly”), rather than being a generic “unit”. In Java, this can be achieved with the abstract keyword. This keyword ensures that our superclass cannot be directly instantiated (even though it can still have a constructor). For instance:

public abstract class Unit {
    String color;
    int health;
    int attackCountdown;

    public Unit(String color, int health) {
      this.color = color;
      this.health = health;
      int attackCountdown = -1;
    }
}

Because this class is abstract, the following code will not compile:

Unit myUnit = new Unit("green", 2);

Next, we can declare that Unicorn extends Unit, by using the Java keyword extends. We can also use the constructor of Unit within the constructor of Unicorn, with the Java keyword super.

Note. In Java (as opposed to C++ for instance), a class can only have one immediate superclass, so the keyword super is never ambiguous.

This yields:

public class Unicorn extends Unit {

    public Unicorn(String color) {
      super(color, 1);
    }
}

And we can proceed similarly for the class Butterfly.

Observe that all the attributes are now carried by the superclass Unit. However, because they are inherited, these attributes can be accessed as if they were regular attributes of the subclass. For instance,

  Unicorn myUnit = new Unicorn("green");
  System.out.println(myUnit.health);

outputs

1

This allows us to write a generic encounter method, as follows:

    void encounter(Unit u1, Unit u2) {

      u1.health -= u2.health;
      u2.health = -u1.health;
    }

And this method can be used with unicorns and/or butterflies. For instance:

  Unicorn myUnicorn = new Unicorn("green");
  Butterfly myButterfly = new Butterfly("yellow");
  encounter(myUnicorn, myButterfly);
}

Transitive inheritance #

In the example above, we assumed that all units have a color and can attack. What if we also want to create a type of unit called Wall that has no color and cannot attack? An instance of this class does not need the attributes color and attackCountdown.

A quick solution here consists is setting attackCountdown to -1, and color to null. However, unnecessary attributes make code harder to understand (therefore bug-prone), and such a design may not scale well if the game is extended with more units types.

Modify our model to accommodate for the class Wall, so that an instance of Wall only has the health attribute.

One solution (among others) is the following:

  1. modify the class Unit so that it only carries the attribute health,
  2. Wall extends Unit,
  3. create an (abstract) subclass of Unit (for instance MobileUnit) that carries the other two attributes,
  4. Butterfly and Unicorn extend MobileUnit (therefore they also extend Unit, by transitivity).

Or in Java:

public abstract class Unit {
    int health;

    public Unit(int health) {
      this.health = health;
    }
}
public class Wall extends Unit {

    public Wall() {
      super(5);
    }
}
public abstract class MobileUnit extends Unit {
    String color;
    int attackCountdown;

    public MobileUnit(String color, int health) {
      super(health);
      this.color = color;
      this.attackCountdown = -1;
    }
}
public class Unicorn extends MobileUnit {

    public Unicorn(String color) {
      super(color, 1);
    }
}

and similarly for Butterfly.