Что такое принцип подстановки Барбары Лисков (LSP) в ООП?

Ответ

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

Почему это важно? Нарушение LSP приводит к хрупкому коду, где замена типа вызывает неожиданные ошибки, и ломает полиморфизм.

Пример нарушения LSP:

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

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

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

    @Override
    public void setHeight(int h) {
        super.setHeight(h);
        super.setWidth(h); // Побочный эффект: меняет ширину
    }
}

// Клиентский код, который сломается
public void resizeRectangle(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.getArea() == 20; // Утверждение провалится для Square
}

Решение: Не наследовать Square от Rectangle. Вместо этого использовать композицию, общий интерфейс или признак (flag), если квадрат — частный случай прямоугольника.

Ответ 18+ 🔞

Давай разберем эту хитрую концепцию, которая с виду проще пареной репы, а на деле — сплошные подводные ебучки.

Вот представь, есть у тебя класс Прямоугольник. Ну, прямоугольник и прямоугольник, ширина, высота, площадь. Всё логично, хуле. И тут тебе в голову приходит гениальная, блядь, мысль: «А квадрат-то — это частный случай прямоугольника! Наследую-ка я класс Квадрат от Прямоугольника!». И вроде бы всё по науке, ёпта.

А теперь смотри, какую дичь мы получаем. Клиентский код, который работает с прямоугольниками, ожидает вполне определённого поведения: поставил ширину — ширина поменялась, поставил высоту — высота поменялась. И площадь, соответственно, пересчиталась.

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

А теперь подсовываем этому коду наш «квадрат», который, по нашей «гениальной» логике, является подклассом прямоугольника. И что происходит? А происходит пиздец, вот что.

// Подкласс, нарушающий всё на свете
class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w); // АХТУНГ! Меняем и высоту тоже!
    }

    @Override
    public void setHeight(int h) {
        super.setHeight(h);
        super.setWidth(h); // И тут, нахуй, ширину подменяем!
    }
}

Клиентский код вызвал setWidth(5), а квадрат, сука, заодно и высоту в 5 поставил. Потом клиент вызывает setHeight(4), а квадрат и ширину в 4 меняет. В итоге у нас квадрат 4x4, площадь 16, а утверждение assert r.getArea() == 20 летит в пизду вместе с доверием к нашей архитектуре. Вот это и есть нарушение принципа подстановки Лисков.

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

Как не наступать на эти грабли? Да очень просто — не делать Квадрат наследником Прямоугольника. Это классический пример, который в учебниках приводят, чтобы показать, что «is-a» отношение (квадрат IS-A прямоугольник) в реальном ООП — часто хуйня полная. Лучше использовать композицию, общий интерфейс Фигура с методом getArea(), или вообще отдельные классы без наследования.

Иначе получается, что подкласс не расширяет, а изменяет поведение родителя. А это, как говорится, чих-пых тебя в сраку, прямой путь к багам, которые ищешь потом полгода.