В чем суть принципа подстановки Барбары Лисков (LSP)?

Ответ

Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP) — это третий из пяти принципов SOLID. Он гласит:

Объекты базового класса должны быть заменяемыми объектами его подклассов без изменения корректности программы.

Это означает, что наследующий класс должен:

  1. Полностью реализовывать контракт (интерфейс) родительского класса.
  2. Не ужесточать предусловия (требования к входным данным).
  3. Не ослаблять постусловия (гарантии на результат и состояние).
  4. Не изменять инварианты класса (условия, всегда истинные для объекта).
  5. Не выбрасывать новых исключений, не являющихся подтипами исключений базового метода.

Классический пример нарушения LSP (Квадрат/Прямоугольник):

// Базовый класс
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }
    public void setHeight(int height) {
        this.height = height;
    }
    public int getArea() {
        return width * height;
    }
}

// Подкласс, нарушающий LSP
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // Побочный эффект: меняет и высоту!
    }
    @Override
    public void setHeight(int height) {
        this.height = height;
        this.width = height; // Побочный эффект: меняет и ширину!
    }
}

// Клиентский код, рассчитанный на работу с Rectangle
public void resizeRectangle(Rectangle r) {
    // Предполагается, что ширина и высота меняются независимо
    r.setWidth(5);
    r.setHeight(4);
    assert r.getArea() == 20; // Утверждение будет ЛОЖНЫМ для Square!
    // Для Square после setHeight(4) ширина тоже станет 4, и площадь будет 16.
}

Почему это нарушение? Клиентский код resizeRectangle ожидает, что setWidth и setHeight работают независимо, как определено в Rectangle. Класс Square нарушает это поведение, внося скрытые побочные эффекты. Замена Rectangle на Square приводит к ошибке.

Следствие LSP: Наследование должно описывать отношение «является» (is-a) не только с точки зрения данных, но и с точки зрения поведения. Если подкласс не может выполнять все обязанности базового класса, не внося изменений в ожидаемое поведение, наследование применять не следует. Часто проблему решает использование композиции или выделение общего интерфейса.

Ответ 18+ 🔞

Да ты послушай, какой принцип эти программисты выдумали, блядь! Подстановка Барбары Лисков, ёпта! Звучит, как название какого-то ебучьего детектива, а на деле — простая мысль, до которой некоторые, блядь, своим ебалом дойти не могут.

Суть в чём, в рот меня чих-пых: если у тебя есть класс-родитель и класс-ребёнок, то ты должен спокойно подсунуть этого ребёнка везде, где ждут родителя, и всё должно работать, а не разъебываться в пизду. Не должно быть сюрпризов, типа «ой, а я ещё и не то сделал».

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

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

Всё чинно, благородно. Прямоугольник. Ширину поставил — ширина поменялась. Высоту поставил — высота поменялась. Красота.

А теперь его сынок, квадратик:

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // А вот и хуйня, блядь!
    }
    @Override
    public void setHeight(int height) {
        this.height = height;
        this.width = height; // И тут та же самая хуйня!
    }
}

Ну вроде бы правильно, ёпта! Квадрат же! Поставил ширину — и высота автоматом такая же. Математически-то верно!

А теперь представь, есть у тебя функция, которая работает с прямоугольниками:

public void resizeRectangle(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.getArea() == 20; // Ожидаем 5 * 4 = 20
}

Ты, такой довольный, суёшь туда свой Square. И что происходит? Блядь, волнение ебать!

  1. setWidth(5) — ширина 5, и высота ТОЖЕ становится 5, сука!
  2. setHeight(4) — высота 4, и ширина ТОЖЕ становится 4, ёбаный насос!
  3. В итоге у тебя и ширина, и высота равны 4. Площадь — 16. А твой assert ждал 20. ПИЗДЕЦ. Программа пошла по пизде, потому что квадратик нарушил негласный договор: «метод setWidth меняет ТОЛЬКО ширину».

Вот это и есть нарушение принципа Лисков, блядь! Наследник внаглую меняет ожидаемое поведение родителя. Клиентский код, рассчитанный на родителя, ломается о наследника.

Мораль сей басни, ёпта: наследование — это не про данные («у квадрата четыре угла, как и у прямоугольника»), а в первую очередь про ПОВЕДЕНИЕ, блядь! Если твой наследник не может честно выполнять все контракты родителя, не подкладывая свинью в виде скрытых действий — не лезь в наследование, иди нахуй. Используй композицию, интерфейсы, что угодно. Чтобы не было потом таких вот ебушки-воробушки, когда подмена одного объекта другим приводит к тихому, ебучему краху.