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

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

Ответ

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