Skip to content

Instantly share code, notes, and snippets.

@GubaEvgeniy
Last active September 5, 2019 11:38
Show Gist options
  • Select an option

  • Save GubaEvgeniy/706f3588106586ab657d800c39148ab2 to your computer and use it in GitHub Desktop.

Select an option

Save GubaEvgeniy/706f3588106586ab657d800c39148ab2 to your computer and use it in GitHub Desktop.

Наверх

Объектная композиция

В данной главе, описывается система, которая помогает правильно организовывать код построенный на классах.

Предположим что мы делаем сайт имеющий механизм аутентификации. После её выполнения, пользователю выводится приветствие, которое строится по разному в зависимости от возраста пользователя. Если пользователю не исполнилось 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. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.

Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.

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

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

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

Дополнительные материалы

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment