12.1.3 — третья буква SOLID12.1.3 — the third letter of SOLID

Liskov Substitution Principle (LSP)

Объекты базового типа должны быть заменяемы объектами подтипа без нарушения корректности программы. Формализует, что вообще значит «правильное» наследование — не синтаксическое (подтип компилируется на месте базового), а поведенческое.Objects of a base type must be replaceable with objects of a subtype without breaking the program's correctness. It formalizes what "correct" inheritance actually means — not syntactic (the subtype compiles in place of the base type), but behavioral.

частый вопрос на собеседованииcommon interview question SOLID · 3/5

Что этоWhat it is

Сформулирован Барбарой Лисков (1987): пусть φ(x) — свойство, доказуемое для объектов x типа T. Тогда φ(y) должно быть верно для объектов y типа S, если S — подтип T. На практике это выражается через Design by Contract (Бертран Мейер): подкласс не может усиливать пред-условия (требовать больше от вызывающего кода, чем базовый класс) и не может ослаблять пост-условия (гарантировать меньше, чем обещал базовый класс).Formulated by Barbara Liskov (1987): let φ(x) be a property provable for objects x of type T. Then φ(y) must hold for objects y of type S, where S is a subtype of T. In practice this is expressed through Design by Contract (Bertrand Meyer): a subclass must not strengthen preconditions (demand more from calling code than the base class did) and must not weaken postconditions (guarantee less than the base class promised).

Классический контрпример: прямоугольник и квадратThe classic counterexample: rectangle and square

Математически квадрат — это прямоугольник (частный случай), поэтому интуитивно кажется естественным сделать Square наследником Rectangle. На практике это нарушает LSP, потому что поведенческий контракт Rectangle — «ширину и высоту можно менять независимо» — Square обязан нарушить, чтобы остаться квадратом.Mathematically a square is a rectangle (a special case), so it feels natural to make Square inherit from Rectangle. In practice this violates LSP, because Rectangle's behavioral contract — "width and height can be changed independently" — is exactly what Square must break in order to remain a square.

нарушение LSP — Square ломает контракт RectangleLSP violation — Square breaks Rectangle's contract
class Rectangle {
public:
    virtual void setWidth(double w) { width = w; }
    virtual void setHeight(double h) { height = h; }
protected: double width, height;
};

class Square : public Rectangle {
public:
    void setWidth(double w) override { width = height = w; }  // побочный эффект!// a side effect!
    void setHeight(double h) override { width = height = h; } // побочный эффект!// a side effect!
};

// код, написанный против контракта Rectangle, ломается на Square:// code written against Rectangle's contract breaks on Square:
void test(Rectangle& r) {
    r.setWidth(5); r.setHeight(4);
    assert(r.area() == 20);  // падает для Square: area() == 16// fails for Square: area() == 16
}
без наследования — общий интерфейс Shapeno inheritance — a shared Shape interface
struct Shape { virtual double area() const = 0; };
struct Rectangle : Shape {
    double width, height;
    double area() const override { return width * height; }
};
struct Square : Shape {
    double side;
    double area() const override { return side * side; }
};
// Square больше не притворяется Rectangle — контракт Shape (только area())
// не диктует, что width и height меняются независимо// Square no longer pretends to be a Rectangle — the Shape contract (just area())
// never dictates that width and height vary independently

LSP за пределами геометрииLSP beyond geometry

01

Исключения как усиление пред-условийExceptions as strengthened preconditions

Подкласс, выбрасывающий исключение в ситуации, где базовый класс его не выбрасывал (например, ReadOnlyList.add(), наследующий от List, но кидающий UnsupportedOperationException), нарушает LSP: код, написанный против контракта базового класса, ожидает, что add() отработает без исключения.A subclass that throws an exception in a situation where the base class didn't (say, ReadOnlyList.add() inheriting from List but throwing UnsupportedOperationException) violates LSP: code written against the base class's contract expects add() to succeed without an exception.

02

Rust: trait bounds как явный контракт вместо наследованияRust: trait bounds as an explicit contract instead of inheritance

Отсутствие наследования реализации в Rust структурно устраняет целый класс LSP-нарушений: любой тип, реализующий trait Shape, обязан предоставить именно ту сигнатуру, которую объявляет трейт — компилятор не позволит «частично» реализовать контракт с иным поведением сигнатуры.The absence of implementation inheritance in Rust structurally eliminates an entire class of LSP violations: any type implementing trait Shape must provide exactly the signature the trait declares — the compiler won't allow a "partial" implementation with a differently-behaving signature.

ГраблиGotchas

Компилятор проверяет только сигнатуры, не поведениеThe compiler checks only signatures, not behavior

Square-наследник-Rectangle компилируется без единого предупреждения — типовая система любого мейнстримного языка не может статически проверить, что переопределённый метод не нарушает поведенческий контракт. LSP — принцип дизайна, а не то, что ловит компилятор.Square-inheriting-from-Rectangle compiles without a single warning — the type system of any mainstream language cannot statically verify that an overridden method doesn't break the behavioral contract. LSP is a design principle, not something the compiler catches.

«Is-a» отношение из предметной области ≠ корректное наследование в кодеA domain "is-a" relationship ≠ correct inheritance in code

Прямоугольник-квадрат — хрестоматийный пример того, как математически верное отношение «is-a» ломается на уровне изменяемого состояния и методов. Критерий LSP — это совместимость поведения (что можно делать с объектом), а не совместимость понятий предметной области.Rectangle-square is the textbook example of how a mathematically valid "is-a" relationship breaks down at the level of mutable state and methods. LSP's criterion is behavioral compatibility (what can be done with the object), not conceptual compatibility in the domain model.

Ковариантность возвращаемого типа — не автоматическое нарушениеCovariant return types aren't an automatic violation

Переопределённый метод, возвращающий более специфичный тип (ковариантный возврат), не нарушает LSP — это разрешённое ослабление в правильную сторону: вызывающий код, ожидающий базовый тип, прекрасно принимает и подтип.An overridden method returning a more specific type (a covariant return) does not violate LSP — it's a permitted relaxation in the right direction: calling code expecting the base type happily accepts a subtype too.