Inheritance
Learning Goals
- Identify inheritance as an essential principle of OOP
- Explain what is inheritance and why we use it
- Identify that you can over-write a method locally
- Write code that demonstrates the single inheritance principle
Vocabulary
- Inheritance: In software programming, inheritance is a mechanism where a new class, known as a child or subclass, is created based on an existing class, called a parent or superclass. The child class inherits properties and methods from the parent class, allowing for code reuse and the creation of a hierarchical relationship between classes.
- Parent/Superclass: The class that the child inherits properties and methods from.
- Child/Subclass: The class that inherits the pre-existing properties and methods from the parent class.
Warmup
In your notebook, jot down your thoughts for the following questions. Be ready to share.
- What do you think of when you hear the word inheritance?
- Where do you think we get the ability to call the method
attr_readerorassert_equal, etc? Where are they defined?
Inheritance
In software development, inheritance is one way that we can use code defined in one class in multiple classes. One of the benefits of inheritance is the reduction in duplication that comes from defining methods on a Parent class that can then be used across many child classes.
From Sandi Metz’s Practical Object Oriented Programming in Ruby, here is a more technical definition of inheritance:
Inheritance is, at its core, a mechanism for automatic message delegation. It defines a forwarding path for not-understood messages.
Here are a few rules of inheritance in programming:
- The objects modeled using inheritance must demonstrate a “generalization–specialization” relationship
- When a class inherits from another class, it receives access to all of the methods and instance variables from that other class
- The inheriting class is called the child or subclass
- The class being inherited from is called the parent or superclass
- A class can only inherit from one parent
- The chain of a parent’s parents (and parent’s parent’s parents, etc.) is called ancestors
- Any number of classes can inherit from a single superclass
Since inheritance is one of a few ways we can share code across classes, it’s convention to use it when there is a relationship between two things. For example, a dog is a mammal and a CEO is an employee.
Consider the Following:
Again from Metz, here is a practical application of inheritance:
Some of [a] bicycle’s behavior applies to all bicycles, some only to road bikes, and some only to mountain bikes. This single class contains several different, but related, types. This is the exact problem that inheritance solves; that of highly related types that share common behavior but differ along some dimension.
- With a partner, brainstorm what Metz might be talking about when she refers to some behavior applying to all bikes, and some behavior only applying to certain types of bikes. Feel free to check out this site for photos that might help you brainstorm.
Creating a Subclass
Demo
Take notes while we discuss, diagram, and demonstrate these concepts:
- the
superclassmethod - the missing method lookup chain
- difference between number of methods on instance and number of methods on instance’s parent
Group Practice
In a new directory, create a file called employee.rb that contains the following class:
# employee.rb
class Employee
def total_compensation
base_salary + bonus
end
end
Employee Explanation
In the Employee class, we have defined a method called total_compensation that returns the sum of base_salary and bonus.
We can inherit from it using the < character. Let’s try this in a file called ceo.rb:
# ceo.rb
require './employee'
class Ceo < Employee
attr_reader :base_salary,
:bonus
def initialize(base_salary, bonus)
@base_salary = base_salary
@bonus = bonus
end
end
Ceo Explanation
In the Ceo class, we have defined a method called total_compensation that returns the sum of base_salary and bonus. Note, the Ceo class inherits from the Employee class, so it has access to the total_compensation method defined in the Employee class.
Now try out the following in a separate file called runner.rb:
#runner.rb
require './ceo'
ali = Ceo.new(15, 20)
puts ali.total_compensation
Putting It All Together
You can see that even though we didn’t define total_compensation in our Ceo class, the Ceo object still responded to this methods because it inherited them from Employee. In this example, Ceo is the subclass or child, and Employee is the superclass or parent.
We can still define methods in our Ceo class just like with any other class.
NOTE:
It’s tempting to think of inheritance as methods getting “passed down” to a child (as you would when thinking about genetics in living creatures); however, technically, the method calls on the child object are “passed up” to the parent class. When a method is called on the child that only exists in the parent class, that message is automatically delegated to the parent class.
Try It!
With a partner:
- Create a
SalesManagerclass that inherits fromEmployee, and takesbase_salary, andestimated_annual_salesas arguments when you initialize. - Create a
bonusmethod onSalesManagerthat returns 10% ofestimated_annual_sales - Create a new
SalesManagerin your runner file and print their total compensation to the terminal
Super
Overview
When called inside a method, the keyword super calls the method from the superclass with the same name. For instance if you call super in the initialize method, it will call the superclass’s initialize method.
super is kind of a confusing word for this behavior. It might help to think of it as “calling the parent’s method”.
Let’s say we want every Employee to have a name and an id. First, we need to update our Employee superclass to take a name and id on instantiation:
#employee.rb
class Employee
attr_reader :name, :id
def initialize(name, id)
@name = name
@id = id
end
def total_compensation
base_salary + bonus
end
end
Now, we can update our Ceo to take a name and id as well. Rather than recreating the instance variable setup in the Ceo’s initialize, we can use super to fetch the additional setup we want from the Employee superclass:
#ceo.rb
require './employee'
class Ceo < Employee
attr_reader :base_salary,
:bonus
def initialize(base_salary, bonus, name, id)
@base_salary = base_salary
@bonus = bonus
super(name, id)
end
end
Understanding the Key Points
super calls the Employee class’s initialize method which sets the name and id instance variables.
Here, we specified the arguments to super. Note that there are actually three forms of the keyword super:
super(argument1, argument2)calls the superclass method with argument1 and argument2 specificallysupercalls the superclass method with all of the arguments in the current methodsuper()calls the superclass method with no arguments
Overriding
In Ruby we can also override a method from our parent class by re-defining it in our child class. Doing this implies that there is some exception to a general rule. A Mammal class might have a method lays_eggs? that returns false that would work on most child classes, but we would then need to override that method on Platypus to return true.
We’ll diagram this concept before moving on.
Look at an Intern class below. Let’s assume that at this company, interns are paid an hourly wage, but are not paid a bonus. Their compensation would no longer be calculated as base_salary + bonus. Instead, we can redefine that method on Intern, and override the method of the same name on Employee.
require './employee'
class Intern < Employee
attr_reader :hourly_rate
def initialize(hourly_rate, name, id)
@hourly_rate = hourly_rate
super(name, id)
end
def total_compensation
hourly_rate * 2000
end
end
We can also combine super and overriding. Imagine that you wanted to use a method from the superclass, but you needed to modify something about the return value. Take a look at the benefits method in both this parent class and the child class:
#employee.rb
class Employee
attr_reader :name, :id
def initialize(name, id)
@name = name
@id = id
end
def total_compensation
base_salary + bonus
end
def benefits
[:sick_leave]
end
end
#ceo.rb
require './employee'
class Ceo < Employee
attr_reader :base_salary,
:bonus
def initialize(base_salary, bonus, name, id)
@base_salary = base_salary
@bonus = bonus
super(name, id)
end
def benefits
super.push(:health_insurance)
end
end
Try It
Override a method and/or use super so that when you call #total_compensation on Ceo it adds a dollar to their base_salary before returning the total compensation.
Solution
#ceo.rb
require './employee'
class Ceo < Employee
attr_reader :base_salary,
:bonus
def initialize(base_salary, bonus, name, id)
@base_salary = base_salary
@bonus = bonus
super(name, id)
end
def benefits
super.push(:health_insurance)
end
def total_compensation
super + 1
end
end
Words of Caution
In the examples we did today, we created just two subclasses for a Employee superclass.
-
While it may be tempting to immediately create a parent class for 1-2 subclasses (for example:
Animalas the parent class andDogandCatas child classes), creating this hierarchy adds overhead. It may end up being less technically expensive to duplicate some functionality initially until you have a better sense of what the potential superclass of many subclasses (Mouse,Lizard,Coral, etc.) should do. Sometimes it is easier to promote the more abstract parts of the code UP to a superclass than it is to move concrete parts of a superclass down to a subclass. -
While subclasses can and should rely on functionality from the parent class, a parent class should not use methods that are not defined within the parent class. For example, our
Employeeclass relies onbase_salaryandbonus. In the event that later on, another child classManageris created, the parent class imposes thatbase_salaryandbonusmust be defined within the subclass since the parent class uses those to calculatetotal_compensation. In order to provide documentation for future developers using your parent class template, you can follow the pattern listed below, as recommended in Metz’s OOP book:
Checkout the revised Employee class below
#employee.rb
class Employee
attr_reader :name, :id
def initialize(name, id)
@name = name
@id = id
end
def total_compensation
base_salary + bonus
end
def base_salary
raise NotImplementedError, "This #{self.class} cannot respond to:"
end
def bonus
raise NotImplementedError, "This #{self.class} cannot respond to:"
end
end
Note that in base_salary and bonus, we are using self.class to refer to the class that called the method. This is important because it allows us to use the raise keyword to raise an error if the method is called on a subclass that does not have the necessary instance variables defined.
Conclusion
Although this lesson has focused on inheritance showing multiple Ruby examples, these concepts are applicable to many other programming languages as well including JavaScript. While the execution can look slightly different from language to language, the main idea is that you can take the methods and attributes of a superclass and use them in a subclass, which allows you to reuse code and create a hierarchy of classes.
Check for Understanding
- Why might we decide to use inheritance?
- Can you think of times that inheritance might not be the right choice?
- What is the syntax for creating a class that inherits from another class? How many classes can you inherit from and how do you decide which should inherit from which?
- What does
superdo, and what are the differences between the three different ways you can call it? - What does it mean to override a method in Ruby?
Additional Practice
To see additional examples and get some practice with inheritance, check out the Mod 1 Exercises repo. In the Inheritance Exercises directory, follow the instructions provided in the README.md file.
Optional Next Steps
- Check out Chapter 6 (pages 105 - 139) of Sandi Metz’s Practical Object Oriented Design in Ruby. Toward the end of the chapter, Metz talks about an alternative design pattern to using
superwhich is beyond the scope of today’s lesson but that you may find interesting. - Google “Composition vs. Inheritance in Ruby” for some more perspectives on when to use inheritance and when to include a module.