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
- How different is
HawaiianPizzaapart from just its toppings?
- How differently is a
PepperoniPizzahandled from a
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
PepperoniPizzaa 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
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!