Understanding the Liskov Substitution Principle

Apr 1, 2017

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!

Mirzalazuardi Hermawan