← Назад к вопросам

Что такое L в SOLID?

2.3 Middle🔥 101 комментариев
#ООП и проектирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое L в SOLID: Liskov Substitution Principle

Определение

L (Liskov Substitution Principle) — принцип подстановки Барбары Лискова. Он гласит:

Объекты класса-наследника должны корректно заменять объекты класса-родителя без нарушения функциональности программы.

Другими словами: если класс B наследует класс A, то везде, где используется A, можно подставить B без проблем.

Простой пример нарушения LSP

Рассмотрим классическую ошибку — квадрат и прямоугольник:

class Rectangle {
public:
    virtual void setWidth(double w) { width = w; }
    virtual void setHeight(double h) { height = h; }
    virtual double getArea() const { return width * height; }
    
protected:
    double width = 0;
    double height = 0;
};

class Square : public Rectangle {
public:
    void setWidth(double w) override { 
        width = w;
        height = w;  // квадрат: ширина = высота
    }
    
    void setHeight(double h) override { 
        width = h;
        height = h;
    }
};

Проблема:

void printArea(Rectangle& rect) {
    rect.setWidth(5);
    rect.setHeight(3);
    std::cout << rect.getArea();  // ожидаем 15
}

Square sq;
printArea(sq);  // Вывод: 9!
// Ошибка! Square нарушает контракт Rectangle

Функция printArea() ожидает, что ширина останется 5 и высота 3, но Square переопределяет оба значения. Square невозможно подставить вместо Rectangle.

Правильное решение

Использовать композицию вместо наследования:

class Shape {
public:
    virtual double getArea() const = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double getArea() const override { return width * height; }
    
private:
    double width;
    double height;
};

class Square : public Shape {
public:
    Square(double side) : side(side) {}
    double getArea() const override { return side * side; }
    
private:
    double side;
};

// Теперь работает корректно!
void printArea(const Shape& shape) {
    std::cout << shape.getArea();
}

Еще примеры нарушения LSP

Пример 1: Исключения в переопределенных методах

class Database {
public:
    virtual bool connect(const std::string& url) {
        // может вернуть false
        return tryConnect(url);
    }
};

class MockDatabase : public Database {
public:
    bool connect(const std::string& url) override {
        throw std::runtime_error("Mock doesn't connect");  // НАРУШЕНИЕ!
        // Вызывающий код не ожидает исключения
    }
};

Пример 2: Более слабые предусловия / более сильные постусловия

class Account {
public:
    virtual void withdraw(double amount) {
        // Предусловие: balance >= amount
        if (balance < amount) throw std::runtime_error("Insufficient funds");
        balance -= amount;
    }
};

class CreditAccount : public Account {
public:
    void withdraw(double amount) override {
        // Проблема: принимаем любую сумму (более слабое предусловие)
        balance -= amount;
        if (balance < 0) {
            applyInterest();  // применяем проценты
        }
    }
};

Код, использующий Account, может сломаться с CreditAccount, потому что поведение отличается.

Как соблюдать LSP

1. Не переопределяй поведение неправильно

// Правильно: Square не наследует Rectangle
class Shape { virtual double area() = 0; };
class Rectangle : public Shape { /*...*/ };
class Square : public Shape { /*...*/ };

2. Поддерживай контракт базового класса

class Logger {
public:
    virtual void log(const std::string& message) {
        // Контракт: должно логировать сообщение
    }
};

class FileLogger : public Logger {
public:
    void log(const std::string& message) override {
        // ПРАВИЛЬНО: логируем в файл
        file << message << std::endl;
    }
};

3. Используй инвариант базового класса

class Queue {
    // Инвариант: FIFO порядок
    virtual void push(int value) = 0;
    virtual int pop() = 0;
};

class PriorityQueue : public Queue {  // НАРУШЕНИЕ!
    // PriorityQueue не соблюдает FIFO инвариант
};

// Правильно: отдельная иерархия
class Queue { /*...*/ };
class PriorityQueue { /*...*/ };  // Не наследует Queue

Практическое значение

LSP критичен для:

  • Полиморфизма: если не соблюдать LSP, полиморфизм становится опасным
  • Тестирования: mock-объекты должны корректно заменять реальные
  • Расширяемости: новые классы не должны ломать существующий код
  • Контрактов: гарантирует выполнение договоренности между классами

Заключение

LSP гарантирует, что наследник может безопасно заменить родителя. Это возможно только если:

  1. Поведение соответствует контракту базового класса
  2. Не усиливаются предусловия
  3. Не ослабляются постусловия
  4. Не нарушаются инварианты

Нарушение LSP приводит к трудно находимым ошибкам и хрупкому коду.