Что такое буква L в SOLID?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое буква L в SOLID? (Liskov Substitution Principle)
L в SOLID это Liskov Substitution Principle (LSP) — принцип подстановки Барбары Лисков. Он гласит:
Объекты подклассов должны корректно заменять объекты суперклассов, не нарушая работу программы.
Другими словами: если класс B наследуется от класса A, то везде где используется A, должно быть безопасно использовать B.
Основная идея
Общее правило:
Если Bird — это суперкласс, а Penguin и Sparrow — подклассы,
ТО везде где программа ожидает Bird, она может получить Penguin или Sparrow
и всё должно работать корректно.
Программа не должна знать точный тип Bird,
и не должна вести себя по-разному для разных подтипов.
Классический пример нарушения LSP
❌ Плохо:
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
public class Sparrow extends Bird {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins cannot fly!");
}
}
// Использование
public void makeBirdFly(Bird bird) {
bird.fly(); // Что будет если это Penguin? Исключение!
}
Bird bird = new Penguin();
makeBirdFly(bird); // 💥 UnsupportedOperationException
Здесь Penguin нарушает LSP: функция ожидает, что любой Bird может летать, но Penguin выкидывает исключение.
✅ Правильно:
// Базовый класс для всех птиц
public abstract class Bird {
abstract void move();
}
// Летающие птицы
public class Sparrow extends Bird {
@Override
public void move() {
System.out.println("Sparrow is flying");
}
}
// Не-летающие птицы
public class Penguin extends Bird {
@Override
public void move() {
System.out.println("Penguin is swimming");
}
}
// Теперь функция работает со всеми подтипами
public void makeMoving(Bird bird) {
bird.move(); // Работает для любой птицы!
}
Bird sparrow = new Sparrow();
Bird penguin = new Penguin();
makeBirdFly(sparrow); // ✅ OK
makeBirdFly(penguin); // ✅ OK (плывёт)
Более реалистичный пример
❌ Нарушение LSP:
public 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;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int w) {
this.width = w;
this.height = w; // Квадрат имеет равные стороны!
}
@Override
public void setHeight(int h) {
this.width = h;
this.height = h; // Нарушение ожиданий базового класса
}
}
// Проблема
public void printArea(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
System.out.println(rect.getArea()); // Ожидаем 20
}
Rectangle shape = new Square();
printArea(shape); // Выведет 16, а не 20! 💥
Около Square переопределил методы, но нарушил контракт Rectangle.
✅ Правильно:
// Интерфейс для фигур
public interface Shape {
int getArea();
}
// Независимые реализации
public class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
public int getArea() {
return side * side;
}
}
// Работает корректно
public void printArea(Shape shape) {
System.out.println(shape.getArea());
}
Shape rect = new Rectangle(5, 4);
Shape square = new Square(5);
printArea(rect); // 20 ✅
printArea(square); // 25 ✅
Примеры нарушения LSP
1. Выкидывание исключений в переопределённых методах
public class DataProcessor {
public void process(List<Integer> data) {
// Обрабатывает данные
}
}
public class SpecialDataProcessor extends DataProcessor {
@Override
public void process(List<Integer> data) {
if (data.isEmpty()) {
throw new IllegalArgumentException("Cannot process empty list");
}
// Обработка
}
}
// Нарушение: клиент ожидает обработки любого списка
DataProcessor processor = new SpecialDataProcessor();
processor.process(new ArrayList<>()); // 💥 Исключение!
2. Ослабление контрактов
public class PaymentProcessor {
public void processPayment(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
// Обработка платежа
}
}
public class DiscountPaymentProcessor extends PaymentProcessor {
@Override
public void processPayment(double amount) {
// Ослабили контракт: теперь принимаем и отрицательные суммы
// Это нарушает ожидания!
System.out.println("Processing: " + amount);
}
}
3. Изменение семантики методов
public class Employee {
public double calculateBonus() {
return salary * 0.1; // 10% от зарплаты
}
}
public class Manager extends Employee {
@Override
public double calculateBonus() {
return salary * 0.5; // Менеджер получает 50%
}
}
// Проблема: клиент не знает, что бонус может быть другим
// Это вводит в заблуждение относительно того, что он получит
Как избежать нарушения LSP
1. Используйте инвариантное наследование
Перед наследованием спросите себя:
- Является ли подкласс специализацией суперкласса?
- Может ли подкласс безопасно заменить суперкласс везде?
// Хорошо: Employee специализирует Person
public class Person { }
public class Employee extends Person { }
// Плохо: Square не является полной заменой Rectangle
public class Rectangle { }
public class Square extends Rectangle { } // Нарушение!
2. Не переопределяйте поведение, создавайте новые типы
// Вместо наследования с переопределением
// Создавайте разные типы через interface
public interface PaymentMethod {
void pay(double amount);
}
public class CreditCard implements PaymentMethod {
public void pay(double amount) { }
}
public class BitCoin implements PaymentMethod {
public void pay(double amount) { }
}
3. Проверяйте контракт метода
Документируйте ожидания (preconditions и postconditions):
public class Repository {
/**
* Возвращает запись по ID.
* @param id Должна быть > 0
* @return Никогда не возвращает null
*/
public abstract Record findById(int id);
}
public class CustomRepository extends Repository {
@Override
public Record findById(int id) {
// ✅ Корректное переопределение
// Не требует id > 0, но может это требовать (усиление предусловия OK)
// Возвращает null - НАРУШЕНИЕ! (ослабление постусловия BAD)
if (id < 0) return null;
return super.findById(id);
}
}
LSP и контрактное программирование
LSP связан с концепцией Дизайна по Контракту (Design by Contract):
Предусловие (precondition): что нужно для входа
Постусловие (postcondition): что гарантируется на выходе
В подклассе:
- Можно УСИЛИТЬ предусловие (требовать больше)
- Можно ОСЛАБИТЬ предусловие (требовать меньше) ← LSP позволяет
- НЕЛЬЗЯ ОСЛАБИТЬ постусловие
- Можно УСИЛИТЬ постусловие
Заключение
Liskov Substitution Principle гарантирует, что наследование используется правильно. Если класс B наследуется от A, то везде где программа работает с A, она может работать и с B. Соблюдение LSP делает код более предсказуемым, тестируемым и снижает количество неожиданных ошибок.