RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

A Few Notes on Classical Inheritance in Ruby (POODR: Chapter 6)

Having recently graduated from the Flatiron School, I am working my way through programming-related reading, beginning with Sandi Metz’s “Practical Object-Oriented Design in Ruby”. Click here for more posts about POODR.



What’s inheritance?

The idea of inheritance may seem complicated but as with all complexity, there’s a simplifying abstraction. Inheritance is, at its core, a mechanism for automatic message delegation. It defines a forwarding path for not-understood messages. It creates relationships such that, if one object cannot respond to a received message, it delegates that message to another. You don’t have to write code to explicitly delegate the message, instead you define an inheritance relationship between two objects and the forwarding happens automatically.

That’s it, that’s all there is to inheritance. Not really. There’s a lot to inheritance, and it’s one of the things that makes Ruby the language that it is, in all its object-oriented glory.

In this post, I’ll speed through a couple key lessons about classical inheritance. Some of it was old news, some of it brand new.

Classical?

Referring specifically to “classical” inheritance is simply to refer to “class”-based inheritance. This distinguishes it from other inheritance techniques (like modules in Ruby, prototypical inheritance in JavaScript, or any number of others). Classical inheritance deals with subclasses and superclasses (from which subclasses inherit).

Single vs. Multiple Inheritance

Multiple inheritance can get complicated (how do we know from which ancestor a descendant will inherit?). Metz explains that “Many object-oriented languages [including Ruby] sidestep these complications by providing single inheritance, whereby a subclass is allowed only one parent superclass. Ruby does this; it has single inheritance. A superclass may have many subclasses, but each subclass is permitted only one superclass” (112).

Inherent Inheritance (inheritance and nil?)

Whether you’ve implemented a class hierarchy or not, if you’ve used Ruby then you’ve used inheritance. An example: nil?. Ruby contains two implementations of that method, one in NilClass and the other in Object. When nil? is called on an instance of NilClass, it returns true. On everything else, because everything elses is a subclass of Object, the nil? message travels up the superclass hierarchy to Object, which will then return false. See the image below for a depiction of this.

Creating a Hierarchy has Costs (to duplicate or inherit?)

Any decision to implement inheritance should take into consideration the costs. Metz presents an example of a MountainBike and a RoadBike class, both of which could inherit from the Bicycle superclass. But she asks, “Even though you now have a requirement for two kinds of bikes, this still may not be the right moment to commit to inheritance” and continues “A decision to proceed with the hierarchy accepts the risk that you may not yet have enough information to identify the correct abstraction. Your choice about whether to wait or to proceed hinges on how soon you expect a third bike to appear versus how much you expect the duplication to cost” (118-119).

“Push-everything-down-and-then-pull-some-things-up strategy”

That’s a technical term, I think. It’s a direct quote, anyway. Metz describes that, in implementing inheritance, it is important to push certain things down to the subclass, then pull them back up to the superclass to be inherited (and vice versa), even though it means moving code around multiple times. Her reasoning? Every programmer needs to ask themself: “What will happen when I’m wrong?”. Metz argues that “Every decision you make includes two costs: one to implement it and another to change it when you discover that you were wrong. Taking both costs into account when choosing among alternatives motivates you to make conservative choices that minimize the cost of change” (123).

Super keywords

“Sending super in any method passes that message up the superclass chain” (115). This is essential for inheritance. Adding super to a method in a subclass will inherit the code of that same method of the subclass’ parent. If you’re confused, Stack Overflow’s got you covered.

Metz’s code

I’ve copied Metz’s final example of inheritance using RoadBike, MountainBike, and Bicycle classes. As you can see, many of the methods (initialize, spares, different defaults, etc.) are shared by the superclass, but some are unique to the subclasses.

(bicycle_inheritance.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Bicycle
attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size = args[:size]
    @chain = args[:chain] || default_chain
    @tire_size = args[:tire_size] || default_tire_size
    post_initialize(args)
  end

  def spares
    {  tire_size: tire_size,
       chain: chain}.merge(local_spares)
  end

  def default_tire_size
    raise NotImplementedError
  end

  def post_initialize(args)
    nil
  end

  def local_spares
    {}
  end

  def default_chain
    '10-speed'
  end

  end

  class RoadBike < Bicycle
    attr_reader :tape_color

  def post_initialize(args)
    @tape_color = args[:tape_color]
  end

  def local_spares
    {tape_color: tape_color}
  end

  def default_tire_size
    '23'
  end
end

class MountainBike < Bicycle
  attr_reader :front_shock, :rear_shock

  def post_initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
  end

  def local_spares
    {rear_shock: rear_shock}
  end

  def default_tire_size
    '2.1'
  end
end

This is a finished example, but Metz likes to go through what she calls “antipatterns”—common patterns that appear beneficial but are actually detrimental—to demonstrate a concept, so make sure to read the full text.