Don't Abuse Inheritance

December 23, 2020

/images/dont-abuse-inheritance.jpg

Who is this for?

You have a working understanding of OOP like classes and methods but can't seem to use it to structure it in a way to make sense.

You can read a bit of Ruby (it's really easy! Reads like English.)

How does inheritance work?

Without going too deep, inheritance allows us to specify a hierarchy of classes. When a child inherits from a parent, it acquires all the properties and behaviours of the parent. One such benefit is allowing us to create classes that are built upon other existing classes. Very quickly, in code it looks something like this:

class ParentClass
  def hello
    puts 'hi there!'
  end
end

class ChildClass < ParentClass
end

class GrandChildClass < ChildClass
end

# > grandchild = GrandChildClass.new
# > grandchild.hello ---> 'hi there!'

Modelling a Pizza

Let's imagine we run a Pizzeria. A Pizza class can be defined like so

class Pizza
  attr_accessor :toppings

  def initialize(toppings)
    @toppings = toppings
  end
end

What if you wanted to introduce a different pizza, say a pepperoni pizza?

Using Inheritance wrongly

Some of us may choose to do something like this:

class PepperoniPizza < Pizza
  attr_accessor :toppings

  def initialize;	end
end

class PepperoniPizza < Pizza
  def initialize
    super
    @toppings = [:pepperoni]
  end
end

It is not exactly wrong, since PepperoniPizza is technically derived from a Pizza, but let's consider the finer points and see if it makes sense:

  • What happens if you have more Pizzas on the menu? Will you go on to create HawaiianPizza, MargheritaPizza, etc?
  • How different is PepperoniPizza from HawaiianPizza apart from just its toppings?
  • How differently is a PepperoniPizza handled from a HawaiianPizza later downstream?

Consider the behaviour, responsibilities and how other objects perceive it

The trick to simplifying the problem is seeing how differently the siblings behave and interact with other objects around itself.

All pizzas are typically handled the same way and also behave the same way, only differing in their toppings and name. Therefore, It would make more sense to initialise the pizzas differently with their respective names and topping.

class Pizza
  attr_accessor :toppings

  def initialize(name, toppings)
    @name = name
    @toppings = toppings
  end
end

pepperoni_pizza = Pizza.new('pepperoni', [:pepperoni])

Now it reads "a pizza whose name is 'pepperoni' and has many toppings that are pepperoni".

The "is"s and "has"s

English gives us a clue to what tool to use. Attributes describe what "is" about the pizza. In this case the pizza's name is 'pepperoni'. Suppose if we introduced a new square pizza on the menu. All we had to do is to add a new attribute to the Pizza class (assuming square pizzas still handled the same way):

class Pizza
  attr_accessor :toppings

  def initialize(name, toppings, shape)
    @name = name
    @toppings = toppings
    @shape = shape
  end
end

square_pepperoni_pizza = Pizza.new('pepperoni', [:pepperoni], :square)

Composition on the other hand describes what an object "has". A pizza has toppings. A duck has a beak. A car has wheels. It is important to distinguish attributes from composition because attributes are innate to the object, while composition allows it to be made up of other items.

It would be strange to say "the pizza has large". A large what? Likewise, it would be strange to say "the pizza's toppings is" (in this case, you can only use is as you are substituting the name of the would be attribute).

There is more than one way to abuse inheritance

Some of us may propose that a Pizza can be modelled as a subclass of a Hash, after all it can be modelled as an object.

class Pizza < Hash		
  # ... pizza methods
end

This is a big NO in my opinion because it mixes concerns between the business and implementation domain. Your application classes should be composed of implementation classes, not be one of it.

  • Just because you can do it, doesn't mean you should do it.

Problems that come with inheritance

Some commonly documented problems include

  • Deeply nested inheritance: When abused by trigger happy developers, a 4-6 level hierarchy is difficult to debug and trace.
  • Static design: Inheritance is effectively defined in code, so there is no way to dynamically change properties and interfaces of a class by changing its parent class at runtime.
  • Coupling of implementation and method interfaces with parent class: While inheritance helps with reuse of code, it also forces the child class to inherit all the methods and attributes of the parent class. This also implies that the child must be a strict superset of the parent class, or the design quickly breaks down. Imagine we called PepperoniPizza a subclass of Pizza , and later we announce that Calzone, a type of folded pizza, is also a subclass of Pizza. Depending on how complex the earlier classes were, adding a new class will be a nightmare because of the interaction with prior designs. This causes your design to be fragile to change.

When to use inheritance

Inheritance encourages the reuse of interfaces. In the case of Ruby, this means the reuse of methods between parent and child classes.

One clear use case is when inheriting from abstract classes,

class Shape
  attr_accessors :points
  def draw
    # Ruby doesn't support abstract classes
    raise 'Implement in subclass!'
  end
end

class Circle < Shape
  # has :points inherited from Shape
  def draw
    # draw a circle
  end
end

Even so, Ruby specifically allows us to define a module as a mixin, effectively mixing in behaviours from a module to share common methods.

module Shapeable
  def draw
    raise 'Must implement!'
  end
end

class Circle
  attr_accessors :point
  attr_accessors :radius

  include Shapeable

  def draw
    # draw a circle
  end
end

The Shapeable module enforces that the draw method must implemented in the Circle class. However, it also frees Circle from how it wishes to implement draw, and the internal representations it needs to fulfil the end goal.

Modules can be used in more creative ways to impart even more behaviours in to a class, but that is a topic for another long blog post.

Favour composition over inheritance

As we have seen, specifically when it comes to Ruby, there isn't really a lot of reason to use inheritance. In fact, during my research for this article, I also discovered that Java and C++ folks have shifted away from inheritance as a design tool, favouring composition over inheritance.

Composition is underrated and misunderstood; yet when used properly as a design tool it can make your classes more open to changes. More on this on my next article!

Additional reading

https://ruby-doc.com/docs/ProgrammingRuby/html/tut_modules.html

https://www.tedinski.com/2018/02/13/inheritance-modularity.html

Like my content? Subscribe to receive tips on software engineering.
© Alvin Ng