Hi everyone! Let's continue learning about SOLID principles. Today we'll discuss the Liskov Substitution Principle, which Barbara Liskov defined in her 1987 conference keynote "Data abstraction and hierarchy".
The definition states:
If S is a subtype of T, then objects of type T may be replaced with objects of type S
To rephrase this definition for Ruby programming language, we could say:
If class Car is inherited from class Vehicle, then objects of class Vehicle may be replaced with objects of class Car
While this might not be as precise as the original definition which mentions "types" and "subtypes", remember that in Ruby we use Duck Typing:
If it looks like a duck and quacks like a duck, it's a duck
Let's Look at Some Examples
First, we need to define our basic class, let's call it Vehicle:
class Vehicle
def start_engine
''
end
def max_speed
''
end
end
Now we can define a couple of "subtypes":
class Car < Vehicle
def start_engine
'Vroom!'
end
def max_speed
'200 km/h'
end
end
class Motorcycle < Vehicle
def start_engine
'Vrooom vrooom!'
end
def max_speed
'280 km/h'
end
end
We should now be able to use these subtypes instead of the basic type Vehicle:
car = Car.new
motorcycle = Motorcycle.new
def describe_vehicle(vehicle)
puts "This vehicle goes #{vehicle.max_speed} and sounds #{vehicle.start_engine}"
end
describe_vehicle(car) # => This vehicle goes 200 km/h and sounds Vroom!
describe_vehicle(motorcycle) # => This vehicle goes 280 km/h and sounds Vrooom vrooom!
Side note: Because we have Duck Typing, we could create a class that isn't inherited from the basic class Vehicle but implements the same interface (start_engine
and max_speed
), and that would work too. That's where polymorphism kicks in.
How Can We Break the Liskov Substitution Principle?
So far, we've used inheritance, which allowed us to substitute objects of the parent class with objects of inherited classes. But how could we break the Liskov Substitution Principle?
From Wikipedia:
Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping
That last part is key: (strong) behavioral subtyping.
Programming languages with strong typing have less chance to break the Liskov substitution principle because they strictly define types of method arguments and returning values.
In Ruby, we're responsible for that. We can easily break the Liskov substitution principle by changing the return type:
class Car < Vehicle
def start_engine
'Vroom!'
end
def max_speed
{ city: '120 km/h', highway: '200 km/h' }
end
end
Now Car
returns a hash instead of a string for max_speed
. This might break code that expects a string from the max_speed
method call.
Inheritance works when it's an is-a relation type. So Car is a Vehicle, that works. But wrong usage of inheritance would break the Liskov Substitution Principle as well:
class Car < Vehicle
def start_engine
'Vroom!'
end
def max_speed
'200 km/h'
end
def license_plate
'ABC-123'
end
end
class Bicycle < Vehicle
def start_engine
'No engine!'
end
def max_speed
'30 km/h'
end
def license_plate
raise NotImplementedError
end
end
In this case, we cannot substitute Car with any object of class Vehicle or its "subtypes" (Bicycle) because those don't respond to license_plate
properly, which might break our application. Here, wrong usage of inheritance breaks the Liskov Substitution Principle.
Conclusion
It's interesting to see how all SOLID principles are related to each other and to basic ideas of OOP: polymorphism, inheritance, 'is-a' vs 'has-a' types of relations between classes.
I hope this helped you understand the idea of this principle and showed how to keep objects of the same types and subtypes substitutable.
Thanks for reading!