In the previous chapter we talked briefly about inheritance. Inheritance is when a class inherits behavior from another class. The class that is inheriting behavior is called the subclass and the class it inherits from is called the superclass.
We use inheritance as a way to extract common behaviors from classes that share that behavior, and move it to a superclass. This lets us keep logic in one place. Let's take a look at an example.
Here, we're extracting the speak
method from the GoodDog
class to the superclass Animal
, and we use inheritance to make that behavior available to GoodDog
and Cat
classes.
class Animal
def speak
"Hello!"
end
end
class GoodDog < Animal
end
class Cat < Animal
end
sparky = GoodDog.new
paws = Cat.new
puts sparky.speak # => Hello!
puts paws.speak # => Hello!
We use the <
symbol to signify that the GoodDog
class is inheriting from the Animal
class. This means that all of the methods in the Animal
class are available to the GoodDog
class for use. We also created a new class called Cat
that inherits from Animal
as well. We've eliminated the speak
method from the GoodDog
class in order to use the speak
method from Animal
.
When we run this code we see the correct output. Both classes are now using the superclass Animal
's speak
method.
But what if we want to use the original speak
method from the GoodDog
class only. Let's add it back and see what happens.
class Animal
def speak
"Hello!"
end
end
class GoodDog < Animal
attr_accessor :name
def initialize(n)
self.name = n
end
def speak
"#{self.name} says arf!"
end
end
class Cat < Animal
end
sparky = GoodDog.new("Sparky")
paws = Cat.new
puts sparky.speak # => Sparky says arf!
puts paws.speak # => Hello!
In the GoodDog
class, we're overriding the speak
method in the Animal
class because Ruby checks the object's class first for the method before it looks in the superclass. That means when we wrote the code sparky.speak
, it first looked at sparky
's class, which is GoodDog
. It found the speak
method there and used it. When we wrote the code paws.speak
, Ruby first looked at paws
's class, which is Cat
. It did not find a speak
method there, so it continued to look in Cat
's superclass, Animal
. It found a speak
method in Animal
, and used it. We'll talk about this method lookup path more in depth in a bit.
Inheritance can be a great way to remove duplication in your code base. There is an acronym that you'll see often in the Ruby community, "DRY". This stands for "Don't Repeat Yourself". It means that if you find yourself writing the same logic over and over again in your programs, there are ways to extract that logic to one place for reuse.
Ruby provides us with the super
keyword to call methods earlier in the method lookup path. When you call super
from within a method, it searches the method lookup path for a method with the same name, then invokes it. Let's see a quick example of how this works:
class Animal
def speak
"Hello!"
end
end
class GoodDog < Animal
def speak
super + " from GoodDog class"
end
end
sparky = GoodDog.new
sparky.speak # => "Hello! from GoodDog class"
In the above example, we've created a simple Animal
class with a speak
instance method. We then created GoodDog
which subclasses Animal
also with a speak
instance method to override the inherited version. However, in the subclass' speak
method we use super
to invoke the speak
method from the superclass, Animal
, and then we extend the functionality by appending some text to the return value.
Another more common way of using super
is with initialize
. Let's see an illustration of that:
class Animal
attr_accessor :name
def initialize(name)
@name = name
end
end
class GoodDog < Animal
def initialize(color)
super
@color = color
end
end
bruno = GoodDog.new("brown") # => #<GoodDog:0x007fb40b1e6718 @color="brown", @name="brown">
The interesting concept we want to explain is the use of super
in the GoodDog
class. In this example, we're using super
with no arguments. However, the initialize
method, where super
is being used, takes an argument and adds a new twist to how super
is invoked. Here, in addition to the default behavior, super
automatically forwards the arguments that were passed to the method from which super
is called (initialize
method in GoodDog
class). At this point, super
will pass the color
argument in the initialize
defined in the subclass to that of the Animal
superclass and invoke it. That explains the presence of @name="brown"
when the bruno
instance is created. Finally, the subclass' initialize
continues to set the @color
instance variable.
When called with specific arguments, eg. super(a, b)
, the specified arguments will be sent up the method lookup chain. Let's see a quick example:
class BadDog < Animal
def initialize(age, name)
super(name)
@age = age
end
end
BadDog.new(2, "bear") # => #<BadDog:0x007fb40b2beb68 @age=2, @name="bear">
This is similar to our previous example, with the difference being that super
takes an argument, hence the passed in argument is sent to the superclass. Consequently, in this example when a BadDog
object is created, the passed in name
argument ("bear") is passed to the superclass and set to the @name
instance variable.
There's one last twist. If you call super()
exactly as shown -- with parentheses -- it calls the method in the superclass with no arguments at all. If you have a method in your superclass that takes no arguments, this is the safest -- and sometimes the only -- way to call it:
class Animal
def initialize
end
end
class Bear < Animal
def initialize(color)
super()
@color = color
end
end
bear = Bear.new("black") # => #<Bear:0x007fb40b1e6718 @color="black">
If you forget to use the parentheses here, Ruby will raise an ArgumentError
exception since the number of arguments is incorrect.
Another way to DRY up your code in Ruby is to use modules. We've already seen a little bit of how to use modules, but we'll give a few more examples here.
Extracting common methods to a superclass, like we did in the previous section, is a great way to model concepts that are naturally hierarchical. We gave the example of animals. We have a generic superclass called Animal
that can keep all basic behavior of all animals. We can then expand on the model a little and have, perhaps, a Mammal
subclass of Animal
. We can imagine the entire class hierarchy to look something like the figure below.
The above diagram shows what pure class based inheritance looks like. Remember the goal of this is to put the right behavior (i.e., methods) in the right class so we don't need to repeat code in multiple classes. We can imagine that all Fish
objects are related to animals that live in the water, so perhaps a swim
method should be in the Fish
class. We can also imagine that all Mammal
objects will have warm blood, so we can create a method called warm_blooded?
in the Mammal
class and have it return true
. Therefore, the Cat
and Dog
objects will have access to the warm_blooded?
method which is automatically inherited from Mammal
by the Cat
and Dog
classes, but they won't have access to the methods in the Fish
class.
This type of hierarchical modeling works, to some extent, but there are always exceptions. For example, we put the swim
method in the Fish
class, but some mammals can swim as well. We don't want to move the swim
method into Animal
because not all animals swim, and we don't want to create another swim
method in Dog
because that violates the DRY principle. For concerns such as these, we'd like to group them into a module and then mix in that module to the classes that require those behaviors. Here's an example:
module Swimmable
def swim
"I'm swimming!"
end
end
class Animal; end
class Fish < Animal
include Swimmable # mixing in Swimmable module
end
class Mammal < Animal
end
class Cat < Mammal
end
class Dog < Mammal
include Swimmable # mixing in Swimmable module
end
And now Fish
and Dog
objects can swim, but objects of other classes won't be able to:
sparky = Dog.new
neemo = Fish.new
paws = Cat.new
sparky.swim # => I'm swimming!
neemo.swim # => I'm swimming!
paws.swim # => NoMethodError: undefined method `swim' for #<Cat:0x007fc453152308>
Using modules to group common behaviors allows us to build a more powerful, flexible and DRY design.
Note: A common naming convention for Ruby is to use the "able" suffix on whatever verb describes the behavior that the module is modeling. You can see this convention with our Swimmable
module. Likewise, we could name a module that describes "walking" as Walkable
. Not all modules are named in this manner, however, it is quite common.
Now you know the two primary ways that Ruby implements inheritance. Class inheritance is the traditional way to think about inheritance: one type inherits the behaviors of another type. The result is a new type that specializes the type of the superclass. The other form is sometimes called interface inheritance: this is where mixin modules come into play. The class doesn't inherit from another type, but instead inherits the interface provided by the mixin module. In this case, the result type is not a specialized type with respect to the module.
You may wonder when to use class inheritance vs mixins. Here are a couple of things to consider when evaluating these choices.
B
can be described as a kind of class A
, then we say that B
and A
have an is-a relationship. if such a relationship exists, then we probably want to use class inheritance such that class B
inherits from class A
.
B
and A
do not have an is-a relationship, there may be a has-a relationship involved. We saw has-a relationships in conjunction with composition and aggregation, but such relationships also exist when interface existence is desired. If you can say that class A
has the behaviors of type B
, but B
and A
don't have an is-a relationship, then you probably want to define B
as a module and use interface inheritance.
As you get better at OO design, you'll start to develop a feel for when to use class inheritance versus mixing in modules.
Now that you have a grasp on both inheritance and mixins, let's put them both together to see how that affects the method lookup path. Recall the method lookup path is the order in which classes are inspected when you call a method. Let's take a look at the example code below.
module Walkable
def walk
"I'm walking."
end
end
module Swimmable
def swim
"I'm swimming."
end
end
module Climbable
def climb
"I'm climbing."
end
end
class Animal
include Walkable
def speak
"I'm an animal, and I speak!"
end
end
We have three modules and one class. We've mixed in one module into the Animal
class. The method lookup path is the path Ruby takes to look for a method. We can see this path with the ancestors
class method.
puts "---Animal method lookup---"
puts Animal.ancestors
The output looks like this:
---Animal method lookup---
Animal
Walkable
Object
Kernel
BasicObject
This means that when we call a method of any Animal
object, first Ruby looks in the Animal
class, then the Walkable
module, then the Object
class, then the Kernel
module, and finally the BasicObject
class.
fido = Animal.new
fido.speak # => I'm an animal, and I speak!
Ruby found the speak
method in the Animal
class and looked no further.
fido.walk # => I'm walking.
Ruby first looked for the walk
instance method in Animal
, and not finding it there, kept looking in the next place according to our list, which is the Walkable
module. It saw a walk
method there, executed it, and stopped looking further.
fido.swim
# => NoMethodError: undefined method `swim' for #<Animal:0x007f92832625b0>
Ruby traversed all the classes and modules in the list, and didn't find a swim
method, so it threw an error.
Let's add another class to the code above. This class will inherit from the Animal
class and mix in the Swimmable
and Climbable
modules.
class GoodDog < Animal
include Swimmable
include Climbable
end
puts "---GoodDog method lookup---"
puts GoodDog.ancestors
And this is the output we get:
---GoodDog method lookup---
GoodDog
Climbable
Swimmable
Animal
Walkable
Object
Kernel
BasicObject
There are several interesting things about the above output. First, this tells us that the order in which we include modules is important. Ruby actually looks at the last module we included first. This means that in the rare occurrence that the modules we mix in contain a method with the same name, the last module included will be consulted first. The second interesting thing is that the module included in the superclass made it on to the method lookup path. That means that all GoodDog
objects will have access to not only Animal
methods, but also methods defined in the Walkable
module, as well as all other modules mixed in to any of its superclasses.
Sometimes when you're working on a large project, it can be confusing where all these methods are coming from. By understanding the method lookup path, we can have a better idea of where and how all available methods are organized.
We've already seen how modules can be used to mix-in common behavior into classes. Now we'll see two more uses for modules.
The first use case we'll discuss is using modules for namespacing. In this context, namespacing means organizing similar classes under a module. In other words, we'll use modules to group related classes. Therein lies the first advantage of using modules for namespacing. It becomes easy for us to recognize related classes in our code. The second advantage is it reduces the likelihood of our classes colliding with other similarly named classes in our codebase. Here's how we do it:
module Mammal
class Dog
def speak(sound)
p "#{sound}"
end
end
class Cat
def say_name(name)
p "#{name}"
end
end
end
We call classes in a module by appending the class name to the module name with two colons(::
)
buddy = Mammal::Dog.new
kitty = Mammal::Cat.new
buddy.speak('Arf!') # => "Arf!"
kitty.say_name('kitty') # => "kitty"
The second use case for modules we'll look at is using modules as a container for methods, called module methods. This involves using modules to house other methods. This is very useful for methods that seem out of place within your code. Let's use a new module to demonstrate this:
module Conversions
def self.farenheit_to_celsius(num)
(num - 32) * 5 / 9
end
end
Defining methods this way within a module means we can call them directly from the module:
value = Conversions.farenheit_to_celsius(32)
We can also call such methods by doing:
value = Conversions::farenheit_to_celsius(32)
although the former is the preferred way.
The last thing we want to cover is something that's actually quite simple, but necessary; Method Access Control. Access Control is a concept that exists in a number of programming languages, including Ruby. It is generally implemented through the use of access modifiers. The purpose of access modifiers is to allow or restrict access to a particular thing. In Ruby, the things that we are concerned with restricting access to are the methods defined in a class. In a Ruby context, therefore, you'll commonly see this concept referred to as Method Access Control.
The way that Method Access Control is implemented in Ruby is through the use of the public
, private
, and protected
access modifiers. Right now, all the methods in our GoodDog
class are public methods. A public method is a method that is available to anyone who knows either the class name or the object's name. These methods are readily available for the rest of the program to use and comprise the class's interface (that's how other classes and objects will interact with this class and its objects).
Sometimes you'll have methods that are doing work in the class but don't need to be available to the rest of the program. These methods can be defined as private. How do we define private methods? We use the private
method call in our program and anything below it is private (unless another method, like protected
, is called after it to negate it).
In our GoodDog
class we have one operation that takes place that we could move into a private method. When we initialize an object, we calculate the dog's age in Dog years. Let's refactor this logic into a method and make it private so nothing outside of the class can use it.
class GoodDog
DOG_YEARS = 7
attr_accessor :name, :age
def initialize(n, a)
self.name = n
self.age = a
end
private
def human_years
age * DOG_YEARS
end
end
sparky = GoodDog.new("Sparky", 4)
sparky.human_years
We get the error message:
NoMethodError: private method `human_years' called for
#<GoodDog:0x007f8f431441f8 @name="Sparky", @age=4>
We have made the human_years
method private by placing it under the private
method. What is it good for, then, if we can't call it? private
methods are only accessible from other methods in the class. For example, given the above code, the following would be allowed:
# assume the method definition below is above the "private" method
def public_disclosure
"#{self.name} in human years is #{human_years}"
end
Note that in this case, we can not use self.human_years
, because the human_years
method is private. Remember that self.human_years
is equivalent to sparky.human_years
, which is not allowed for private methods. Therefore, we have to just use human_years
. In summary, private methods are not accessible outside of the class definition at all, and are only accessible from inside the class when called without self
.
As of Ruby 2.7, it is now legal to call private methods with a literal self
as the caller. Note that this does not mean that we can call a private method with any other object, not even one of the same type. We can only call a private method with the current object.
Public and private methods are most common, but in some less common situations, we'll want an in-between approach.
For this, we can use the protected
method to create protected methods.
Protected methods are similar to private methods in that they cannot be invoked outside the class.
The main difference between them is that protected methods allow access between class instances, while private methods do not.
Let's take a look at an example:
class Person
def initialize(age)
@age = age
end
def older?(other_person)
age > other_person.age
end
protected
attr_reader :age
end
malory = Person.new(64)
sterling = Person.new(42)
malory.older?(sterling) # => true
sterling.older?(malory) # => false
malory.age
# => NoMethodError: protected method `age' called for #<Person: @age=64>
The above code shows us that like private methods, protected methods cannot be invoked from outside of the class. However, unlike private methods, other instances of the class (or subclass) can also invoke the method. This allows for controlled access, but wider access between objects of the same class type.
It’s important to remember that every class you create inherently subclasses from class Object. The Object
class is built into Ruby and comes with many critical methods.
class Parent
def say_hi
p "Hi from Parent."
end
end
Parent.superclass # => Object
This means that methods defined in the Object
class are available in all classes.
Further, recall that through the magic of inheritance, a subclass can override a superclass’s method.
class Child < Parent
def say_hi
p "Hi from Child."
end
end
child = Child.new
child.say_hi # => "Hi from Child."
This means that, if you accidentally override a method that was originally defined in the Object
class, it can have far-reaching effects on your code. For example, send
is an instance method that all classes inherit from Object
. If you defined a new send
instance method in your class, all objects of your class will call your custom send
method, instead of the one in class Object
, which is probably the one they mean to call. Object send
serves as a way to call a method by passing it a symbol or a string which represents the method you want to call. The next couple of arguments will represent the method's arguments, if any. Let's see how send normally works by making use of our Child
class:
son = Child.new
son.send :say_hi # => "Hi from Child."
Let's see what happens when we define a send
method in our Child
class and then try to invoke Object
's send
method:
class Child
def say_hi
p "Hi from Child."
end
def send
p "send from Child..."
end
end
lad = Child.new
lad.send :say_hi
Normally we would expect the output of this call to be "Hi from Child."
but upon running the code we get a completely different result:
ArgumentError: wrong number of arguments (1 for 0)
from (pry):12:in `send'
In our example, we're passing send
one argument even though our overridden send
method does not take any arguments. Let's take a look at another example by exploring Object's instance_of?
method. What this handy method does is to return true
if an object is an instance of a given class and false
otherwise. Let's see it in action:
c = Child.new
c.instance_of? Child # => true
c.instance_of? Parent # => false
Now let's override instance_of?
within Child
:
class Child
# other methods omitted
def instance_of?
p "I am a fake instance"
end
end
heir = Child.new
heir.instance_of? Child
Again, we'll see something completely different though our intention was to use Object's instance_of?
method:
ArgumentError: wrong number of arguments (1 for 0)
from (pry):22:in `instance_of?'
That said, one Object
instance method that's easily overridden without any major side-effect is the to_s
method. You'll normally want to do this when you want a different string representation of an object. Overall, it’s important to familiarize yourself with some of the common Object
methods and make sure to not accidentally override them as this can have devastating consequences for your application.
We've covered quite a bit of ground now. You should be feeling pretty comfortable with the general syntax and structure of the Ruby language. You've got one more set of exercises to help put this information to good use, then you'll be ready to take the next step in your journey as a Ruby developer.
All this complex knowledge about OOP is meant to help us build better designed applications. While there are definitely wrong ways to design an application, there is often no right choice when it comes to object oriented design, only different tradeoffs. As you gain more experience in object oriented design, you'll start to develop a taste for how to organize and shape your classes. For now, all this may feel a little daunting, but once you learn how to think in an OO way, it's hard to not think in that manner.
Finally, make sure to take time to go through the exercises. OOP is a tough concept if this is your first time encountering it. Even if you've programmed in another OO language before, Ruby's implementation may be a little different. It's not enough to read and understand; you must learn by doing. Let's get on to the exercises!
Create a superclass called Vehicle
for your MyCar
class to inherit from and move the behavior that isn't specific to the MyCar
class to the superclass. Create a constant in your MyCar
class that stores information about the vehicle that makes it different from other types of Vehicles.
Then create a new class called MyTruck that inherits from your superclass that also has a constant defined that separates it from the MyCar class in some way.
class Vehicle
def self.gas_mileage(gallons, miles)
puts "#{miles / gallons} miles per gallon of gas"
end
end
class MyCar < Vehicle
NUMBER_OF_DOORS = 4
end
class MyTruck < Vehicle
NUMBER_OF_DOORS = 2
end
Video Walkthrough
Add a class variable to your superclass that can keep track of the number of objects created that inherit from the superclass. Create a method to print out the value of this class variable as well.
class Vehicle
@@number_of_vehicles = 0
def self.number_of_vehicles
puts "This program has created #{@@number_of_vehicles} vehicles"
end
def self.gas_mileage(gallons, miles)
puts "#{miles / gallons} miles per gallon of gas"
end
def initialize
@@number_of_vehicles += 1
end
end
class MyCar < Vehicle
NUMBER_OF_DOORS = 4
#code omitted for brevity...
end
class MyTruck < Vehicle
NUMBER_OF_DOORS = 2
end
Video Walkthrough
Create a module that you can mix in to ONE of your subclasses that describes a behavior unique to that subclass.
module Towable
def can_tow?(pounds)
pounds < 2000
end
end
class Vehicle
@@number_of_vehicles = 0
def self.number_of_vehicles
puts "This program has created #{@@number_of_vehicles} vehicles"
end
def self.gas_mileage(gallons, miles)
puts "#{miles / gallons} miles per gallon of gas"
end
def initialize
@@number_of_vehicles += 1
end
end
class MyCar < Vehicle
NUMBER_OF_DOORS = 4
#code omitted for brevity...
end
class MyTruck < Vehicle
include Towable
NUMBER_OF_DOORS = 2
end
Video Walkthrough
Errata:
Towable#can_tow?
method.
This isn't necessary as the comparison itself (ie, pounds < 2000
) returns a boolean.
Print to the screen your method lookup for the classes that you have created.
# code omitted for brevity...
puts MyCar.ancestors
puts MyTruck.ancestors
puts Vehicle.ancestors
Video Walkthrough
Move all of the methods from the MyCar class that also pertain to the MyTruck class into the Vehicle class. Make sure that all of your previous method calls are working when you are finished.
module Towable
def can_tow?(pounds)
pounds < 2000
end
end
class Vehicle
attr_accessor :color
attr_reader :model, :year
@@number_of_vehicles = 0
def self.number_of_vehicles
puts "This program has created #{@@number_of_vehicles} vehicles"
end
def self.gas_mileage(gallons, miles)
puts "#{miles / gallons} miles per gallon of gas"
end
def initialize(year, model, color)
@year = year
@model = model
@color = color
@current_speed = 0
@@number_of_vehicles += 1
end
def speed_up(number)
@current_speed += number
puts "You push the gas and accelerate #{number} mph."
end
def brake(number)
@current_speed -= number
puts "You push the brake and decelerate #{number} mph."
end
def current_speed
puts "You are now going #{@current_speed} mph."
end
def shut_down
@current_speed = 0
puts "Let's park this bad boy!"
end
def spray_paint(color)
self.color = color
puts "Your new #{color} paint job looks great!"
end
end
class MyTruck < Vehicle
include Towable
NUMBER_OF_DOORS = 2
def to_s
"My truck is a #{self.color}, #{self.year}, #{self.model}!"
end
end
class MyCar < Vehicle
NUMBER_OF_DOORS = 4
def to_s
"My car is a #{self.color}, #{self.year}, #{self.model}!"
end
end
lumina = MyCar.new(1997, 'chevy lumina', 'white')
lumina.speed_up(20)
lumina.current_speed
lumina.speed_up(20)
lumina.current_speed
lumina.brake(20)
lumina.current_speed
lumina.brake(20)
lumina.current_speed
lumina.shut_down
MyCar.gas_mileage(13, 351)
lumina.spray_paint("red")
puts lumina
puts MyCar.ancestors
puts MyTruck.ancestors
puts Vehicle.ancestors
Video Walkthrough
Write a method called age
that calls a private method to calculate the age of the vehicle. Make sure the private method is not available from outside of the class. You'll need to use Ruby's built-in Time class to help.
class Vehicle
# code omitted for brevity...
def age
"Your #{self.model} is #{years_old} years old."
end
private
def years_old
Time.now.year - self.year
end
end
# code omitted for brevity...
puts lumina.age #=> "Your chevy lumina is 17 years old"
Video Walkthrough
Create a class 'Student' with attributes name
and grade
. Do NOT make the grade getter public, so joe.grade
will raise an error. Create a better_grade_than?
method, that you can call like so...
puts "Well done!" if joe.better_grade_than?(bob)
class Student
def initialize(name, grade)
@name = name
@grade = grade
end
def better_grade_than?(other_student)
grade > other_student.grade
end
protected
def grade
@grade
end
end
joe = Student.new("Joe", 90)
bob = Student.new("Bob", 84)
puts "Well done!" if joe.better_grade_than?(bob)
Video Walkthrough
Given the following code...
bob = Person.new
bob.hi
And the corresponding error message...
NoMethodError: private method `hi' called for #<Person:0x007ff61dbb79f0>
from (irb):8
from /usr/local/rvm/rubies/ruby-2.0.0-rc2/bin/irb:16:in `<main>'
What is the problem and how would you go about fixing it?
The problem is that the method hi
is a private method, therefore it is unavailable to the object. I would fix this problem by moving the hi
method above the private
method call in the class.
Video Walkthrough