“Не повторяйся” (Don’t Repeat Yourself, DRY) — один из самых распространенных принципов программирования, регулярно упоминаемый при рассмотрении запросов на включение изменений. Первоначально обоснование соблюдения принципа DRY было вполне разумным.
Однако со временем, как и многое другое в программной инженерии, его смысл и обоснование, на мой взгляд, были утрачены. Вместо того чтобы применять его по мере необходимости, он используется при малейшем подозрении на дублирование, что, по-моему, чревато ухудшением кода в долгосрочной перспективе.
Предлагаю поразмышлять над тем, почему дублирование не является источником всех бед и почему совершенно нормально иногда повторяться.
Что призван решить принцип DRY?
В 1999 году Энди Хант и Дэйв Томас выпустили книгу “Прагматичный программист”. С тех пор она приобрела невероятную популярность, а ряд ее положений стали неизменным руководством к действию для многих инженеров-программистов.
Одно из них — “не повторяйся”. Вот определение DRY из книги “Прагматичный программист”:
Каждая часть информации должна иметь единое, однозначное, авторитетное представление в системе.
Я с этим полностью согласен. Иначе говоря, бизнес-логика не должна повторяться в двух местах.
Коротко об использовании DRY
Допустим, вы работаете над системой электронной коммерции и вам нужно указать, что заказ действителен в течение 7 дней. Логика для определения того, просрочен ли заказ, должна определяться только один раз. Например, в классе Order
(заказ):
class Order
def initialize(order_id:, order_timestamp:, items:)
@order_id = order_id
@order_timestamp = order_timestamp
@items = items
end
def expired?
Time.zone.now > order_timestamp + 7.days
end
end
Как видите, логика для определения того, истек ли срок действия заказа, находится в классе Order
. Если предположить, что это единственное место, где эта логика определена, то код в данном репозитории соответствует принципу DRY.
Однако если бы в другом месте была вторая часть кода, использующая ту же логику, это считалось бы нарушением принципа DRY, так как бизнес-правило было бы продублировано в двух местах.
Это неоптимально, поскольку обновление бизнес-правила — по истечении срока действия заказа через 7 дней — потребует изменений в нескольких местах. Стоит не внести изменение в одном месте (что вполне вероятно), и в разных частях приложения будет применяться разная логика.
Более того, бизнес-логика должна принадлежать доменным сущностям, а не просто находиться в разных частях кодовой базы. Логически рассуждая, чтобы понять логику истечения срока действия заказа, вы бы обратились к классу Order
.
Этот второй фрагмент кода должен подвергнуться рефакторингу, чтобы использовать метод expired?
из Order
вместо того, чтобы придерживаться принципа DRY.
Пока все верно. Описанное выше является классическим примером применения DRY. Однако в последние годы этот принцип используется слишком своевольно, причем в ущерб коду.
Проблема злоупотребления DRY
Итак, мы выяснили, для чего предназначен принцип DRY и как правильно его использовать.
Однако DRY не всегда применяется именно так. Я видел множество комментариев к запросам на включение изменений, где авторы неправильно полагали, что код должен быть обновлен, чтобы соответствовать принципу DRY, в то время как был смысл в дублировании кода.
При этом считается, что нужно применять DRY по отношению к любому дублированию, а не только к дублированию бизнес-правил и информации. Это часто усугубляется автоматизированными инструментами анализа кода, которые обычно выделяют любое дублирование как признак кода “с душком”.
Пример неуместного использования DRY
Думаю, лучше всего объяснить правильный и неправильный подходы на конкретном примере. Рассмотрим фрагмент кода с двумя классами, в котором есть дублирование. Это пример из реального проекта (я лишь упростил код, чтобы ограничить количество представленных данных).
class OrderPlacedEventPublisher
def publish(order)
publisher = EventPublisher.for_event('orer_placed')
.with_data(order_id: order.id, items: order.items)
add_location_context(location_uuid: order.location_uuid, publisher: publisher)
publisher.publish
end
def add_location_context(location_uuid:, publisher:)
return if location_uuid.blank?
publisher.with_context(
name: 'location_context',
data: {
location_uuid: location_uuid
},
version: '1-0-0'
)
end
end
class ItemViewedEventPublisher
def publish(item)
publisher = EventPublisher.for_event('item_viewed')
.with_data(item_id: item.id)
add_location_context(location_uuid: item.location_uuid, publisher: publisher)
publisher.publish
end
def add_location_context(location_uuid:, publisher:)
return if location_uuid.blank?
publisher.with_context(
name: 'location_context',
data: {
location_uuid: location_uuid
},
version: '1-0-0'
)
end
end
Классы находятся в отдельных файлах в репозитории (я просто поместил их в один блок кода для удобства просмотра). Оба класса публикуют событие в Kafka в совершенно разных частях приложения и в разных случаях использования.
Каждое событие имеет основной раздел (например, order_placed
) и опционально список контекстов для предоставления дополнительной информации, которая часто относится к нескольким событиям. Например, location
, т.е. данные о местоположении пользователя, не хранятся в проекте вместе с данными о товарах и заказах, но здесь это не имеет значения.
Я ввел контексты для обоих событий, и инструмент анализа кода выделил это дублирование как проблему (так же поступил и мой коллега в запросе о включении изменений).
Повторяйтесь
В данном случае я не согласен ни с инструментом анализа кода, ни с рецензентом. Хотя здесь есть дублирование, оно не связано с бизнес-правилами и информацией.
К сожалению, не могу найти доклад, сделанный на конференции O’Reilly в Берлине, где я впервые услышал рекомендацию “повторяйтесь”. Она очень сильно меня привлекла.
Конечно, в этом примере можно устранить дублирование, например выделив метод add_location_context
во вспомогательный модуль и включив этот модуль в каждый из приведенных выше классов. В Ruby это можно было бы сделать следующим образом:
module LocationContextHelper
def add_location_context(location_uuid:, publisher:)
return if location_uuid.blank?
publisher.with_context(
name: 'location_context',
data: {
location_uuid: location_uuid
},
version: '1-0-0'
)
end
end
class OrderPlacedEventPublisher
include LocationContextHelper
... rest of the class ...
end
class ItemViewedEventPublisher
include LocationContextHelper
... rest of the class ...
end
Вы будете правы, если скажете, что дублирование было удалено, но ценой введения взаимозависимости обоих классов. В проекте со множеством контекстов во многих частях кодовой базы для соблюдения DRY при работе с дополнительными контекстами событий может понадобиться 5 вспомогательных классов (или один большой вспомогательный класс, что еще хуже) с включением их примерно в 15 классов event publisher
.
Цена устранения такого дублирования, на мой взгляд, слишком высока. В методе add_location_context
нет бизнес-логики — это простой код, который необходим в совершенно несвязанных частях кодовой базы.
Удалив дублирование, мы без необходимости связали большое количество классов. Конечно, если бы пришлось добавлять дополнительные данные в контекст, это было бы быстрее при наличии вспомогательного класса. Но нет никакой гарантии, что нужные данные находятся в передаваемых параметрах. И в этом случае все равно придется обновлять все места, где используется вспомогательный класс.
Заключение
Надеюсь, эта статья была полезной для вас. Возможно, в следующий раз, собираясь удалить дублирование, вы подумаете, имеет ли смысл делать это.
Стоит отметить, что мы обсуждали DRY только в контексте одного репозитория. Если вы работаете, например, в архитектуре микросервисов, то дублирование некоторых бизнес-правил в нескольких сервисах может быть более оправданным и целесообразным.
Читайте также:
- Новый взгляд на старые истины: принцип «Не повторяйся!» (DRY)
- Как научиться не только писать код, но и быть хорошим программистом
- От джуниора до мидла: 7 советов для фронтенд-разработчиков
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ashley Peacock: The Case Against Relying Solely on DRY