SOLID: L — Liskov Substitution Principle

SOLID: L — Liskov Substitution Principle


Makar M.

Введение

В 1986 году, в городе Портленде (США), была организована ежегодная исследовательская конференция ACM под названием OOPSLA, благодаря которой, был внесён огромный вклад в формирование шаблонов проектирования, рефакторинга и ООП языков программирования. А значимые для индустрии доклады на ней, освещаются и по нынешний день.

В 1987 году, спустя год после основания конференции, произошло событие, оказавшее значительное влияние на развитие практик написания чистого кода. Барбара Лисков, впервые участвуя в этой конференции и уже имеющая на тот момент свыше 48 работ в области компьютерных наук, представила доклад «Data Abstraction and Hierarchy», который положил начало Liskov Substitution Principle (принципу подстановки Лисков) и существенно способствовал развитию идеи поведенческого подтипирования.

Неформальное правило

На заре формирования этого принципа, Лисков предложила следующее определение, известное как строгий вариант поведенческого подтипирования:

Иерархия типов состоит из подтипов и супертипов. Интуитивное представление о подтипе состоит в том, что его объекты предоставляют всё поведение объектов другого типа (супертипа), а также нечто дополнительное. Здесь требуется нечто вроде следующего свойства подстановки: если для каждого объекта o₁ типа S существует объект o₂ типа T такой, что для всех программ P, определённых с использованием T, поведение P остаётся неизменным при подстановке o₁ вместо o₂, то S является подтипом T.
(«Data Abstraction and Hierarchy» в «OOPSLA ‘87 Addendum to the Proceedings», p. 25, Barbara Liskov)

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

Суть высказывания такова: если в программе P, ориентированной на работу с объектом типом T, происходит замена этого объекта на другой объект типа S и при этом поведение программы остаётся неизменным, то объект типа S является подтипом объекта типа T.

В такой формулировке можно увидеть несколько проблем:

  1. На практике редко требуется, чтобы подтипы в точности имитировали поведение своих супертипов — иногда наоборот, необходимо обеспечить более обобщённое поведение посредством замены подтипа на супертип.
  2. Отсутствие отсылки на концепцию поведенческого подтипирования (через понятия «документация», «спецификация» или «контракт») приводит к тому, что сравниваются конкретные реализации S и T, а не их спецификации. Это вызывает вопросы, особенно если T является абстрактным классом.
  3. В контексте ООП, сложно определить, что значит универсально/экзистенциально квантифицировать объект какого-то типа или заменить один объект на другой. Например, как можно универсально утверждать, что один объект можно заменить другим, если объекты могут вести себя по-разному, в зависимости от их состояния? А в определённых случаях, например, мы можем не заменять один объект другим, а использовать один объект как другой.

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

Поведенческое подтипирование

Изначально в языках объектно-ориентированного программирования понятия наследования и поведенческого подтипирования были тесно взаимосвязаны. При этом различие между наследованием как механизмом повторного использования кода и поведенческим подтипированием, отражающим поведенческую совместимость, не всегда было очевидно. Такая путаница в ранних ООП-языках могла приводить к созданию менее надёжных систем типов.

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

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

До работ Америка, хоть идеи подстановки и существовали ранее, термин «поведенческое подтипирование» и его особый акцент на наблюдаемом поведении не были столь чётко сформулированы. Его работы заложили важный терминологический и концептуальный фундамент для развития этих идей в дальнейшем.

Пьер Америка определил тип как спецификацию поведения его элементов, включающих сигнатуры методов и более подробные поведенческие свойства. Для Америка, подтипирование означало, что любой объект, принадлежащий подтипу так же принадлежит и супертипу, подразумевая удолетворение подтипом спецификации супертипа.

В своей работе от 1987 года под названием «Inheritance and Subtyping in a Parallel Object-Oriented Language» он заложил основы этой концепции. А в статье 1990 года «Designing an Object-Oriented Programming Language with Behavioural Subtyping» большое внимание уделялось разработке языка POOL-I, в котором наследование и подтипирование были разделены на основе наблюдаемого поведения.

В то время как Америка фокусировался на формальных спецификациях и наблюдаемом поведении, работа Лисков, особенно через LSP, сформулировала концепцию с точки зрения взаимодействия объектов различных типов внутри программы, что сделало её более применимой к проектированию программного обеспечения. 

Развитие поведенческого подтипирования

Влиятельное выступление Барбары Лисков на конференции OOPSLA в 1987 году, заложившее основу для принципа подстановки Лисков, не осталось незамеченным.

Это выступление привлекло внимание Дженнет Винг, которая сначала была магистрантом Барбары Лисков, а затем продолжила обучение в аспирантуре у Джона Гаттага. Спустя некоторое время Винг обратилась к Лисков с предложением: «Почему бы нам не попытаться, точно разобраться, что это означает?» Так, они начали совместную работу, результатом которой стали несколько статей, опубликованных позднее.

В 1994 году Барбара Лисков и Дженнет Винг представили более строгую и широко цитируемую формализацию поведенческого подтипирования в статье «A Behavioral Notion of Subtyping», опубликованной в ACM Transactions on Programming Languages and Systems (TOPLAS):

В этой статье вводится понятие отношения подтипа, основанное на семантических свойствах подтипа и супертипа. Тип объекта определяет как множество допустимых значений, так и интерфейс взаимодействия с внешним окружением (через вызовы его методов). Таким образом, при разработке подтипа нас интересует сохранение свойств, значений и методов супертипа. Мы требуем, чтобы подтип сохранял поведение методов супертипа, а также все инвариантные свойства и свойства истории своего супертипа.
Особый интерес для нас представляет наблюдаемое поведение объекта (изменения его состояния), что и стало причиной нашего акцента на свойствах истории, а также на изменяемых типах и мутаторах (методах, изменяющих состояние).
В статье предлагаются два способа определения отношения подтипа: один основан на ограничениях (constraints), другой — на правиле расширения (extension rule).
Оба подхода гарантируют, что подтипы сохраняют свойства своих супертипов. Это первая работа, в которой рассматриваются свойства истории, и предлагается способ проверки допустимости «дополнительных» методов в условиях изменяемости.
(«A Behavioral Notion of Subtyping» в «ACM Transactions on Programming Languages and Systems, Vol. 16, No. 6, November 1994», p. 1839, Barbara Liskov, Jeannette Wing)

В этой работе они формализовали понятие поведенческого подтипирования с учётом изменяемости объектов, используя инварианты с ограничениями в спецификациях типов. Их определение требовало, чтобы любое свойство, доказуемое для объектов супертипа, также выполнялось для объектов его подтипа.

Понятие поведенческого подтипирования у Лисков и Винг имеет определённое сходство с проектированием по контракту, предложенное Бертраном Мейером, поскольку оно тоже рассматривает подтипирование при помощи предусловий, постусловия и инвариантов.

Они представили два способа определения отношения подтипа, которые удовлетворяют этому критерию. А также расширили более ранние работы, включая работу Америка, разрешив подтипам иметь больше методов, чем их супертипы.

В то время как Америка ввел термин и основную идею, формализация Лисков и Винг, закрепила эту концепцию в этой области, что послужило основой для дальнейших исследований и применений.

Принцип подстановки Барбары Лисков

Принцип подстановки Барбары Лисков (LSP) — это принцип в объектно-ориентированном программировании, который определяет особый вид отношения подтипирования, известный как сильное поведенческое подтипирование. Он гласит, что объект (например, класс) может быть заменён подобъектом (например, классом, который расширяет первый) без нарушения работы программы. Этот принцип фокусируется на семантической совместимости типов внутри иерархии, особенно объектных типов, а не только на их синтаксической структуре.

Более формальное описание принципа следующее:

Если ⁠ϕ(x)⁠ — свойство, справедливое для всех объектов типа T, то для любого объекта ⁠y⁠ типа S (при условии, что S является подтипом T) должно выполняться свойство ⁠ϕ(y)⁠.

Говоря простым языком, если существует свойство, доказуемое для объектов типа T, то это же свойство должно быть истинным для объектов типа S, при условии, что S является подтипом T. Это гарантирует, что подтипы сохраняют существующее поведение своих супертипов. Если подтип нарушает LSP, это означает, что он ведёт себя несовместимо со своим супертипом, а это может привести к неожиданным ошибкам и затруднить понимание, поддержку, системы. 

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

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

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

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

Все реализации интерфейсов являются подтипами своего интерфейса. Все утиные типы являются подтипами подразумеваемого интерфейса. Каждый пользователь базового интерфейса, будь то явно определённый или подразумеваемый, должен разделять единое понимание его смысла. Если реализация вносит неясность и сбивает с толку, разработчики будут вынуждены массово использовать условные операторы if/switch для устранения неоднозначностей.

Заключение

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

Report Page