Course contentsShow
← C#
Lesson 121 of 1,9082. Object-Oriented Design in C#Free lesson

The Liskov Substitution Principle in Practice

You will refactor deep inheritance trees to ensure base classes can be replaced by derived types without altering program correctness.

The Liskov Substitution Principle (LSP) in Practice

In this lesson, you’ll learn how to ensure your derived classes honor the "contracts" of their base classes so that your application remains stable when you swap them out.

The Core Problem: Fragile Hierarchies

The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Beginners often create deep inheritance trees where a child class overrides a method but essentially "breaks" its functionality (often by throwing a NotImplementedException).

Imagine a base class Bird with a Fly() method. If you create a Penguin class that inherits from Bird, you might be tempted to make Fly() throw an exception because penguins can't fly. This violates LSP because any code expecting a Bird will crash when it encounters a Penguin.

Refactoring for Correctness

To fix this, we avoid forcing unrelated behaviors into a single hierarchy. Instead of a deep, rigid tree, we decompose the logic into smaller, focused interfaces or base classes.

Bad Design:

public class Bird { public virtual void Fly() { /* ... */ } }
public class Penguin : Bird { 
    public override void Fly() => throw new NotSupportedException(); 
}

Refactored Design: Instead of forcing Penguin to be a Bird, we identify what they actually share. We might create a Bird class for generic behavior, and an IFlyable interface only for birds that can actually fly:

public class Bird { /* Common bird properties */ }

public interface IFlyable { void Fly(); }

public class Sparrow : Bird, IFlyable { 
    public void Fly() { /* Implementation */ } 
}

public class Penguin : Bird { 
    // No Fly() method here, so it never breaks the contract!
}

By refactoring, we ensure that any code expecting an IFlyable is guaranteed that the Fly() method will work. We no longer rely on inheritance just to share code; we use interfaces to define capabilities. When your classes only promise what they can actually deliver, your program becomes predictable and robust.

Key Takeaway: If a subclass cannot fulfill the expectations set by the base class, don't inherit—refactor your hierarchy to use interfaces that strictly define capabilities.