• 4 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 2/18/22

"L" for the Liskov Substitution Principle

What is Liskov Substitution? 

Inheritance starts as a cool idea. You have this existing concept and then want to add another. And this new concept is just a more specific implementation of the original. Easy, derive a new class!

But over time, it’s easy to abuse inheritance. I’ve worked on projects where the hierarchy of classes is 10 levels deep. It’s natural to lose track of what the specific thing the bottom classes are doing. Eventually, some class gets added that breaks the existing system.

This is a violation of Liskov substitution, which says:

Adding derived classes shouldn't break the functionality of an already existing system.

I call this the no surprises principle. When adding a new class in the hierarchy, the existing system shouldn’t break when it uses the new class. Otherwise, you get a surprise. But not a good one. 😕

Why Use the Liskov Substitution Principle?

Let’s say you have a class called cat. It has a method in it called eat(). You can pass a standard cat kibble brand - i.e., Kitten Munch - into the eat method, and the cat is fine. 🐈 If you add a new class, tiger, which is a kind of cat. It will reimplement the eat method. 🐅

However, tigers won't eat dry food that comes from a bag. They eat raw meat. That’s not behavior you’ve seen before. Surprise! 😬

The problem happens when you think passing Kitten Munch into the eat method works for every cat. A tiger is a cat, so it should work. But it doesn't. So, you can’t just put a tiger in your system in places where you've had a cat.

This violates the Liskov substitution principle: you can't replace a base class (cat), with a derived class (tiger), without it affecting the rest of the system.

The Liskov substitution principle helps by limiting your use of inheritance. While it is easy to come up with low-level, concrete implementation classes (hey, they do what you want them to do), it is better to step back and think of a high-level abstraction.

For example, let's try to fix the cat and tiger problem. You really have two concrete (that is, specific implementation) classes. You need to introduce a couple of higher-level interfaces/abstractions:

  • Carnivore, which only eats meat.

  • Omnivore, which eats meat and other things (like Kitten Munch).

Now when you add any animal, you can have it implement one of those two interfaces.

How Do We Apply Liskov Substitution Our Code?

In the movie Jurassic Park, they made the mistake of having people get close to the carnivores. 🐊 Oh wait, you wanted to know about using interfaces to follow Liskov substitution! Right! Well, the hard part is knowing where to create the interfaces in a hierarchy.

Like in the cat example, it made sense at the start to have a tiger be a kind of cat. But then you saw the problem when you got to implementation. Often it’s hard to recognize when Liskov is going to be violated ahead of time. But when you discover a violation, you need to rethink your use of inheritance. So, when looking to use inheritance, ask yourself:

  • Does the derived class have a meaningful implementation for all overridden methods? If so, that's a good thing. ✅

  • Would implementing an overridden method be out of the ordinary, possibly resulting in throwing an exception? If so, that's bad. ❌

  • Would implementing an overridden method ignore the call, and do nothing? Usually that's a bad thing, but might be justifiable. You should keep an eye out if the derived class does this for:

    • A single method. ✅

    • Many of them. ❌

In our card game, if we introduced the concept of a joker, which has no rank or suit, asking for those values would be an error:

class Joker extends PlayingCard {
    public Rank getRank() {
        throw new UnsupportedOperationException();
    }
    public Suit getSuit() {
        throw new UnsupportedOperationException();
    }
};

Our code does not expect a card not to have a rank or suit. So implementing joker as just a playing card through inheritance would be incorrect. Good thing we aren't going to deal with jokers.

What if we had to implement a joker? 🤔

So far, we have been applying SOLID correctly. We have been talking through interfaces, and then deferring the implementations. But in this case, we have a concrete class as the starting point. That makes it tougher. In hindsight, we should have found about jokers during discussions with our customer. But sometimes, new stuff comes up, and you have to deal with it. You could call this the nobody's perfect principle.

Now, imagine you'd have to go back and modify the rank and suit enumerations to support the new concept. What's your solution?

Try It Out For Yourself
Try it out for yourself

Add NONE for both of them. So, not too rough a fix: 

package com.openclassrooms.cardgame.model;

public enum Rank {
	NONE(0),
	TWO (2),
	THREE (3),
	FOUR (4),
	FIVE (5),
	SIX (6),
	SEVEN (7),
	EIGHT (8),
	NINE (9),
	JACK (10),
	QUEEN (11),
	KING (12),
	ACE (13);
	
	int rank;
	
	private Rank(int value) {
		rank = value;
	}
	
	public int value() {
		return rank;
	}
}

And for the suits:

package com.openclassrooms.cardgame.model;

public enum Suit {
	NONE(0),
	DIAMONDS (1),
	HEARTS (2),
	SPADES (3),
	CLUBS (4);
	
	int suit;
	
	private Suit(int value) {
		suit = value;
	}
	
	public int value() {
		return suit;
	}
}

Let's Recap! 

  • Liskov substitution principle applies to inheritance hierarchies. It is violated when a derived class cannot take the place of a base class without the system breaking.

  • To make sure you avoid violating this rule, try to first think of high-level abstractions/interfaces instead of low-level/concrete implementations.

In the next chapter, we'll look at the value of keeping interfaces small with the interface-segregation principle.

Example of certificate of achievement
Example of certificate of achievement