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

Ответ

Принцип подстановки Лисков (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 заставляет проектировать иерархии так, чтобы подтипы не ужесточали предусловия и не ослабляли постусловия базового типа.

Ответ 18+ 🔞

А, принцип подстановки Лисков, ёпта! Это ж та самая история, когда ты думаешь, что у тебя в программе один объект, а на самом деле подсунули другого, и всё летит в тартарары, как твой код после пятничного деплоя.

Смотри, в чём суть, блядь. Представь, есть у тебя базовый класс, скажем, Утка. И ты пишешь код, который с этими утками работает: заставляет крякать, плавать, летать. А потом ты подсовываешь ему подтип Утка-приманка (резиновую). И если эта резиновая утка вдруг начнёт в твоём коде пытаться летать и падать, потому что у неё крылья не для того — это и есть нарушение, блядь. Клиентский код ожидает утку, а не пиздёж.

Вот тебе классический пиздец, который все приводят — прямоугольник и квадрат.

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);
}

Ты вызываешь его, передавая прямоугольник — всё ок. Ожидаешь 20, получаешь 20. Ура.

А теперь подсовываешь квадрат:

var sq = new Square();
TestRectangleArea(sq); // Выведет: Expected area: 20, Actual area: 16

Вот и всё, приехали. Клиентский код сломался, потому что квадрат не может быть адекватной заменой прямоугольнику в этом контексте. Он нарушил контракт: у прямоугольника стороны независимы, а у квадрата — нет. После установки ширины в 5, высота автоматом тоже стала 5, и когда код попытался поставить высоту в 4, он и ширину заодно поменял на 4. Итог — площадь 16, а не 20. Охуенно, да?

Так как же это исправить, чтобы не было мучительно больно?

Вариант первый, самый правильный — перестать ебаться с наследованием там, где оно не нужно. Квадрат с математической точки зрения — да, прямоугольник. А с точки зрения поведения объекта в программе — нихуя не прямоугольник, если мы меняем стороны.

Поэтому делаем так: либо общий интерфейс, либо композиция.

public interface IShape
{
    int GetArea();
}

public class Rectangle : IShape { /* ... свою логику ... */ }
public class Square : IShape { /* ... свою логику ... */ }

Теперь клиентский код работает с IShape, и ему похуй, квадрат там или прямоугольник — главное, чтобы площадь умел возвращать. Подстановка безопасна.

Вариант второй — сделать объекты неизменяемыми, как скала. Если состояние менять нельзя, то и проблем нет.

public class Rectangle
{
    public Rectangle(int width, int height)
    {
        Width = width;
        Height = height;
    }
    public int Width { get; } // Только геттер, сеттера нет
    public int Height { get; }
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    // Квадрат создаётся с одной стороной, и это логично
    public Square(int side) : base(side, side) { }
}

Вот теперь подставляй что хочешь. Объекты создал — и забыл. Менять их после создания нельзя, поэтому и подмена безопасна. Хоть квадрат, хоть прямоугольник — площадь посчитается корректно на момент создания, и никто не сможет потом испортить состояние.

Вывод, блядь: LSP — это про то, чтобы подтипы не выёбывались и не меняли правила игры, заданные базовым классом. Нельзя ужесточать предусловия (требовать больше, чем база) и нельзя ослаблять постусловия (обещать меньше, чем база). Иначе клиентский код, который полагается на контракт базового типа, получит неожиданный пиздец вместо работы. Проектируй иерархии так, чтобы подтипы были настоящими заменами, а не вот этим вот всем.