ЖАНРЫ

Чистая архитектура. Искусство разработки программного обеспечения
Шрифт:

В этом примере класс

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-службу.

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

Допустим, что для водителя с именем Bob адрес URI отправки заказа выглядит так:

purplecab.com/driver/Bob

Наша система добавит в конец этого URI информацию о заказе и пошлет его методом PUT:

purplecab.com/driver/Bob

/pickupAddress/24 Maple St.

/pickupTime/153

/destination/ORD

Это явно означает, что все службы должны соответствовать общему интерфейсу REST. Они должны единообразно интерпретировать поля

pickupAddress
,
pickupTime
и
destination
.

Теперь предположим, что компания такси Acme наняла несколько программистов, которые ознакомились со спецификацией недостаточно внимательно. Они сократили имя поля

destination
до
dest
. Компания Acme – крупнейшая компания такси в нашем регионе, и бывшая жена президента компании Acme вышла замуж за президента нашей компании, и… В общем, вы поняли. Что может произойти с архитектурой нашей системы?

Очевидно, мы должны бы добавить особый случай. Запрос с заказом для любого водителя из Acme должен бы конструироваться в соответствии с иным набором правил, чем для всех остальных.

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

if (driver.getDispatchUri. startsWith("acme.com"))…

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

Например, представьте, что компания Acme добилась большого успеха, купила компанию Purple Taxi и объединенная компания решила сменить имя и адрес веб-сайта и объединить все системы оригинальных компаний. Получается, что теперь мы должны добавить еще одну инструкцию if для «purple»?

Архитектор должен изолировать систему от ошибок, подобных этой, и добавить модуль, управляющий созданием команд доставки заказов в соответствии с параметрами, указанным для URI в базе данных с настройками. Настройки могли бы выглядеть как-то так:

UPI

Acme.com

Формат команды

/pickupAddress/%s/pickupTime/%s/dest/%s

UPI

*.*

Формат команды

/pickupAddress/%s/pickupTime/%s/destination/%s

В результате архитектор вынужден добавить важный и сложный механизм из-за того, что интерфейсы не всех REST-служб оказались совместимыми.

Заключение

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

10. Принцип разделения интерфейсов

Происхождение названия принципа разделения интерфейсов (Interface Segregation Principle; ISP) наглядно иллюстрирует схема на рис. 10.1.

Рис. 10.1. Принцип разделения интерфейсов

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

OPS
. Допустим, что
User1
использует только операцию
op1
,
User2
– только
op2
и
User3
– только
op3
.

Теперь представьте, что

OPS
– это класс, написанный на таком языке, как Java. Очевидно, что в такой ситуации исходный код
User1
непреднамеренно будет зависеть от
op2
и
op3
, даже при том, что он не пользуется ими. Эта зависимость означает, что изменения в исходном коде метода
op2
в классе
OPS
потребуют повторной компиляции и развертывания класса
User1
, несмотря на то что для него ничего не изменилось.

Поделиться с друзьями: