Derived classes must be substitutable for their base classes.
Robert C. Martin
Welcome back to the series on SOLID. By now, I’ll assume you’ve read my earlier post on the Open/Closed Principle. This principle is quite an obvious principle. I’m going to use the time-and-time-again Duck example below. Think: If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck. That is unless you break the Liskov Substitution Principle. With this principle, you must be able to substitute any object for its base type. What do I mean? Let’s say you have a duck base class. You can add a Mallard Duck, a Marbled Duck, and a Rubber Duck. But – a rubber duck can’t fly. It’ll break your base class implementation of a Duck. You’ll see more below.
Breaking the principle
So, as I explained above: We’ll start by using a Duck base class. In this base class, we’ll have a method Quack(). We’ll also add a few duck types.
public abstract class Duck { public abstract void Quack(); } public class MallardDuck : Duck { public override void Quack() { System.Console.WriteLine("Quack!"); } } public class MarbledDuck : Duck { public override void Quack() { System.Console.WriteLine("Quack!"); } } public class RubberDuck : Duck { public override void Quack() { System.Console.WriteLine("Squeak!"); } }
But, you realise that Ducks also Swim, right? I mean they should swim. So far, so good.
public abstract class Duck { public abstract void Quack(); public abstract void Swim(); } public class MallardDuck : Duck { public override void Quack() { System.Console.WriteLine("Quack!"); } public override void Swim() { System.Console.WriteLine("Swimming!"); } } public class MarbledDuck : Duck { public override void Quack() { System.Console.WriteLine("Quack!"); } public override void Swim() { System.Console.WriteLine("Swimming!"); } } public class RubberDuck : Duck { public override void Quack() { System.Console.WriteLine("Squeak!"); } public override void Swim() { System.Console.WriteLine("Floating!"); } }
Finally, you realise that Ducks should also fly. Do you know a duck that can’t fly?
public abstract class Duck { public abstract void Quack(); public abstract void Swim(); public abstract void Fly(); } public class MallardDuck : Duck { public override void Quack() { System.Console.WriteLine("Quack!"); } public override void Swim() { System.Console.WriteLine("Swimming!"); } public override void Fly() { System.Console.WriteLine("Flying!"); } } public class MarbledDuck : Duck { public override void Quack() { System.Console.WriteLine("Quack!"); } public override void Swim() { System.Console.WriteLine("Swimming!"); } public override void Fly() { System.Console.WriteLine("Flying!"); } } public class RubberDuck : Duck { public override void Quack() { System.Console.WriteLine("Squeak!"); } public override void Swim() { System.Console.WriteLine("Floating!"); } public override void Fly() { throw new Exception("Rubber ducks can't fly!"); } }
Uh oh! A Rubber Duck can’t fly. Let’s use this principle: Subtypes must be substitutable for their base types. Can we substitute RubberDuck for a Duck? No. Because it’ll throw an exception. We definitely don’t want that. So how do we fix this code? Should we fix it?
Fix using the principle
Well, to be honest, a Rubber Duck shouldn’t be a Duck. Full Stop. If we really wanted to fix this, we can use the next principle to take out the Duck abstraction. We can have an interface for Quack, Swim, and Fly. We won’t have a Duck base class anymore, but we’ll have ducks who can do all three and ducks who can only do two.
public interface IFly { void Fly(); } public interface ISwim { void Swim(); } public interface IQuack { void Quack(); } public class MallardDuck : IFly, ISwim, IQuack { public void Quack() { System.Console.WriteLine("Quack!"); } public void Swim() { System.Console.WriteLine("Swimming!"); } public void Fly() { System.Console.WriteLine("Flying!"); } } public class MarbledDuck : IFly, ISwim, IQuack { public void Quack() { System.Console.WriteLine("Quack!"); } public void Swim() { System.Console.WriteLine("Swimming!"); } public void Fly() { System.Console.WriteLine("Flying!"); } } public class RubberDuck : ISwim, IQuack { public void Quack() { System.Console.WriteLine("Squeak!"); } public void Swim() { System.Console.WriteLine("Floating!"); } }
L for Liskov Substitution Principle
I hope this was a little help explaining the principle. Ducks will be ducks, but a rubber duck is not a duck. Remember that you should be able to substitute your subclass for you base class at any point and we should expect it to do what’s expected. If it ends up doing something unexpected, then you’re probably breaking the Liskov substitution principle.
Republished on Medium.
[…] https://www.alexaitken.nz/blog/l-for-liskov-substitution-principle/ […]