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



В 1988 году Барбара Лисков написала следующие строки с формулировкой определения подтипов.

Здесь требуется что-то вроде следующего свойства подстановки: если для каждого объекта o1 типа S существует такой объект o2 типа T, что для всех программ P, определенных в терминах T, поведение P не изменяется при подстановке o1 вместо o2, то S является подтипом T[24].

Чтобы понять эту идею, известную как принцип подстановки Барбары Лисков (Liskov Substitution Principle; LSP), рассмотрим несколько примеров.

Руководство по использованию наследования

Представьте, что у нас есть класс с именем >License, как показано на рис. 9.1. Этот класс имеет метод с именем >calcFee(), который вызывается приложением >Billing. Существует два «подтипа» класса >License:>PersonalLicense и >BusinessLicense. Они реализуют разные алгоритмы расчета лицензионных отчислений.


Рис. 9.1. Класс License и его производные, соответствующие принципу LSP


Этот дизайн соответствует принципу подстановки Барбары Лисков, потому что поведение приложения >Billing не зависит от использования того или иного подтипа. Оба подтипа могут служить заменой для типа >License.

Проблема квадрат/прямоугольник

Классическим примером нарушения принципа подстановки Барбары Лисков может служить известная проблема квадрат/прямоугольник (рис. 9.2).


Рис. 9.2. Известная проблема квадрат/прямоугольник


В этом примере класс >Square (представляющий квадрат) неправильно определен как подтип класса >Rectangle (представляющего прямоугольник), потому что высоту и ширину прямоугольника можно изменять независимо; а высоту и ширину квадрата можно изменять только вместе. Поскольку класс >User полагает, что взаимодействует с экземпляром >Rectangle, его легко можно ввести в заблуждение, как демонстрирует следующий код:


>Rectangle r =…

>r. setW(5);

>r. setH(2);

>assert(r.area() == 10);


Если на место… подставить код, создающий экземпляр >Square, тогда проверка >assert потерпит неудачу. Единственный способ противостоять такому виду нарушений принципа LSP – добавить в класс >User механизм (например, инструкцию if), определяющий ситуацию, когда прямоугольник фактически является квадратом. Так как поведение >User зависит от используемых типов, эти типы не являются заменяемыми (совместимыми).

LSP и архитектура

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

Подразумеваемые интерфейсы могут иметь множество форм. Это могут быть интерфейсы в стиле Java, реализуемые несколькими классами. Или это может быть группа классов на языке Ruby, реализующих методы с одинаковыми сигнатурами. Или это может быть набор служб, соответствующих общему интерфейсу REST.

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

Лучший способ понять значение LSP с архитектурной точки зрения – посмотреть, что случится с архитектурой системы при нарушении принципа.

Пример нарушения LSP

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