Putting the "L" in SOLID
In 1987, while I was busy dropping quarters in Contra, Barbara Liskov was dropping her keynote address called Data Abstraction and Hierarchy. Her address contained concepts that have become known as the Liskov Substitution Principle (LSP). This principle governed ways to implement inheritance, polymorphism, and subclasses. Specifically:
Liskov’s notion of a behavioral subtype defines a notion of substitutability for objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g. correctness). -wikipedia
The way I interpret this is that we may replace the superclass with the subclass and the program should still operate correctly. A classic example that is typically used is the rectangle
vs. square
where the square
“IS A” subclass of the rectangle
(it’s just a special rectangle
where the sides have equal length).
This violates the Liskov Substitution Principle when hypothetical methods like setLength
and setHeight
exist on rectangle
(the superclass) and are inherited by square
(the subclass). When implementing a square
, we must make sure the properties Length
and Height
are equal which then violates an invariant of the base rectangle
class. It’s expected in the superclass that Length
and Width
can be independent of each other.
Let’s go through an example using the video game Contra. Up to two people can play Contra at the same time. Each player can assume the role of a Commando
named either Bill or Lance. Based on these rules, we might have the following class:
class Commando
def talk
''
end
def shoot
''
end
end
We can also define two subtypes to represent each player:
class PlayerOne < Commando
def talk
'Lance'
end
def shoot
'Bang Bang'
end
end
class PlayerTwo < Commando
def talk
'Bill'
end
def shoot
'Pow Pow'
end
end
Now we can attempt to substitute the subtypes (PlayerOne
and PlayerTwo
) where we were expecting the superclass (Commando
):
playerOne = PlayerOne.new
playerTwo = PlayerTwo.new
def create_commando(commando)
puts "#{commando.talk} here, my gun sound is - #{commando.shoot}"
end
create_commando(playerOne) # Lance here, my gun sound is - Bang Bang
create_commando(playerTwo) # Bill here, my gun sound is - Pow Pow
This is all looking pretty good. PlayerOne
“IS A” Commando
and Commandos can be replaced with the PlayerOne
or PlayerTwo
subtypes.
We can violate the Liskov Substitution Principle in Ruby easily by changing the return type in one of the subclasses which would make the substitution break. Suppose PlayerTwo
is a medic. Still a Commando
, but has a slightly different return type to the shoot
method:
class PlayerTwo < Commando
def talk
'Bill'
end
def shoot
{ health: 50, food: 50 }
end
end
Now PlayerTwo
returns a hash instead of a string when the shoot
method is called. That would break our create_commando
code since we are expecting a string
. A common code smell would be some code that type checks to see if we have PlayerTwo
or PlayerOne
and then deals with the change in return type accordingly. In cases where we see this, we should fix the inheritance gone wrong rather than work around the problem by type checking and changing the logic.
It’s interesting to see how closely related all the SOLID principles are and cases where an implementation to fix one violation may inadvertantly cause another violation.