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

Расскажи про принцип Барбары Лисков

1.0 Junior🔥 141 комментариев
#Основы Java

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

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

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

Принцип Барбары Лисков (Liskov Substitution Principle)

Что это такое?

Принцип подстановки Барбары Лисков (LSP) — это принцип объектно-ориентированного программирования, который гласит:

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

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

Формальное определение

Если S является подтипом T, то объекты типа S могут быть заменены объектами типа T (т.е. объекты типа T в программе могут быть заменены объектами типа S) без нарушения желаемых свойств этой программы.

// T — родительский класс
public abstract class PaymentMethod {
    abstract public void pay(double amount);
}

// S — подтип (наследник)
public class CreditCard extends PaymentMethod {
    @Override
    public void pay(double amount) {
        // Реализация оплаты кредитной картой
    }
}

public class PayPal extends PaymentMethod {
    @Override
    public void pay(double amount) {
        // Реализация оплаты PayPal
    }
}

// Везде где нужен PaymentMethod, можно использовать CreditCard или PayPal
public class Order {
    public void processPayment(PaymentMethod method, double amount) {
        method.pay(amount); // ← S (CreditCard, PayPal) может заменить T (PaymentMethod)
    }
}

Пример нарушения LSP (антипаттерн)

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

public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    public int getArea() {
        return width * height;
    }
}

// ❌ НАРУШЕНИЕ LSP
public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // ← Квадрат: ширина = высоте
    }
    
    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height; // ← Квадрат: высота = ширине
    }
}

// Проблема
public class Main {
    public static void test(Rectangle rect) {
        rect.setWidth(5);
        rect.setHeight(3);
        System.out.println("Area: " + rect.getArea()); // Ожидаем 15
    }
    
    public static void main(String[] args) {
        test(new Rectangle()); // ✅ Работает: Area = 15
        test(new Square()); // ❌ ОШИБКА: Area = 9 (3*3), а не 15!
    }
}

Почему это нарушение LSP?

  • Мы ожидаем, что Square (подклассы Rectangle) работает как Rectangle
  • Но Square нарушает контракт Rectangle: setWidth и setHeight работают по-другому
  • Программа дает неправильный результат

Как исправить нарушение LSP

Вариант 1: Не использовать наследование (композиция)

// Правильная иерархия
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 void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    
    @Override
    public int getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    private int side;
    
    public Square(int side) {
        this.side = side;
    }
    
    public void setSide(int side) { this.side = side; }
    
    @Override
    public int getArea() {
        return side * side;
    }
}

// Теперь оба класса удовлетворяют LSP
public class Main {
    public static void test(Shape shape) {
        System.out.println("Area: " + shape.getArea());
    }
    
    public static void main(String[] args) {
        test(new Rectangle(5, 3)); // ✅ Area = 15
        test(new Square(3)); // ✅ Area = 9
    }
}

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

Ошибка 1: Переопределение метода с другим контрактом

// Родитель
public class BankAccount {
    public void withdraw(double amount) throws InsufficientFundsException {
        // Выброс исключения если недостаточно средств
        if (amount > balance) {
            throw new InsufficientFundsException();
        }
        balance -= amount;
    }
}

// ❌ НАРУШЕНИЕ LSP
public class GoldAccount extends BankAccount {
    @Override
    public void withdraw(double amount) {
        // Золотой аккаунт позволяет овердрафт (отрицательный баланс)
        balance -= amount; // Не выбрасывает исключение!
    }
}

// Проблема: кто-то ожидает InsufficientFundsException
public void processTransaction(BankAccount account, double amount) {
    try {
        account.withdraw(amount); // Может не выбросить исключение!
    } catch (InsufficientFundsException e) {
        System.out.println("Недостаточно средств");
    }
}

Ошибка 2: Более строгие условия в подклассе

// Родитель
public class Bird {
    public void fly() {
        // Любая птица может летать
    }
}

// ❌ НАРУШЕНИЕ LSP
public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Пингвины не летают");
    }
}

// Проблема
public void letBirdFly(Bird bird) {
    bird.fly(); // Выбросит исключение если это пингвин!
}

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

public interface Bird {
    // Не предполагаем полет
}

public interface FlyingBird extends Bird {
    void fly();
}

public class Eagle implements FlyingBird {
    @Override
    public void fly() {
        System.out.println("Орел летит");
    }
}

public class Penguin implements Bird {
    public void swim() {
        System.out.println("Пингвин плывет");
    }
    // Penguin не реализует FlyingBird
}

Принцип LSP в контексте типов возвращаемых значений

Ковариантность возвращаемого типа (OK в Java 5+):

public class Animal {
    public Animal reproduce() {
        return new Animal();
    }
}

// ✅ ПРАВИЛЬНО (ковариантность)
public class Dog extends Animal {
    @Override
    public Dog reproduce() { // Можно возвращать Dog вместо Animal
        return new Dog();
    }
}

// Это нарушает LSP если вернуть более общий тип
// Но ковариантность для возвращаемых типов разрешена

Контравариантность параметров (Java не поддерживает, но контролируй вручную):

public class PaymentProcessor {
    public void process(CreditCard card) {
        // Обработка платежа по кредитной карте
    }
}

// ❌ НАРУШЕНИЕ LSP (усиление предусловия)
public class SecurityPaymentProcessor extends PaymentProcessor {
    @Override
    public void process(VIPCreditCard card) {
        // Требует VIP карту, но контракт обещал CreditCard
    }
}

// Проблема: обычная CreditCard не пройдет
public void pay(PaymentProcessor processor, CreditCard card) {
    processor.process(card); // Может выбросить ClassCastException
}

Реальный пример: коллекции Java

List соответствует LSP:

public interface Collection<E> {
    boolean add(E e);
    boolean remove(Object o);
}

public class ArrayList<E> implements Collection<E> {
    @Override
    public boolean add(E e) {
        // Добавляет элемент, может увеличить емкость
    }
}

public class LinkedList<E> implements Collection<E> {
    @Override
    public boolean add(E e) {
        // Добавляет элемент в конец списка
    }
}

// ✅ LSP соблюдается
public <E> void printCollection(Collection<E> collection) {
    for (E item : collection) {
        System.out.println(item);
    }
}

Collection<String> array = new ArrayList<>();
Collection<String> linked = new LinkedList<>();
printCollection(array); // ✅ Работает
printCollection(linked); // ✅ Работает

Best Practices для LSP

1. Проверяй постусловия и предусловия

// Родитель обещает: если amount > 0, вычтет из баланса
public abstract class Account {
    public abstract void withdraw(double amount);
}

// Подкласс должен сохранить контракт
public class SavingsAccount extends Account {
    @Override
    public void withdraw(double amount) {
        if (amount <= 0) throw new IllegalArgumentException();
        // Вычитаем из баланса
    }
}

2. Используй интерфейсы для определения контракта

public interface PaymentMethod {
    void pay(double amount) throws PaymentException;
}

public class CreditCard implements PaymentMethod {
    @Override
    public void pay(double amount) throws PaymentException {
        // Реализация
    }
}

3. Избегай выброса неожиданных исключений в подклассах

// ❌ Плохо
public class BrokenPaymentMethod implements PaymentMethod {
    @Override
    public void pay(double amount) {
        throw new RuntimeException("Не реализовано");
    }
}

// ✅ Хорошо
public class MockPaymentMethod implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Mock: платеж " + amount);
    }
}

4. Используй @Override для явного переопределения

// @Override помогает обнаружить ошибки
public class Dog extends Animal {
    @Override // IDE предупредит если метода нет в родителе
    public void bark() { }
}

Выводы

  • LSP гарантирует, что подклассы могут заменять суперклассы без нарушения корректности
  • Нарушение LSP приводит к неожиданному поведению программы
  • Иерархия Square extends Rectangle — классический пример нарушения LSP
  • Используй интерфейсы вместо наследования когда семантика различается
  • Контракт (предусловия, постусловия) должен сохраняться в подклассах
  • LSP связан с Single Responsibility Principle (SRP)
  • Правильное применение LSP делает код более предсказуемым и безопасным
Расскажи про принцип Барбары Лисков | PrepBro