Может ли наследник служить заменой своему классу-родителю?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Может ли наследник служить заменой своему родителю
Ответ: Да, это принцип Liskov Substitution Principle (LSP)
Это один из пяти принципов SOLID. Наследник (подкласс) должен быть полностью заменяемым на своего родителя (суперкласс) без нарушения корректности программы.
Определение LSP
"Если S является подтипом T, то объекты типа T в программе могут быть заменены объектами типа S без каких-либо нарушений желаемых свойств программы".
Другими словами: наследник должен усиливать, а не ослаблять контракт родителя.
Пример правильной замены
// Родительский класс
public class Animal {
public void makeSound() {
System.out.println("Некий звук");
}
public void move() {
System.out.println("Движение");
}
}
// Наследники
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Гав!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Мяу!");
}
}
// Собака заменяет Животное
Animal animal = new Dog();
animal.makeSound(); // Гав! — работает как ожидается
// Кошка заменяет Животное
animal = new Cat();
animal.makeSound(); // Мяу! — работает как ожидается
Нарушение LSP
public class Bird extends Animal {
@Override
public void move() {
System.out.println("Летит");
}
}
public class Penguin extends Bird {
@Override
public void move() {
throw new UnsupportedOperationException("Пингвины не летают!");
}
}
// Нарушение LSP:
Bird bird = new Penguin();
bird.move(); // Выбросит исключение!
// Мы не можем заменить Bird на Penguin без изменения логики
Это нарушение LSP, потому что Penguin нарушает контракт Bird — он не может летать.
Правильное решение
// Иерархия классов
public abstract class Bird {
public abstract void move();
}
public class FlyingBird extends Bird {
@Override
public void move() {
System.out.println("Летит");
}
}
public class NonFlyingBird extends Bird {
@Override
public void move() {
System.out.println("Ходит");
}
}
public class Penguin extends NonFlyingBird {
@Override
public void move() {
System.out.println("Ходит и плывёт");
}
}
// Теперь можно безопасно заменять
NonFlyingBird bird = new Penguin();
bird.move(); // Работает корректно
Проблемы нарушения 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;
}
}
// Нарушение LSP:
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
Assert.assertEquals(50, rect.getArea()); // Ожидаем 50, получим 100!
Это нарушение, потому что Square нарушает контракт Rectangle.
Пример с коллекциями
// Правильное использование LSP с интерфейсами
public interface Storage {
void save(String data);
String retrieve(String key);
}
public class FileStorage implements Storage {
@Override
public void save(String data) {
// Сохранить в файл
}
@Override
public String retrieve(String key) {
// Получить из файла
return null;
}
}
public class DatabaseStorage implements Storage {
@Override
public void save(String data) {
// Сохранить в БД
}
@Override
public String retrieve(String key) {
// Получить из БД
return null;
}
}
// Клиент может работать с любой реализацией
public class DataProcessor {
private final Storage storage;
public DataProcessor(Storage storage) {
this.storage = storage; // Может быть FileStorage или DatabaseStorage
}
public void process(String data) {
storage.save(data); // Работает одинаково для всех реализаций
}
}
Правила соблюдения LSP
- Не ослабляй предусловия — наследник не должен требовать больше, чем родитель
// Плохо
public class Parent {
public void method(int x) { // x может быть любым
}
}
public class Child extends Parent {
@Override
public void method(int x) {
if (x < 0) {
throw new IllegalArgumentException(); // Ослабили предусловие
}
}
}
- Не усиляй постусловия — наследник не должен гарантировать меньше
// Плохо
public class Parent {
public List<String> getValues() {
return new ArrayList<>(); // Гарантирует, что не null
}
}
public class Child extends Parent {
@Override
public List<String> getValues() {
return null; // Нарушили контракт
}
}
- Соблюдай инварианты — условия, которые должны быть верны для всех объектов
Практический пример
public interface PaymentProcessor {
void processPayment(double amount);
boolean isSuccessful();
}
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// Обработка платежа по карте
}
@Override
public boolean isSuccessful() {
return true; // Успешно
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// Обработка платежа через PayPal
}
@Override
public boolean isSuccessful() {
return true; // Успешно
}
}
public class CheckoutService {
public void checkout(PaymentProcessor processor, double amount) {
processor.processPayment(amount);
if (processor.isSuccessful()) {
System.out.println("Платёж успешен");
}
}
}
Все реализации PaymentProcessor могут использоваться одинаково.
Итог
Да, наследник должен служить полной заменой родителю (принцип LSP). Это означает:
- Наследник может переопределять методы, но не должен нарушать контракт
- Предусловия не могут ослабляться
- Постусловия не могут усиливаться
- Инварианты должны соблюдаться
- Клиентский код должен работать с наследником так же, как с родителем
Соблюдение LSP делает код более гибким, тестируемым и предсказуемым.