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:
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:
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:
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:
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:
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:
public class LockableTreasureChest : TreasureChest {
public LockableTreasureChest(Item contents) : base(contents) {
// ...
}
}
Java requires it to be the first statement of the body:
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.