Приведи пример принципа подстановки Барбары Лисков (LSP) из SOLID

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

Ответ

Принцип подстановки Лисков (Liskov Substitution Principle, LSP): Объекты производного класса должны быть заменяемы на объекты базового класса без нарушения корректности программы. Клиентский код, работающий с базовым типом, должен продолжать работать корректно с любым из его подтипов.

Классический пример нарушения LSP — «Прямоугольник-Квадрат»:

// Базовый класс
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int Area => Width * Height;
}

// Производный класс, нарушающий контракт базового
public class Square : Rectangle
{
    public override int Width
    {
        get => base.Width;
        set { base.Width = value; base.Height = value; } // Нарушение!
    }
    public override int Height
    {
        get => base.Height;
        set { base.Height = value; base.Width = value; } // Нарушение!
    }
}

// Клиентский код, который сломается
public void TestRectangleArea(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 4;
    Console.WriteLine("Expected area: 20, Actual area: " + rect.Area);
}

// Вызов
var sq = new Square();
TestRectangleArea(sq); // Выведет: Expected area: 20, Actual area: 16
// Квадрат не может быть заменой прямоугольника в этом контексте!

Как исправить (соблюсти LSP): Нужно пересмотреть иерархию наследования. Квадрат — это частный случай прямоугольника с математической точки зрения, но не с точки зрения поведения объекта, изменяющего свои стороны независимо.

// Решение 1: Не связывать наследованием, использовать композицию или общий интерфейс.
public interface IShape
{
    int GetArea();
}

public class Rectangle : IShape { /* ... */ }
public class Square : IShape { /* ... */ }

// Решение 2: Сделать классы неизменяемыми (immutable).
public class Rectangle
{
    public Rectangle(int width, int height)
    {
        Width = width;
        Height = height;
    }
    public int Width { get; } // Только getter
    public int Height { get; }
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    // Квадрат создается с одним параметром, что логично.
    public Square(int side) : base(side, side) { }
}
// Теперь подстановка безопасна, так как состояние объектов нельзя изменить после создания.

Вывод: LSP заставляет проектировать иерархии так, чтобы подтипы не ужесточали предусловия и не ослабляли постусловия базового типа.