Не будем утомлять вас долгим вступлением и сразу же сделаем сакраментальное заявление: паттерны (шаблоны) программирования — это отличная вещь. Ну как минимум — по задумке. В первую очередь, они должны помогать при работе в команде. Представьте: все разработчики используют паттерны, код стандартизирован. Для того чтобы быстро в нем разобраться, достаточно лишь узнать паттерн. Волки сыты, овцы целы и все счастливы.
Однако на практике этот подход дает больше минусов, чем плюсов. В чем они заключаются?
- Неопытные программисты, а порой и опытные не знают паттернов и не могут быстро выделять их в коде.
- Довольно часто применение паттерна неоправданно усложняет решение задачи.
- Существуют задачи, которые не вписываются в паттерн. Работа над ними приводит к тому, что происходит отклонение от стандарта.
А теперь все плохо — не правда ли? Не расстраивайтесь, зачастую паттерны здорово упрощают жизнь.
Задача
Перед нами возникла задача посчитать коэффициент “дружбы” между двумя пользователями на основе их взаимодействия в социальных сетях. Мы не будем приводить алгоритм расчета — он выходит за рамки этой статьи. Гораздо важнее идея и постановка задачи.
Итак, есть две таблицы. К примеру: values и values_maximums. Алгоритм расчетов состоит в обработке данных из двух таблиц (обратим ваше внимание на то, что они пересекаются).
Например, в таблице values есть столбцы a1, a2, a3, a4, b1, b2, c1.
В наблице values_maximums есть столбцы am, bm, cm.
Смысл расчета в следующем:
- На основе 100 строк из таблицы values и столбцов a1, a2, a3 рассчитывается одна строка таблицы values_maximums — колонка am.
- В таблице values на основе столбцов a1, a2, a3 и полученного значения am — пересчитываем строки и получаем b1, b2.
- На основе 100 строк и столбцов b1, b2 — считаем bm для values_maximums.
- Производим аналогичные действия для столбцов “c”.
Кроме того, мы выделили еще 4 случая:
- Первоначальный расчет;
- Расчет на основе готовых данных;
- Рекуррентный расчет;
- Расчет по запросу.
Таким образом, мы имеем четыре общих шага для всех типов расчетов. При этом для двух расчетов, которые производятся на основе готовых данных, алгоритм полностью совпадает. Разница лишь в процессе обработке. При расчете по запросу нам необходимо выполнить все 4 шага. Причем подряд. При рекуррентном расчете каждый шаг сохраняет свой результат в базу. Следующий шаг запускается, когда воркер получает ресурс сервера.
Итак, постановка задачи состоялась. Мы располагаем данными, требующими четырехшаговой обработки. Кроме того, у нас есть три типа запросов, в зависимости от которых меняется либо алгоритм расчета данных, либо вызов следующего шага (inline или async).
Builder pattern — общие сведения
Builder pattern — это паттерн, применяемый, когда известны этапы производства чего-либо, но его реализация на каждом конкретном этапе отличается. Для примера возьмем автомобильное производство. Этапы: сборка кузова, установка двигателя, установка колес, отделка салона, подключение электроники.
Перед вами — схема. На ее основе можно собирать кабриолеты, седаны, грузовики и даже мотоциклы. Различаться будет лишь техника реализации (количество и/или размер колес, форма кузова, комплект электроники и т.д.). Однако этапы производства останутся неизменными.
Приведенный пример отлично подходит и к нашей задаче. С одной оговоркой: у нас этапы могут полностью совпадать, а различия будут заключаться в процессе переходов от одного этапа к другому.
Реализация
Начнем с конца. Именно так выглядит код в итоге:
Как видите, у нас есть два класса. В каждом из них описаны одни и те же шаги. А внутри каждого шага — метод perform, описывающий логику его работы. ReccurentProcessing наследуется от DedicatedProcessing, потому что логика шагов одинакова, но передача управления между ними должна быть асинхронной (через Sidekiq worker).
Код выглядит читаемым. Осталось лишь разобраться, как эта магия работает. Оказывается, что все реализуется за счет двух небольших классов.
Класс, описывающий логику шага. Он достаточно прост — принимает входные данные и возвращает результат. То есть, чтобы обратиться к входным данным внутри шага, мы используем options, а чтобы вернуть результат — кладем его в results.
Рассмотрим второй класс. Именно он приводит магию в действие.
Шаги, которые нужно выполнить и их последовательность:
Это — переменная класса, в которой хранятся блоки для обработки каждого шага.
Самый важный момент — построение класса обработки:
Class.new(Step, &block) — мы создаем класс на основе Step и полученного блока. В итоге метод perform, описанный внутри блока, будет помещен в этот класс. Т.е. для каждого типа расчета у нас получится своя структура steps. И она будет выглядеть примерно так:
и так далее
Теперь, когда нам необходимо выполнить новый шаг, мы просто построим экземпляр нужного класса и передадим в него нужные значения.
Перед вами метод класса. Поэтому он не будет пересекаться с методом внутри описания шага. Мы инициализируем нужный нам класс. Заполняем его входными данными и вызываем perform. Тот самый perform, который описывается внутри каждого шага.
При наличии следующего шага, вызываем метод perform_next_step, который передает ему управление:
Метод предельно прост. По умолчанию он сразу же выполняет следующий шаг.
В завершение мы переписали этот метод для рекуррентных расчетов:
Выводы
- Мы получили довольно простой и вполне читаемый код. Он достаточно четко описывает необходимые шаги, передачу управления между ними и регулируется отдельным механизмом, полностью подконтрольным программисту.
- Описание каждого шага по отдельности — это очень удобно и вдобавок — читаемо. А читаемость (напомним!) является одним из главных требований к разработке. Особенно, когда речь идет о проведении сложных расчетов.