Don't Abuse Inheritance

Software Engineering Series #2
In “Software Engineering Series”, I cover and discuss specific techniques and methods that are commonly used in large code bases. Software is a rapidly evolving field, and new ideas and techniques will always re-emerge over the years. This is by no means an exhaustive discussion. Opinions are welcomed, even more so when accompanied with actual experience.

Wed, Dec 23, 2020

Read in 6 minutes

When first starting out with object-oriented programming (OOP), it is easy to abuse its many language features; one such feature is inheritance.

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:

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.

Problems that come with inheritance

Some commonly documented problems include

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

Learning to write software? Subscribe now to receive tips on software engineering.

Back to home page