Putting the "L" in SOLID

Contra Intro Graphic

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.

Written on August 7, 2017