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

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

Я поделюсь здесь одним таким случаем, когда мы решили использовать конвейер для рефакторинга одного из таких длинных методов.

Отправная точка

Рассмотрим следующий код. Знакомый обработчик заказов, который принимает объект OrderInfo и обрабатывает заказ после применения набора бизнес-правил. В приведенном ниже примере метод проверяет поля, проверяет, поступил ли заказ из Калифорнии, проверяет адрес, проверяет осуществимость, создает заказ в БД и отправляет уведомление.

Очевидно, это долгий метод. Если мы поручим новому инженеру работать над этим кодом, он в конечном итоге проклянет нас за плохую карму, а затем добавит ее сам. Проблема такого длинного метода заключается в 1) тестируемости и 2) ремонтопригодности. Модульное тестирование таких методов требует очень много времени, и технически мы будем тестировать не один модуль кода, а всю функциональность. Если нам нужно добавить еще один шаг в процесс обработки заказа, скажем, проверить и применить рекламные акции, мы в конечном итоге добавим кучу промежуточных строк. Никто не может объяснить это ограничение лучше, чем Мартин Фаулер. Одна вещь, которую мы потенциально могли бы сделать, — это извлечь методы. Хотя это временно решает проблему ремонтопригодности, тестируемость по-прежнему остается проблемой.

Место назначения

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

Идея

Метод processOrder после рефакторинга должен выглядеть так.

Идея состоит в том, что мы разбиваем метод на набор небольших задач и создаем структуру, которая выполняет часть передачи данных от одной задачи к другой. Функциональный стиль программирования упрощает понимание для любого нового инженера, а добавление нового промежуточного шага представляет собой простую реализацию интерфейса. (С появлением Java 8 это можно было легко заменить лямбда-выражениями). Диаграмма классов для решения приведена ниже.

Путешествие

У нас есть три части для реализации Pipeline.

  1. Интерфейс задач для задач, которых придерживаются
  2. Оркестратор для управления последовательностью задач и распространения исключений, если таковые имеются
  3. Способ передачи и чтения данных из контекста

Интерфейс задач

PipelineTask будет базовым классом для всех задач. Подзадачи должны реализовывать метод handleRequest(). Метод handleRequest содержит параметр ExecutionContext, который содержит объект запроса, переданный в конвейер, результат ранее выполненных задач. Вместо объекта мы также могли бы использовать дженерики.

Пример задачи будет выглядеть примерно так, как показано ниже.

@Depends – это аннотация, добавленная для упрощения внедрения результатов одной из предварительных задач. В этом случае ответ PersistOrderPipelineTask вводится обработчиком аннотаций.

Оркестратор

Оркестратор ( PipelineController) использует универсальные шаблоны для определения типов запросов и ответов.

Оркестратор будет иметь структуру данных, которая поддерживает задачи (порядок должен поддерживаться. Поэтому я использую LinkedHashSet). Результат каждой задачи сохраняется на карте, чтобы передать его в контексте следующих задач. Любое исключение, возникающее из конвейерной задачи, помещается в PipelineException и обрабатывается вызывающим методом.

Методы runFirst и thenRun делают одно и то же. Однако я сохранил оба метода, чтобы повысить удобочитаемость вызывающего метода. PipelineControllerTaskAppender — это интерфейс, используемый для того, чтобы методы thenRun и runFirst разрешали добавлять только дополнительные задачи.

Фактическую координацию выполняет метод process. Он перебирает список задач и вызывает handleRequest по одному. Он сохраняет результат в ExecutionContext для передачи последующим задачам. Я использую Spring и, следовательно, использую фабрику бобов. Задачи могут быть созданы так же, как и любые другие объекты Java.

Контекст выполнения

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

Некоторым задачам может не потребоваться возвращать что-либо вроде NotificationTask, показанного выше. Поэтому для этой цели вводится тип NoOp.

С их помощью ту же логику исходного длинного метода можно скопировать в отдельные классы с минимальными изменениями. Следовательно, родительские модульные тесты должны пройти, и мы также можем добавить дополнительные модульные тесты, относящиеся к конкретным задачам. Кроме того, добавить дополнительный промежуточный шаг так же просто, как добавить PipelineTask и добавить его в thenRun.

Надеюсь, вы нашли это полезным. Весь исходный код можно найти здесь

Перенесено из моего блога, первоначально опубликованного на www.programmingartistry.com 19 января 2016 года.