Когда-то в детстве я лежал на кровати и долго разглядывал узоры на старом советском ковре, видя в них животных и фантастические фигуры. Теперь я чаще смотрю на код, но в моем сознании по-прежнему рождаются похожие образы. Как и на ковре, эти образы складываются в повторяющиеся паттерны. Они могут быть как приятными, так и отталкивающими. Сегодня я хочу рассказать вам о таком неприятном паттерне, который встречается в программировании.
Сценарий
Представьте себе сервис, который обрабатывает запрос на регистрацию клиента и отправляет событие об этом в Kafka. В статье я покажу пример реализации, который считаю антипаттерном, и предложу исправленный вариант.
Вариант 1: Methodcentipede
В коде Java ниже представлен код класса RegistrationService, который обрабатывает запрос и отправляет событие.
Структуру кода упрощенно можно представить в таком виде:
Здесь видно, что методы образуют неразрывную цепочку, по которой перетекает поток данных, как по длинной узкой кишке. Методы в середине этой цепочки ответственны не только за логику, непосредственно описанную в их теле, но и за логику вызываемых ими методов и их контракты (например, необходимость обработки определённых ошибок). Все методы, предшествующие вызываемому, наследуют всю его сложность. Например, если kafkaTemplate.send
имеет сайд-эффект в виде отправки события, то и вызывающий его sendEvent
приобретает тот же сайд-эффект. Метод sendEvent
также несёт ответственность за сериализацию, включая обработку её ошибок. Тестирование отдельных частей кода усложняется тем, что нет возможности проверить каждую часть изолированно без использования моков.
Вариант 2: Исправленный вариант
Код:
Схема представлена ниже:
Здесь видно, что метода sendEvent
вовсе нет, и за отправку отвечает kafkaTemplate.send
. Весь процесс построения сообщения для Kafka вынесен в отдельный метод mapToEventMessage
. Метод mapToEventMessage
не имеет сайд-эффектов, граница его ответственности четко очерчена. Исключения, связанные с сериализацией и отправкой сообщений, являются частью контракта отдельных методов и могут быть индивидуально обработаны.
Метод mapToEventMessage
является чистой функцией. Когда функция детерминированная и не имеет побочных эффектов, мы называем её “чистой” функцией. Чистые функции:
- проще читать,
- проще отлаживать,
- проще тестировать,
- не зависят от порядка, в котором они вызываются,
- просто запустить параллельно.
Рекомендации
Я бы предложил следующие техники, которые помогут избежать подобных антипаттернов в коде:
- Подход Testing Trophy
- Техника One Pile
- Test-Driven Development (TDD)
Все эти техники тесно связаны и взаимно дополняют друг друга.
Testing Trophy
Это подход к покрытию кода тестами, при котором акцент делается на интеграционные тесты, проверяющие контракт сервиса в целом. Unit-тесты используются для отдельных функций, которые сложно или дорого тестировать через интеграционные тесты. Тесты с подобным подходом я описывал в своих статьях: https://habr.com/ru/articles/781812/, https://habr.com/ru/articles/804673/, https://habr.com/ru/articles/797049.
One Pile
Эта техника описана в книге “Tidy First?” Кента Бека. Основная мысль: чтение и понимание кода сложнее, чем его написание. Если код разбит на слишком много мелких частей, может быть полезно сначала объединить его в одно целое, чтобы увидеть общую структуру и логику, а затем снова разделить на более понятные куски.
В контексте данной статьи предлагается не разделять код на методы до тех пор, пока он не будет обеспечивать выполнение требуемого контракта.
Testing Driven Development
Этот подход позволяет разделить усилия на написание кода для реализации контракта и на формирование дизайна кода. Мы не пытаемся сразу сделать хороший дизайн и написать код, соответствующий требованиям, а разделяем эти задачи. Процесс разработки выглядит следующим образом:
- Пишем тесты для контракта сервиса, используя подход Testing Trophy.
- Пишем код в стиле One Pile, добиваясь того, чтобы он обеспечивал выполнение требуемого контракта. Не обращаем внимания на качество дизайна кода.
- Делаем рефакторинг кода. Весь код написан, у нас есть полное представление о реализации и возможных узких местах.
Заключение
В статье рассмотрен пример антипаттерна, который может привести к сложностям в поддержке и тестировании кода. Подходы, такие как Testing Trophy, One Pile и Test-Driven Development, позволяют структурировать работу таким образом, чтобы код не превращался в непроходимый лабиринт. Инвестируя время в правильную организацию кода, мы закладываем основу для долговременной устойчивости и простоты сопровождения наших программных продуктов.
Спасибо за внимание к статье, и удачи в вашем стремлении к написанию простого кода!