Что такое принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP)?

«Что такое принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP)?» — вопрос из категории ООП, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Принцип подстановки Барбары Лисков (LSP) — третий принцип SOLID. Он гласит: объекты в программе должны быть заменяемыми экземплярами их базовых типов без изменения корректности этой программы.

Простыми словами: Если у вас есть функция, работающая с классом Родитель, то вы должны иметь возможность передать ей любой класс-Ребенок, и программа продолжит работать правильно. Наследник не должен ужесточать предусловия или ослаблять постусловия родительского метода.

Классический пример нарушения LSP:

public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("I'm flying!");
    }
}

public class Penguin : Bird // Пингвин — птица, но не летает!
{
    public override void Fly()
    {
        throw new NotImplementedException("Penguins can't fly!");
    }
}

// Клиентский код, ожидающий работу с Bird
public class BirdAviary
{
    public void ReleaseBird(Bird bird)
    {
        bird.Fly(); // ВЫБРОСИТ ИСКЛЮЧЕНИЕ, если bird — Penguin!
    }
}

Здесь Penguin нарушает контракт, заданный базовым классом Bird (метод Fly), и ломает клиентский код.

Исправление через пересмотр иерархии (соблюдение LSP):

// Базовый класс не навязывает необщее поведение
public abstract class Bird { }

// Интерфейс определяет способность, которой обладают не все птицы
public interface IFlyable
{
    void Fly();
}

public class Sparrow : Bird, IFlyable // Воробей — птица и умеет летать
{
    public void Fly() => Console.WriteLine("Sparrow is flying!");
}

public class Penguin : Bird // Пингвин — птица, но не реализует IFlyable
{
    // Специфичные для пингвина методы, например, Swim()
    public void Swim() => Console.WriteLine("Penguin is swimming!");
}

// Клиентский код теперь корректен
public class FlightSimulator
{
    public void StartFlight(IFlyable flyableBird) // Принимает только то, что может летать
    {
        flyableBird.Fly();
    }
}

Почему LSP важен:

  • Обеспечивает надежность полиморфизма. Вы можете безопасно использовать коллекции базового типа.
  • Упрощает тестирование. Можете использовать mock-объекты, наследующие от того же базового класса.
  • Предотвращает появление неочевидных ошибок, когда подстановка подкласса приводит к неожиданному поведению.

Нарушение LSP часто сигнализирует о неправильно спроектированной иерархии наследования.