Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое 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 гарантирует, что наследник может безопасно заменить родителя. Это возможно только если:
- Поведение соответствует контракту базового класса
- Не усиливаются предусловия
- Не ослабляются постусловия
- Не нарушаются инварианты
Нарушение LSP приводит к трудно находимым ошибкам и хрупкому коду.