Inheritance
This chapter introduces inheritance, the mechanism that allows a class to take everything another class defines and add its own specifics on top.
Principle
We created a Pokemon class in chapter 8. But not all Pokemon are the same: a wild Pokemon has a spawn area and a catch rate, a trainer's Pokemon has an owner and possibly a nickname.
We could copy-paste the Pokemon class and modify it for each case, but that would be wasteful. If we fix a bug in Pokemon, we would have to fix it in all the copies.
Inheritance solves this problem. We create a child class that inherits from the parent class. The child automatically receives all methods and attributes from the parent, and can add or modify them.
We say that WildPokemon is a Pokemon with additional properties. It is the "is a" relationship that guides inheritance.
Inheriting from a class
The syntax is class Child < Parent:
class Pokemon
attr_reader :name, :level
def initialize(name, level)
@name = name
@level = level
end
def to_s
return "#{@name} Lvl.#{@level}"
end
def cry
return "#{@name} !"
end
end
class WildPokemon < Pokemon
attr_reader :area
def initialize(name, level, area)
super(name, level)
@area = area
end
end
wild = WildPokemon.new('Rattata', 3, 'Route 1')
puts wild # => Rattata Lvl.3
puts wild.name # => Rattata
puts wild.area # => Route 1
puts wild.cry # => Rattata !
class WildPokemon < Pokemon:WildPokemoninherits fromPokemon. It receivesname,level,to_s,cry— everything thatPokemondefines.super(name, level)ininitializecalls the parent's constructor. Without this call,@nameand@levelwould not be initialized.@areais an attribute specific toWildPokemon. The parentPokemondoes not know about it.
super — calling the parent's method
super is the keyword that calls the parent's method with the same name. There are three forms:
class Pokemon
def initialize(name, level)
@name = name
@level = level
end
end
class WildPokemon < Pokemon
def initialize(name, level, area)
# super with arguments: passes exactly these arguments to the parent
super(name, level)
@area = area
end
end
The three forms of super:
super(name, level): passes exactly the specified arguments. This is the most common and most explicit form.super(without parentheses): passes all the arguments received by the current method, as-is. Handy when the child receives the same arguments as the parent.super()(empty parentheses): passes no arguments. Useful when the parent expects nothing.
Beware: confusing super and super() is a common source of bugs. If the parent expects arguments and you write super(), Ruby will raise an error.
Overriding a method
The child can redefine a parent method to change its behavior:
class Pokemon
attr_reader :name, :level
def initialize(name, level)
@name = name
@level = level
end
def to_s
return "#{@name} Lvl.#{@level}"
end
def cry
return "#{@name} !"
end
end
class WildPokemon < Pokemon
attr_reader :area
def initialize(name, level, area)
super(name, level)
@area = area
end
# Override: enriches the parent's display
def to_s
return "#{super} [Wild - #{@area}]"
end
# Override: completely replaces the parent's behavior
def cry
return "A wild #{@name} appears!"
end
end
wild = WildPokemon.new('Rattata', 3, 'Route 1')
puts wild # => Rattata Lvl.3 [Wild - Route 1]
puts wild.cry # => A wild Rattata appears!
to_scallssuperto retrieve the parent's display ("Rattata Lvl.3") and adds information to it. This is override by enrichment.crydoes not callsuper. The parent's behavior is entirely replaced. This is override by replacement.
You choose one or the other depending on the need. Enrichment is more common because it avoids duplicating the parent's logic.
A second child
class TrainerPokemon < Pokemon
attr_reader :trainer_name
def initialize(name, level, trainer_name)
super(name, level)
@trainer_name = trainer_name
end
def to_s
return "#{super} [Trainer: #{@trainer_name}]"
end
def cry
return "#{@trainer_name} sends out #{@name}!"
end
end
trainer_pokemon = TrainerPokemon.new('Charizard', 36, 'Red')
puts trainer_pokemon # => Charizard Lvl.36 [Trainer: Red]
puts trainer_pokemon.cry # => Red sends out Charizard!
TrainerPokemonandWildPokemonboth inherit fromPokemonbut add different behaviors. This is polymorphism: the same call (cry) produces a different result depending on the object's class.
Chain inheritance
A child class can itself be the parent of another class:
class LegendaryPokemon < WildPokemon
attr_reader :signature_move
def initialize(name, level, area, signature_move)
super(name, level, area)
@signature_move = signature_move
end
def to_s
return "#{super} | Signature move: #{@signature_move}"
end
end
mewtwo = LegendaryPokemon.new('Mewtwo', 70, 'Azure Cave', 'Psychic Strike')
puts mewtwo
# => Mewtwo Lvl.70 [Wild - Azure Cave] | Signature move: Psychic Strike
LegendaryPokemoninherits fromWildPokemon, which inherits fromPokemon. Thesuperchain goes up automatically.to_scallssuperwhich callsWildPokemon'sto_s, which itself callsPokemon'sto_s. Each level adds its own information.
Inspecting the hierarchy
Ruby offers several tools to examine relationships between classes:
mewtwo = LegendaryPokemon.new('Mewtwo', 70, 'Azure Cave', 'Psychic Strike')
# is_a? checks the class AND all its ancestors
puts mewtwo.is_a?(LegendaryPokemon) # => true
puts mewtwo.is_a?(WildPokemon) # => true
puts mewtwo.is_a?(Pokemon) # => true
puts mewtwo.is_a?(TrainerPokemon) # => false
# instance_of? checks ONLY the exact class
puts mewtwo.instance_of?(LegendaryPokemon) # => true
puts mewtwo.instance_of?(Pokemon) # => false
# Hierarchy inspection
puts mewtwo.class # => LegendaryPokemon
puts LegendaryPokemon.superclass # => WildPokemon
puts WildPokemon.superclass # => Pokemon
p LegendaryPokemon.ancestors
# => [LegendaryPokemon, WildPokemon, Pokemon, Object, ...]
.is_a?is the most used: it checks if the object is an instance of the class or any of its ancestors..instance_of?is strict: only the exact class..superclassreturns the direct parent of a class. It is called on the class, not on an object..ancestorsreturns the full hierarchy chain. It is a useful tool to understand in what order Ruby looks for methods.
Inheritance vs composition
Inheritance is not always the right tool. The rule is simple:
- Inheritance when the relationship is "is a": a
WildPokemonis aPokemon. - Composition when the relationship is "has a" or "uses a": a
Pokemonhas attacks, a team uses an Array of Pokemon. In these cases, you store the object in an instance variable instead of inheriting from it.
When in doubt, composition is often the more flexible choice. We will see composition in detail with modules in chapter 10.
Conclusion
class Child < Parentcreates a class that inherits everything the parent defines.super(arguments)calls the parent's method. Always callsuperininitialize.- Overriding a method = redefining it in the child. You can call
superto enrich the parent's behavior, or not call it to replace it entirely. .is_a?checks the class and its ancestors..instance_of?checks only the exact class..superclassreturns the direct parent of a class..ancestorsshows the full method lookup chain.- Use inheritance for "is a" relationships. Prefer composition for "has a" or "uses a" relationships.