Здесь требуется что-то вроде следующего свойства подстановки: если для каждого объекта o1 типа S существует такой объект o2 типа T, что для всех программ P, определенных в терминах T, поведение P не изменяется при подстановке o1 вместо o2, то S является подтипом T [24] Barbara Liskov. Data Abstraction and Hierarchy , SIGPLAN Notices 23, 5 (May 1988).
.
Чтобы понять эту идею, известную как принцип подстановки Барбары Лисков (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 был преобразован в более широкий принцип проектирования программного обеспечения, который распространяется также на интерфейсы и реализации.
Подразумеваемые интерфейсы могут иметь множество форм. Это могут быть интерфейсы в стиле Java, реализуемые несколькими классами. Или это может быть группа классов на языке Ruby, реализующих методы с одинаковыми сигнатурами. Или это может быть набор служб, соответствующих общему интерфейсу REST.
Во всех этих и многих других ситуациях применим принцип LSP, потому что существуют пользователи, зависящие от четкого определения интерфейсов и замещаемости их реализаций.
Лучший способ понять значение LSP с архитектурной точки зрения — посмотреть, что случится с архитектурой системы при нарушении принципа.
Допустим, что мы взялись за создание приложения, объединяющего несколько служб, предоставляющих услуги такси. Клиенты, как предполагается, будут использовать наш веб-сайт для поиска подходящего такси, независимо от принадлежности к той или иной компании. Как только клиент подтверждает заказ, наша система передает его выбранному такси, используя REST-службу.
Теперь предположим, что URI службы является частью информации, хранящейся в базе данных водителей. Выбрав водителя, подходящего для клиента, наша система извлекает URI из записи с информацией о водителе и использует ее для передачи заказа этому водителю.
Допустим, что для водителя с именем Bob адрес URI отправки заказа выглядит так:
purplecab.com/driver/Bob
Наша система добавит в конец этого URI информацию о заказе и пошлет его методом PUT:
purplecab.com/driver/Bob
/pickupAddress/24 Maple St.
Читать дальше
Конец ознакомительного отрывка
Купить книгу