Главное
Объяснение
Объекты в программе должны быть заменяемы экземплярами их подтипов без изменения правильности выполнения программы.
Если проще: наследник должен дополнять, а не заменять поведение родителя.
По факту это значит, что любая подстановка — интерфейс + реализация, базовый класс + наследник, абстракция + конкретика — должна быть безопасной: клиент не должен замечать разницы. Если после замены одной реализации на другую программа начинает вести себя иначе (падать, молча игнорировать ошибки, возвращать не то, что обещала сигнатура) — LSP нарушен.
LSP — это не только про сигнатуру. Это про контракт целиком: предусловия, постусловия и инварианты. Наследник не может усиливать предусловия (требовать больше, чем родитель), не может ослаблять постусловия (гарантировать меньше), не может нарушать инварианты. Сигнатура может оставаться корректной, а контракт — нарушенным.
Примеры
Что за примеры?
-
Conversation в uust-schedule-bot — три реализации одного типа по-разному реагируют на отсутствие
telegramId. Одна молча завершается, две бросают исключение, и все три по-разному работают с сессией до проверки. Эталонное нарушение LSP через неявные предусловия и разный порядок операций. -
BasePlayer в buckshot-bot — абстрактный класс обещает «выполнить ход и вернуть результат», а три наследника ломают это обещание тремя разными способами: неявное предусловие (
!), деление на ноль, исключение из-за пользовательского ввода. Классическое усиление постусловия. -
IProductRepository в CsTestShop — пример согласованного контракта с одной брешью. Два метода из пяти ведут себя иначе, чем остальные, при отсутствии записи: не бросают ошибку и не возвращают
false, а молча ничего не делают. На первый взгляд не проблема — но первая же вторая реализация интерфейса сломает подстановку. -
IUserRepository в CsTestShop — пример правильной подстановки через DI-контейнер.
AddScoped<IUserRepository, UserRepository>()— это декларация LSP: контейнер обещает, что клиент не заметит разницы между абстракцией и реализацией. И в завершённом коде это обещание выполняется. Отдельно разбирается, почемуNotImplementedExceptionв WIP-методах — это не нарушение LSP.
Важно: LSP — это не только про иерархии классов. Это про любую подстановку. Если вы видите, что три реализации одного типа ведут себя по-разному в одинаковой ситуации — это сигнал, что контракт неполный. Делайте его явным: через шаблонный метод, через защитные проверки, через единообразную стратегию для одинаковых ситуаций.