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
fromHawaiianPizza
apart from just its toppings? - How differently is a
PepperoniPizza
handled from aHawaiianPizza
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 ofPizza
, and later we announce thatCalzone
, a type of folded pizza, is also a subclass ofPizza
. 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