Subclassing

Dear Computer

Chapter 5: Objects

Subclassing

Once we start coding with objects, we inevitably encounter some choices about how to to organize our classes in sustainable ways. Suppose we are writing a Ruby interpreter for a new programming language, and we've defined this class to model an addition expression:

Ruby
class Add
  attr_accessor :left, :right

  def initialize(left, right)
    @left = left
    @right = right
  end

  def evaluate
    Number.new(@left.evaluate.value + @right.evaluate.value)
  end

  def to_s
    "(#{left.to_s} + #{right.to_s})"
  end

  def to_s_expr
    "(+ #{left.to_s} #{right.to_s})"
  end
end

We go to create a similar Subtract class, and we find that it has a lot in common with Add. Do we copy and paste? That won't scale well if our language has more than a couple of operators.

Consider also the design of a LockableTreasureChest class. It is similar to the TreasureChest class, but it only yields its contents if it has first been unlocked. Do we create a brand new class and copy over the code from TreasureChest? That smells like work and poor maintainability.

Both these problems may be solved through subclassing, in which a class is built on the foundation of another. The repeated code for the add and subtract classes is factored out to an BinaryOperator superclass:

Ruby
class BinaryOperator
  attr_accessor :left, :right, :symbol

  def initialize(symbol, left, right)
    @symbol = symbol
    @left = left
    @right = right
  end

  def to_s
    "(#{left.to_s} #{symbol} #{right.to_s})"
  end

  def to_s_expr
    "(#{symbol} #{left.to_s} #{right.to_s})"
  end
end

The subclasses extend this superclass, letting it do as much of the common work as possible:

Ruby
class Add < BinaryOperator
  def initialize(left, right)
    super('+', left, right)
  end

  def evaluate
    Number.new(@left.evaluate.value + @right.evaluate.value)
  end
end

class Subtract < BinaryOperator
  def initialize(left, right)
    super('-', left, right)
  end

  def evaluate
    Number.new(@left.evaluate.value - @right.evaluate.value)
  end
end

The LockableTreasureChest class may reuse the code of TreasureChest but introduce its own version of open that checks the lock status before returning its contents:

Ruby
class LockableTreasureChest < TreasureChest
  def initialize(contents)
    super(contents)
    @is_locked = true
  end

  def open
    @is_locked ? nil : super.open
  end

  def unlock
    @is_locked = false
  end
end

Subclassing confers two primary benefits: code reuse and subtype polymorphism. Code reuse is conferred because a subclass inherits the methods of its superclasses; we don't have to reimplement them. Subtype polymorphism is conferred because we can write code that targets the superclass, which is a higher level of abstraction, and it will work just as well with any subclass. This Ruby function randomly places any kind of treasure chest, including those that can be locked and those that haven't been written yet:

Ruby
def randomizeChests(chests)
  chests.each do |chest|
    chest.location = [rand(100), rand(100)]
  end
end

chests = [
  TreasureChest.new('Courage Juice'),
  LockableTreasureChest.new('Hedge Slammer'),
]
randomizeChests(chests)

All treasure chests have a location= setter, whether they're plain or lockable, so the randomizeChests function is subtype polymorphic.

Super Constructor

When a superclass has a constructor, the subclass must invoke it to initialize its superclass foundation. If the superclass constructor can be called without parameters, compilers for Java, C++, and C# will automatically insert a call to the superclass constructor if we don't have an explicit call. If parameters are required, compilation will fail if we don't have an explicit call.

The superclass constructor must be called before any other code that depends on the object's state gets executed. That's because the object isn't considered stable to build on until its superclass foundation has been constructed. Java, JavaScript, C++, and C# enforce that the superclass constructor call is the very first statement. C++ and C# elevate its importance by extracting it from the constructor body and promoting it to the header, as demonstrated in this C# class:

C#
public class LockableTreasureChest : TreasureChest {
  public LockableTreasureChest(Item contents) : base(contents) {
    // ...
  }
}

Java requires it to be the first statement of the body:

Java
public class LockableTreasureChest extends TreasureChest {
  public LockableTreasureChest(Item contents) {
    // No other code can appear before the super call.
    super(contents);
    // ...
  }
}

We can't even have an innocuous print statement prior to the call. Ruby, Python, and C# are more flexible about when the superclass constructor is called.

← PropertiesOverriding Behaviors →