Вопрос требует объяснения сути принципа LSP, который гарантирует, что объекты производных классов могут быть использованы вместо объектов базовых классов без изменения корректности программы.
Принцип подстановки Лисков гласит: функции, которые используют ссылку на базовый класс, должны иметь возможность использовать объекты производных классов, не зная об этом. Наследник не должен усиливать предусловия, ослаблять постусловия и не должен изменять поведение, ожидаемое от базового класса. Классический пример нарушения — квадрат, наследующий от прямоугольника и ломающий его логику.
LSP является расширением полиморфизма и обеспечивает логическую согласованность в иерархии наследования. Нарушение этого принципа приводит к неожиданным ошибкам, которые сложно обнаружить.
Основные правила, вытекающие из LSP:
Синтаксическая совместимость:
Наследник должен иметь те же публичные методы и свойства, что и родитель. Это обеспечивается компилятором.
Семантическая совместимость (поведенческая):
Это самая важная и сложная часть. Поведение наследника не должно противоречить поведению, обещанному базовым классом.
Ослабление предусловий (Preconditions): Наследник не может требовать большего, чем базовый класс. Например, если метод базового класса принимает любой целый число, метод наследника не может требовать только положительные числа.
Усиление постусловий (Postconditions): Наследник не может гарантировать меньше, чем базовый класс. Например, если метод базового класса гарантирует, что база данных будет подключена после вызова, наследник не может нарушать это обещание.
Сохранение инвариантов (Invariants): Наследник должен сохранять все логические условия, которые всегда истинны для базового класса.
Классический пример нарушения LSP: "Квадрат/Прямоугольник"
class Rectangle {
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
class Square : Rectangle {
public override int Width {
get => base.Width;
set { base.Width = value; base.Height = value; } // Нарушение инварианта!
}
public override int Height {
get => base.Height;
set { base.Height = value; base.Width = value; } // Нарушение инварианта!
}
}
class Program {
static void UseRectangle(Rectangle rect) { // Функция ожидает Rectangle
rect.Width = 5;
rect.Height = 4;
Console.WriteLine($"Expected area: 20, Got: {rect.Area}"); // Ожидается 20
}
static void Main() {
Rectangle rect = new Rectangle();
UseRectangle(rect); // Вывод: Expected area: 20, Got: 20 - OK.
Rectangle square = new Square(); // Подстановка наследника
UseRectangle(square); // Вывод: Expected area: 20, Got: 16 - ОШИБКА!
}
}В этом примере Square нарушает LSP, потому что он меняет ожидаемое поведение: после установки ширины высота изменяется вслед за ней, что ломает логику метода UseRectangle, ожидающего независимого управления шириной и высотой.
Вывод:
LSP — это руководство по созданию корректных иерархий наследования. Если подтип нельзя прозрачно подставить вместо базового типа, значит, сама архитектура наследования неверна. Часто решением является композиция вместо наследования.