В данной главе, описывается система, которая помогает правильно организовывать код построенный на классах.
Предположим что мы делаем сайт имеющий механизм аутентификации. После её выполнения, пользователю выводится приветствие, которое строится по разному в зависимости от возраста пользователя. Если пользователю не исполнилось 18, то пишется одно, всем остальным — другое.
В данном случае, реализация в лоб, через
if, будет лучшим решением задачи. Но в этом уроке мы отрабатываем использование полиморфизма в рамках классовой модели, поэтому пойдём другим путём. Сама задача специально упрощена, чтобы не тратить время на её анализ
Первый порыв у многих разработчиков ввести два типа (интерфейса и класса): Under18 и Above18. Дальше в каждом из классов, которые реализуют эти интерфейсы добавить по методу getGreetingMessage(). В итоге мы получили полиморфизм подтипов:
<!-- Где-то в шаблоне -->
<!-- Правильный класс для пользователя выбирается на момент начала обработки http-запроса -->
<?= $user->getGreetingMessage() ?>
Это решение хоть и работает, но ведёт не по тому пути. Сегодня у нас до 18 и после, потом появится отдельное поведение для тех кто старше 65. Всё станет ещё хуже, когда кроме этих разделений, появится дополнительное разделение на девушек и парней. В таком случае мы получим большое число комбинаций, под каждую из которых придётся создать отдельный класс пользователя:
- девушки старше 18
- девушки младше 18
- парни старше 18
- парни младше 18
- ...
В книжках по паттернам любят приводить пример с разделением средств передвижения по типам: плавающие, летающие и ездящие. А потом, внезапно оказывается, что некоторые одновременно и плавают и ездят.
Теперь попробуем ответить на вопрос, почему эту задачу не надо решать подтипами в любом случае. Сам по себе, пользователь, это сущность взятая из нашей предметной области. Предметная область и вывод текста на экран, это совершенно разные вещи. Второе относится к логике приложения, но не бизнес-логике. Если об этом не задумываться, то в конце концов настанет момент, когда внутри пользователя окажется вообще всё что только происходит на сайте, ведь оно всё так или иначе связано с самим пользователем. И мы получим божественный объект.
Правильное решение основано на композиции, подходе при котором создаются классы под конкретные задачи. Начнём сначала. В нашей задаче есть две ситуации: пользователи до 18 лет и пользователи старше. Создадим интерфейс GreetingMessage с методом getGreetingMessage и реализуем его в двух классах, один GreetingForUnder18 и другой GreetingForAbove18. В каждом из них, будет тот вывод, который нужен для конкретного пользователя.
Как пользователь будет взаимодействовать с объектами этих классов? Варианта два, либо мы передаём его в конструктор, либо в сам метод getGreetingMessage. Что правильнее? Всегда пытайтесь понять, имеем ли мы дело с абстракцией данных или нет. С самим пользователем всё понятно. Пользователь это абстракция данных, у него есть уникальность (все пользователи отличаются) и время жизни. А вот вывод сообщения, это операция без состояния. Само наличие класса и объекта для него обусловлено желанием получить полиморфизм подтипов и ничем более. Поэтому в данном примере лучше передавать пользователя через метод:
<!-- Где-то в шаблоне -->
<?= $greeting->getGreetingMessage($user) ?>
За кадром остался вопрос выбора и создания соответствующего объекта. За это отвечает фабрика, которая вызывается где-то до формирования вывода из шаблона.
<?php
function buildGreetingObject($user)
{
if ($user->getAge() < 18) {
return new GreetingForUnder18();
} else {
return new GreetingForAbove18();
}
}
Главное в этой схеме, то что пользователь остался пользователем. Он по-прежнему отвечает только за логику ядра приложения. Даже если добавятся новые условия вывода сообщения и наши два класса превратятся в 10 классов (потому что 10 вариантов вывода в зависимости от разных параметров), то это никак не повлияет на пользователя.
Что ещё более важно, при появлении новых задач, не связанных с выводом сообщения, пользователь по-прежнему не будет затронут. Например, мы захотим отправлять письма разным пользователям после регистрации. В зависимости от количества видов писем, будет создано такое же количество классов, реализующих интерфейс RegistrationEmailText. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.
Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.
В итоге, в коде появляется большое количество небольших интерфейсов (типов) и множество классов их реализующих. Количество классов, реализующих конкретный интерфейс, равно количеству возможных вариантов поведения. Большинство объектов этих классов не имеют своего состояния и нужны для организации полиморфного кода.
Стоит ли так писать код? Иногда да, но чаще нет. Слепое следование ООП, делает код сложнее и тяжелее, там где подходит простая функция или условная конструкция, начинают вырастать параллельные иерархии классов. В примерах выше это хорошо прослеживается. Задача, которая может быть реализована десятью строчками, решается многими десятками строчек и четырьмя файлами (фабрика, классы и интерфейс). А программист знакомый с абстрактными классами и наследованием, наворотит ещё больше файлов.
Обычно, получаемая сложность оправдывается расширяемостью, но это так не работает. Расширяемость нужно добавлять тогда когда нужно и только туда куда нужно. Другой вопрос, что сам способ организации кода через композицию объектов, является краеугольным камнем при организации кода построенного на классах. При этом надо чётко отслеживать, где у нас абстракция данных, а где действия без состояния, представленные объектами.