In the previous post, I wrote about Barbara Liskov research paper on data abstraction and hierarchy. In the paper, the author states a property which exists between type and subtype. That property later becomes known as the Liskov Substitution Principle. In this post, I continue to go over the principle in more details and give examples. The principle is one out of the five software design principles in SOLID:
The Liskov Substitution Principle refers to a property between a type and a subtype. Oxford language dictionary defines a type as “a category of people or things having common characteristics” and subtype as “a secondary or subordinate type”. For example, Owl, Hummingbird and other flying birds share a common behavior and feature which is that they all have wings and can fly. In terms of type and subtype, we may say flying-bird is a type of which Owl, HummingBird and other flying birds are subtypes. However, not all birds can fly. Penguin is one of them. So, it would be wrong to say that Penguin is a subtype of flying-bird. Bringing this idea into programming, the Liskov Substitution Principle states that a subtype should have all the behaviors of the supertype such that you can replace an object of the supertype with an object of the subtype without altering the behaviors of the program.
The Liskov Principle is most applicable when using inheritance. When used correctly, inheritance can help make codes more concise and easier to maintain. For example, I may have a superclass called Bird of which subclasses include Owl and HummingBird. In the Bird class, i may define common properties such as Age, Name, Feathers, Wings and common behavior such as fly(). All the subclasses of Bird can inherit the properties and methods in the Bird class, so I don’t have to define them again.
public class Bird { public int Age { get; set; } public string Name { get; set; } public BirdWings Wings { get; set; } public BirdFeatures Features { get; set; } public virtual void Fly() { Flap(); Glide(); } } public class HummingBird: Bird { } public class Owl: Bird { }
Inheritance leads to polymorphism, and both can make my codes more generic. For example, suppose I need to call the fly() method in one or more subclasses that extend Bird, I can just pass to the method BirdExampleAction(Bird bird) an instance of Owl or HummingBird or any other subclass that extends from Bird. I don’t need to have multiple methods for each type of subclass, nor do I need to have conditional statements to check for the specific type of Bird.
public static void BirdAction(Bird bird) { bird.Fly(); } public static void main(string[] args) { List < bird > birds = new List(); birds.Add(new Owl()); birds.Add(new HummingBird()); foreach(var bird in birds) { BirdAction(bird); } }
As you can see in the above snippet, I can substitute an instance of Bird with an instance of Owl or HummingBird as parameter to the BirdExampleAction() without altering the behavior of my program. This only work under the assumption that I don’t override the Fly() method in the Owl or HummingBird class. It’s worth noting that if I need to, I can still override the Fly() method in a subclass. I just need to preserve the behavior defined in the base class, as shown in the below example. In this sense, I have followed the Liskov Substitution Principle and can enjoy the benefits that come with using inheritance correctly.
public class Owl: Bird { public override void Fly() { GlideNoiselessly(); FlapLittle(); } }
Suppose I now add the class Penguin as a subclass of Bird. Because Penguin cannot fly, I have a problem. If I don’t know about the Liskov Substitution Principle, or not careful, I may just override the Fly() method in the Penguin class to have a Penguin to swim instead of fly. This is just so I can reuse other properties in the Bird class.
public class Penguin: Bird { public override void Fly() { throw new NotSupportedException("I'm a Penguin. So I cannot fly. I can swim though"); } public void Swim() { ... } }
The problem is I can no longer simply pass an instance of Penguin to the method BirdAction because doing so cause an exception when the Fly() method is called on the Penguin instance.
public static void BirdAction(Bird bird) { bird.Fly(); // cause NotSupportedException if bird is an instance of Penguin } public static void main(string[] args) { List < bird > birds = new List(); birds.Add(new Owl()); birds.Add(new HummingBird()); birds.Add(new Penguin()); foreach(var bird in birds) { BirdAction(bird); } }
How do I resolve this issue? A simple way is to just have a conditional statement that explicitly check the type of bird. However, BirdAction method is no longer generic. It needs to know the specific type of bird. Soon I may have many more if/else statements as I add more types of birds that cannot fly and/or swim. Clearly, adding the conditional statement is not a good approach.
public static void BirdAction(Bird bird) { if (bird is Penguin) { var penguin = (Penguin) bird; penguin.Swim(); } else { bird.Fly(); } }
The issue is that I have violated the LSP. Per the definition, Penguin is not a subtype of Bird in the sense that I cannot substitute an instance of Bird with an instance of Penguin. Adding the conditional statements make my codes harder to maintain, reason, and test. For instance, if I were to test the BirdAction method, I have to consider the special cases and update the tests whenever I add a new subclass of Bird. Suppose the code is part of a shared library that other applications consume. My codes can break the client applications if they expect that they can call the Fly() method on any instance of Bird without checking for the special conditions. In general, I don’t want to violate the LSP to avoid making my codes harder to maintain and fragile because of breaking implied contracts.
Per the definition of the LSP, a subtype must not break the implied or explicit behaviors defined in the base type. As such, my goal is to ensure all the behaviors in a base class are preserved in the subclasses. In the bird example, because not all birds can fly, one way I can do is to extract the Fly() method out of the base class and define more generic subclasses of Bird such as FlyingBird and SwimmingBird.
public class Bird { public int Age { get; set; } public string Name { get; set; } //... } public class FlyingBird: Bird { public virtual void Fly() { Flap(); Glide(); } } public class HummingBird: FlyingBird { } public class Owl: FlyingBird { } public class SwimmingBird: Bird { public virtual void Swim() { // ... } } public class Penguin: SwimmingBird { } public class Duck: SwimmingBird { } public static void main(string[] args) { List < FlyingBird > flyingBirds = new List(); flyingBirds.Add(new Owl()); flyingBirds.Add(new HummingBird()); foreach(var flyingBird in birds) { FlyingBirdAction(flyingBird); } List < SwimmingBird > swimmingBirds = new List(); swimmingBirds.Add(new Penguin()); swimmingBirds.Add(new Duck()); foreach(var swimmingBird in swimmingBirds) { SwimmingBirdAction(swimmingBird); } } public static void FlyingBirdAction(FlyingBird flyingBird) { flyingBird.Fly(); } public static void SwimmingBirdAction(SwimmingBird swimmingBird) { swimmingBird.Swim(); }
My codes now adhere to the LSP because I can substitute an instance of FlyingBird with an instance of Owl or HummingBird when calling FlyingBirdAction()
. Similarly, I can substitute an instance of SwimmingBird with an instance of Penguin when calling SwimmingBirdAction().
In the bird example, besides defining more generic types of Bird, another way I can do to adhere to the LSP is defining a more generic method, i.e. Action() instead of the specific Fly() or Swim(), as the below example demonstrated.
public class Bird { public int Age { get; set; } public string Name { get; set; } // ... public abstract void Action() { } } public class HummingBird: Bird { public override void Action() { Fly(); } private void Fly() { Flap(); Glide(); } } public class Owl: Bird { public override void Action() { Fly(); } private void Fly() { GlideNoiselessly(); FlapLittle(); } } public class Penguin: Bird { public override void Action() { Swim(); } } public static void BirdAction(Bird bird) { bird.Action(); } public static void main(string[] args) { List < bird > birds = new List(); birds.Add(new Owl()); birds.Add(new HummingBird()); birds.Add(new Penguin()) foreach(var bird in birds) { BirdAction(bird); } }
My codes still conform to the LSP as I can substitute an instance of Bird with an instance of one of the subclasses when calling BirdAction().
In summary, when utilizing inheritance or interfaces to group related concepts and reuse codes, avoid surprises for the clients which rely on the explicit or implied behaviors defined in the base classes by not breaking those behaviors in the subclasses. In short, conform to the LSP to make your codes easier to maintain, reason and test.
Notes on the three programming paradigms
More on inheritance
Template method example
Notes on Barbara Liskov paper on data abstraction and hierarchy
Common frameworks, libraries and design patterns I use
Notes on component coupling
Notes on Component Cohesion
Notes on The Clean Architecture