EssayAI
Блог
Блог
Математика и алгоритмы

Принцип подстановки Барбары Лисков (LSP): суть и примеры

19 июня 2026Время чтения: 8 минут
#LSP#принцип подстановки Лисков#SOLID#подтипы#контракты
Принцип подстановки Барбары Лисков (LSP): суть и примеры

Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP) - это буква L в аббревиатуре SOLID и одно из самых неправильно понимаемых правил объектно-ориентированного дизайна. Формулировка короткая: объекты базового типа должны быть заменяемы объектами производного типа без нарушения корректности программы. На практике за этой фразой стоит строгая теория контрактов, ковариантности и контравариантности, а самое известное её нарушение - задача про квадрат и прямоугольник, которая кажется естественной, но ломает наследование. Разберём, что именно требует LSP, как формализуется подстановочность и как отличить корректный подтип от ловушки.

Чтобы быстро проверить собственную иерархию классов на нарушение LSP, опишите её в инструменте ниже - он соберёт запрос и покажет разбор контрактов.

Формулировка принципа

Барбара Лисков сформулировала свой принцип в 1987 году на конференции OOPSLA, а строгое определение дала вместе с Жанетт Уинг в 1994-м. В оригинале оно звучит так: если ϕ(x)\phi(x) - свойство, доказуемое для объектов xx типа TT, то ϕ(y)\phi(y) должно быть истинно для объектов yy типа SS, где SS - подтип TT.

Иными словами, везде, где код работает с базовым типом, можно без оговорок подставить любой его наследник, и поведение останется корректным. Подтип не просто наследует интерфейс - он обязан соблюдать поведенческий контракт базового типа. Это сильнее обычного наследования: компилятор проверяет совместимость сигнатур, но LSP требует совместимости поведения.

Схема подстановочности: ячейка базового типа, в которую без зазора входят подтипы, и красный неподходящий подтип сбоку
Схема подстановочности: ячейка базового типа, в которую без зазора входят подтипы, и красный неподходящий подтип сбоку

Ключевое слово здесь - «доказуемое свойство». Если клиентский код рассчитывает, что метод не выбрасывает исключение или возвращает положительное число, наследник не имеет права это свойство нарушить. Принцип тесно связан с принципом инверсии зависимостей: корректные подтипы позволяют расширять систему без правки клиентского кода, опираясь на абстракции.

Контракты: предусловия и постусловия

Поведенческий контракт метода описывается тремя вещами: предусловиями (что должно быть истинно до вызова), постусловиями (что гарантируется после) и инвариантами (что остаётся истинным всегда). LSP накладывает на переопределённые методы жёсткие правила:

  • Предусловия нельзя усиливать в подтипе. Если базовый метод принимает любое целое число, наследник не вправе требовать только положительные - иначе вызывающий код, написанный под базовый тип, сломается.
  • Постусловия нельзя ослаблять. Если база гарантирует непустой результат, наследник обязан гарантировать как минимум то же самое, а может и больше.
  • Инварианты базового типа должны сохраняться наследником.

Формально, если PTP_T и QTQ_T - пред- и постусловия базового метода, а PSP_S и QSQ_S - наследника, то требуется:

PTPSиQSQTP_T \Rightarrow P_S \quad \text{и} \quad Q_S \Rightarrow Q_T

Предусловие может только ослабляться (принимать больше входов), постусловие - только усиливаться (гарантировать больше). Это и есть математическая запись «заменяемости».

Правило истории и инварианты

Помимо пред- и постусловий, Лисков и Уинг ввели правило истории (history constraint). Подтип не должен позволять изменения состояния, недопустимые для базового типа. Классический пример - неизменяемый базовый тип Точка и наследник ИзменяемаяТочка: добавив сеттер координат, наследник позволяет менять то, что базовый контракт считал константой. Клиент, рассчитывавший на неизменность, получит сюрприз.

Простой тест на правило истории: если у базового типа объект «однажды создан и не меняется», а наследник вводит мутирующие методы, вы почти наверняка нарушаете LSP. Неизменяемость - это инвариант, и его нельзя отменить в подтипе.

Инварианты особенно коварны, потому что не видны в сигнатурах. Компилятор пропустит код, а нарушение проявится только в рантайме, когда клиент сделает допущение, верное для базы, но ложное для наследника.

Классический контрпример: Квадрат и Прямоугольник

Самое известное нарушение LSP - попытка унаследовать Квадрат от Прямоугольник. Геометрически квадрат является прямоугольником, и наследование кажется очевидным. Но поведенчески оно ломается.

Сопоставление: прямоугольник со сторонами ширина и высота и квадрат, у которого изменение ширины тянет за собой высоту
Сопоставление: прямоугольник со сторонами ширина и высота и квадрат, у которого изменение ширины тянет за собой высоту

У прямоугольника есть независимые setШирину и setВысоту. Контракт прямоугольника гласит: после setШирину(5) ширина равна 5, а высота не изменилась. Квадрат обязан держать стороны равными, поэтому его setШирину(5) вынужден менять и высоту. Это ослабляет постусловие прямоугольника - нарушение прямое.

Клиентский код, написанный под прямоугольник, сломается:

r.setШирину(5); r.setВысоту(4);ожидается: S=5×4=20для Квадрата: S=4×4=16\begin{aligned} &\texttt{r.setШирину}(5);\ \texttt{r.setВысоту}(4); \\ &\text{ожидается: } S = 5 \times 4 = 20 \\ &\text{для Квадрата: } S = 4 \times 4 = 16 \end{aligned}

Функция, проверяющая площадь, пройдёт на прямоугольнике и упадёт на квадрате - а ведь квадрат подставлен туда, где ждали прямоугольник. Это и есть нарушение подстановочности. Вывод не в том, что «квадрат не прямоугольник», а в том, что отношение наследования должно отражать поведение, а не житейскую таксономию.

Ковариантность и контравариантность

LSP формализует, как могут меняться типы параметров и возвращаемых значений при переопределении:

  • Возвращаемые типы ковариантны: наследник может вернуть более конкретный тип, чем база. Это безопасно - клиент ждёт базовый тип, а получает его частный случай.
  • Типы параметров контравариантны: наследник может принимать более общий тип, чем база. Тоже безопасно - он способен обработать всё, что обработала бы база, и даже больше.

Эти правила - прямое следствие условий на пред- и постусловия. Параметр метода участвует в предусловии (его нельзя сужать), результат - в постусловии (его можно сужать). Большинство языков поддерживает ковариантность возвращаемого типа, но контравариантность параметров встречается реже - её строго реализует, например, Eiffel и (частично) Scala.

Как обнаружить нарушение

На практике LSP нарушают, когда наследник:

  • бросает исключение там, где база его не бросала (например, NotImplementedException в переопределённом методе);
  • усиливает предусловия - требует более узкий вход;
  • возвращает null или пустой результат там, где база гарантировала значение;
  • молча игнорирует вызов метода, который у базы делал работу;
  • проверяет тип через instanceof в клиентском коде - это почти всегда симптом того, что подтипы ведут себя по-разному.

Последний пункт - самый надёжный индикатор. Если клиентскому коду приходится спрашивать «а какой именно это подтип?», подстановочность уже сломана: клиент не может работать с базовым типом единообразно. Чтобы отделить корректную иерархию от мнимой, полезно сверяться и со смежными принципами SOLID.

Как чинить нарушения

Нарушение LSP - почти всегда сигнал неправильной декомпозиции. Типовые лекарства:

  • Композиция вместо наследования. Если квадрат не подставляется вместо прямоугольника, не наследуйте - сделайте оба наследниками общего неизменяемого Фигура с методом площадь(), без сеттеров.
  • Выделение общего абстрактного базового типа с минимальным контрактом, который честно соблюдают все наследники.
  • Сужение базового интерфейса до того поведения, которое реально общее. Если только часть наследников умеет «летать», метод летать() не место в базовом Птица.
  • Неизменяемость убирает целый класс нарушений правила истории: нет сеттеров - нет способа разойтись в поведении.

Принцип не запрещает наследование - он запрещает наследование, которое лжёт о поведении.

Частые ошибки

  • Путать LSP с обычным наследованием. Компилятор разрешает переопределить метод как угодно; LSP требует, чтобы переопределение сохраняло контракт. Совместимость сигнатур не равна подстановочности.
  • Считать LSP правилом про «is-a» из реального мира. Квадрат «является» прямоугольником в геометрии, но не в коде. Решает поведение, а не словарь.
  • Бросать исключение в наследнике как «заглушку». throw new NotImplementedException() в переопределённом методе - прямое нарушение: база этого не делала, клиент не готов.
  • Усиливать предусловия из лучших побуждений. Добавить в наследнике проверку «аргумент должен быть положительным», которой не было в базе, - значит сломать вызывающий код.
  • Игнорировать инварианты, потому что их не видно. Неизменность, диапазон значений, непустота коллекции - всё это контракт, даже если не выражено в типах.

FAQ

Чем LSP отличается от обычного полиморфизма? Полиморфизм - это техническая возможность вызвать переопределённый метод через базовую ссылку. LSP - требование, чтобы такой вызов всегда давал корректное поведение. Полиморфизм без LSP компилируется, но даёт сюрпризы в рантайме: подтип «подходит» по типу, но «лжёт» по поведению.

Почему квадрат всё-таки нельзя наследовать от прямоугольника? Потому что у прямоугольника независимые ширина и высота, а квадрат вынужден связать их. Любой клиент, меняющий стороны по отдельности и проверяющий результат, сломается на квадрате. Если убрать сеттеры и сделать фигуры неизменяемыми, противоречие исчезает - но это уже другая иерархия.

Связан ли LSP с типизацией языка? Да, частично. Ковариантность возвращаемых типов и контравариантность параметров - это формальная сторона LSP, которую система типов может проверять. Но поведенческие контракты (инварианты, исключения, диапазоны) большинство языков не проверяет - за них отвечает дисциплина проектировщика и тесты.

Коротко

Принцип подстановки Лисков требует, чтобы объект подтипа можно было использовать всюду вместо объекта базового типа без нарушения корректности. За этим стоит теория контрактов: предусловия в подтипе нельзя усиливать, постусловия - ослаблять, инварианты и неизменяемость - нарушать. Формально это даёт ковариантность результатов и контравариантность параметров. Классический контрпример Квадрат-Прямоугольник показывает, что наследование должно отражать поведение, а не житейскую таксономию. Чинят нарушения композицией, неизменяемостью и честным выделением общего контракта. LSP - это буква L в SOLID и тест на то, врёт ли ваша иерархия о поведении.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

Читайте также