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

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

2.0 Middle🔥 181 комментариев
#SOLID и паттерны проектирования

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

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

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

Что такое буква 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 делает код более предсказуемым, тестируемым и снижает количество неожиданных ошибок.

Что такое буква L в SOLID? | PrepBro