Заключение
Что важно запомнить о LSP
-
LSP — это про контракт целиком, а не только про сигнатуру.
Предусловия, постусловия и инварианты — вот что составляет контракт. Наследник не может усиливать предусловия (требовать больше, чем родитель), не может ослаблять постусловия (гарантировать меньше), не может нарушать инварианты. Сигнатура может остаться корректной — а контракт при этом нарушенным. -
Главный признак нарушения — разное поведение при одинаковых условиях.
Если три реализации одного интерфейса при одном и том же входе ведут себя по-разному (одна молчит, две падают; одна бросаетKeyNotFoundException, другая ничего не делает) — это сигнал. Клиент не может писать общий код, потому что не знает, чего ожидать от конкретной реализации. -
Инструменты LSP — шаблонный метод, защитные проверки, единые стратегии.
Вместо того чтобы давать наследнику самому решать, что делать с предусловием, вынесите проверку в базовый класс. Вместо!— явная проверка. Вместо разных стратегий для «не найдено» — одна общая. Чем меньше у наследника свободы «сломать контракт», тем лучше. -
Самое коварное — ошибка не проявляется сразу.
Пока реализация одна, нарушение может спать годами. Первый же вызов метода в неподходящем состоянии, первая же вторая реализация интерфейса, первый же тест с моком — и контракт разваливается. Компилятор этого не поймает: C# и TypeScript проверяют сигнатуры, а не поведение. Ловить нужно тестами на подстановку — один тест, все реализации. -
NotImplementedException— это не нарушение LSP, а WIP.
Незавершённый код не анализируется принципом подстановки. Метод, который ещё не написан, ничего не нарушает — он просто не написан. Другое дело — осознанный отказ: если метод остался в интерфейсе «на будущее» и никогда не будет реализован, это уже LSP-нарушение. Различайте эти два состояния. -
LSP помогает другим принципам SOLID.
- OCP (Open-Closed): расширение через наследников возможно только тогда, когда наследники корректно подставляются вместо родителя. Без LSP OCP превращается в лотерею.
- ISP (Interface Segregation): узкие интерфейсы проще соблюсти, чем «толстые». Если интерфейс обещает пять методов, а реализация выполняет три — это и ISP-проблема, и LSP-проблема.
- DIP (Dependency Inversion): подстановка абстракции вместо конкретики работает только тогда, когда конкретика соблюдает контракт абстракции. DI-контейнер — техническая точка LSP.
Помните: LSP — это не про иерархии классов и не про
extends. Это про любую подстановку: интерфейс + реализация, базовый класс + наследник, абстракция + конкретика. Если клиент не может отличить одну реализацию от другой по поведению — LSP соблюдён. Если может — нарушен. И самое главное: LSP — это не про идеальный код с первого раза. Это про осознанность. Когда вы понимаете, что обещает контракт, вы можете спроектировать наследников так, чтобы они это обещание не ломали.
Теперь, когда вы разобрали четыре реальных примера и прошли чек-лист, вы готовы применять LSP в своих проектах. Переходите к следующему принципу — Interface Segregation (ISP).