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

Как нарушить принцип Лисков

3.0 Senior🔥 161 комментариев
#SOLID и паттерны проектирования#ООП

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

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

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

Как нарушить принцип Лисков

Принцип подстановки Лисков (Liskov Substitution Principle, LSP) - это один из принципов SOLID. Он гласит: объекты подклассов должны корректно заменять объекты родительского класса, не нарушая логики программы. Давай посмотрим, как его нарушить.

Нарушение 1: Изменение контракта в подклассе

// Базовый класс
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 void testArea(Rectangle rect) {
    rect.setWidth(5);
    rect.setHeight(10);
    assert rect.getArea() == 50;  // Ожидаем 50
}

Rectangle rect = new Square();  // Используем Square как Rectangle
testArea(rect);  // ПАДАЕТ! Square вернёт 100, а не 50

Нарушение 2: Выброс неожиданных исключений

public class PaymentService {
    public void processPayment(double amount) {
        // Контракт: выполнить платёж
    }
}

public class CreditCardPaymentService extends PaymentService {
    @Override
    public void processPayment(double amount) {
        // Нарушение: выбрасываем исключение, которого не было в базовом классе
        if (amount < 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        // Это нарушение LSP, если базовый класс не объявляет это исключение
    }
}

// Клиентский код не ожидает исключение
public void handlePayment(PaymentService service) {
    service.processPayment(-10);  // Не ожидает IllegalArgumentException!
}

Нарушение 3: Ослабление предусловий

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

public class UnlimitedAccount extends BankAccount {
    @Override
    public void withdraw(double amount) {
        // Нарушение: переопределяем условие
        // Теперь можем снять сколько угодно (включая отрицательные суммы)
        balance -= amount;  // Даже если amount > balance!
    }
}

// Проблема:
public void transferMoney(BankAccount account, double amount) {
    account.withdraw(amount);  // Ожидаем, что выбросится исключение если не хватает денег
}

BankAccount account = new UnlimitedAccount();
account.deposit(100);
transferMoney(account, 500);  // Не выбросит исключение! LSP нарушен

Нарушение 4: Усиление постусловий (ослабление результата)

public class DataRepository {
    // Контракт: вернуть список пользователей (никогда null)
    public List<User> getAllUsers() {
        return new ArrayList<>();  // Минимум пустой список
    }
}

public class CachedDataRepository extends DataRepository {
    @Override
    public List<User> getAllUsers() {
        if (cacheExpired) {
            return null;  // Нарушение! Клиент не ожидает null
        }
        return cache;
    }
}

// Проблема:
DataRepository repo = new CachedDataRepository();
List<User> users = repo.getAllUsers();
for (User user : users) {  // NullPointerException!
    System.out.println(user.getName());
}

Нарушение 5: Изменение поведения типов исключений

public class DatabaseConnection {
    public void connect() throws DatabaseException {
        // Контракт: выбросит DatabaseException
    }
}

public class MySQLConnection extends DatabaseConnection {
    @Override
    public void connect() throws SQLException {
        // Нарушение: выбросит SQLException вместо DatabaseException
        // Клиент не ловит SQLException!
    }
}

public void establishConnection(DatabaseConnection conn) {
    try {
        conn.connect();
    } catch (DatabaseException e) {  // Ловим DatabaseException
        // Но если это MySQLConnection, выбросит SQLException!
    }
}

Как НЕ нарушать LSP

Правильный вариант: Composition вместо Inheritance

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

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

// Вместо наследования используем общий интерфейс
public interface Shape {
    int getArea();
}

public class Square implements Shape {
    public int getArea() { /* ... */ }
}

public class Rectangle implements Shape {
    public int getArea() { /* ... */ }
}

Ключевые моменты LSP

  1. Подклассы должны расширять функциональность, а не изменять её
  2. Не ослабляйте предусловия (требования к входным параметрам)
  3. Не усиливайте постусловия (требования к результатам)
  4. Не выбрасывайте неожиданные исключения
  5. Сохраняйте инварианты (постоянные свойства)

Вывод

Принцип Лисков нарушается, когда подкласс не может корректно заменить родительский класс. Это приводит к ошибкам, которые сложно отследить. Лучше использовать Composition и Interface Segregation вместо наследования, когда есть сомнения.