Какой из принципов SOLID связан с наследованием?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
SOLID принцип Liskov Substitution Principle (LSP)
Liskov Substitution Principle (LSP) — это принцип SOLID, который напрямую связан с наследованием. Он гласит: объекты подклассов должны корректно заменять объекты базового класса без нарушения корректности программы.
Суть принципа
LSP — третий принцип SOLID (после Single Responsibility и Open/Closed), сформулирован Барбарой Лисков в 1987 году:
"Functions that use pointers or references to base classes must be able
to use objects of derived classes without knowing it."
Функции, которые используют указатели или ссылки на базовые классы,
должны иметь возможность использовать объекты производных классов
без знания об этом.
Другими словами: если S является подтипом T, то объекты типа S в программе могут заменяться объектами типа T без нарушения желаемых свойств этой программы.
Пример нарушения LSP
// ❌ НЕПРАВИЛЬНО - нарушает LSP
public class Bird {
public void fly() {
System.out.println("Flying...");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
// Пингвин не может летать!
throw new UnsupportedOperationException("Penguins cannot fly");
}
}
// Использование
public void makeBirdFly(Bird bird) {
bird.fly(); // Может выбросить исключение для Penguin!
}
makeBirdFly(new Bird()); // OK
makeBirdFly(new Penguin()); // Ошибка!
Проблема: при замене Bird на Penguin программа сломается!
Правильное применение LSP
// ✅ ПРАВИЛЬНО - соответствует LSP
public abstract class Bird {
// Базовый класс для всех птиц
}
public class FlyingBird extends Bird {
public void fly() {
System.out.println("Flying...");
}
}
public class Penguin extends Bird {
public void swim() {
System.out.println("Swimming...");
}
}
// Теперь клиентский код различает типы
public void makeFlyingBirdFly(FlyingBird bird) {
bird.fly(); // Безопасно - только для летающих птиц
}
public void makePenguinSwim(Penguin penguin) {
penguin.swim(); // Безопасно
}
makeFlyingBirdFly(new FlyingBird()); // OK
makePenguinSwim(new Penguin()); // OK
Более практичный пример
❌ Нарушение 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;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Square всегда квадрат!
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// Использование нарушает LSP
public void testArea(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
assert rect.getArea() == 20; // Для Rectangle: 5*4=20 OK
// Для Square: 5*5=25 ОШИБКА!
}
testArea(new Rectangle()); // Проходит
testArea(new Square()); // Падает!
✅ Правильное решение
// Базовый интерфейс для фигур
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;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
// Использование
public void testArea(Shape shape) {
// Работает одинаково для всех Shape
int area = shape.getArea();
}
testArea(new Rectangle(5, 4)); // area = 20
testArea(new Square(5)); // area = 25
Ещё один пример: коллекции
❌ Нарушение
public class Stack<E> extends Vector<E> {
// Stack расширяет Vector - нарушает LSP!
// Vector позволяет вставлять в начало/середину
// Stack должен добавлять только в конец
}
✅ Правильно
public interface Collection<E> {
void add(E element);
E remove();
boolean isEmpty();
}
public class Stack<E> implements Collection<E> {
private List<E> items = new ArrayList<>();
@Override
public void add(E element) {
items.add(element); // В конец
}
@Override
public E remove() {
return items.remove(items.size() - 1); // С конца
}
}
Проверка LSP соответствия
Для проверки, соответствует ли подкласс LSP, задай себе вопросы:
-
Может ли подкласс безопасно заменить базовый класс?
// Если нет - нарушение LSP public void doSomething(BaseClass obj) { // Должно работать и для BaseClass и для DerivedClass } -
Усиливает ли подкласс условия выполнения (preconditions)?
// Плохо - усиливает требования class DerivedClass extends BaseClass { @Override public void method(String param) { if (param == null) throw new Exception(); // Усиление! super.method(param); } } -
Ослабляет ли подкласс результаты (postconditions)?
// Плохо - ослабляет гарантии class DerivedClass extends BaseClass { @Override public int getPositiveNumber() { return -5; // Нарушение контракта! } }
Контрактный подход (Design by Contract)
Для правильного применения LSP используй контракты:
public class Account {
/**
* Снимает деньги со счёта
* Precondition: amount > 0 и <= баланс
* Postcondition: баланс уменьшился на amount
*/
public void withdraw(BigDecimal amount) {
if (amount.compareTo(getBalance()) > 0) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
}
}
public class OverdraftAccount extends Account {
@Override
public void withdraw(BigDecimal amount) {
// ✅ Расширяем возможности (overdraft)
// но сохраняем основной контракт
balance = balance.subtract(amount);
}
}
Правила для соблюдения LSP
- Не усиливай предусловия — подкласс должен принимать всё, что принимает базовый класс
- Не ослабляй постусловия — подкласс должен возвращать как минимум то же
- Сохраняй инварианты — условия, которые всегда верны
- Не выбрасывай новые исключения — только которые выбрасывает базовый класс
- Предпочитай композицию наследованию — если поведение существенно отличается
Связь с другими принципами SOLID
- S (SRP) — каждый класс одна ответственность
- O (OCP) — открыт для расширения, закрыт для изменения
- L (LSP) — подклассы взаимозаменяемы (связан с наследованием)
- I (ISP) — специфичные интерфейсы лучше общих
- D (DIP) — зависимости от абстракций, не конкретики
Вывод: LSP — это самый важный принцип для правильного использования наследования в Java. Он гарантирует что полиморфизм работает корректно и подклассы могут безопасно заменять базовые классы.