Хабрахабр

[Перевод] Ошибочное понимание принципа DRY

Я знаю, о чём вы подумали: «Ещё одна скучная статья про DRY? Нам их мало, что ли?».

Возможно, вы правы. Но я встречаю слишком много разработчиков (junior и senior), применяющих DRY так, словно они охотятся на ведьм. Либо совершенно непредсказуемо, либо везде, где можно. Так что, судя по всему, в интернете никогда не будет достаточно статей о принципе DRY.

Если кто не знает: принцип DRY — это "Don't Repeat Yourself" (не повторяйся), и впервые о нём упомянуто в книге The Pragmatic Programmer. Хотя сам по себе этот принцип был известен и применялся задолго до появления книги, однако в ней ему было дано название и точное определение.

Итак, без лишних рассуждений отправимся в путешествие по чудесной стране DRY!

Не повторяйте свои знания

Хотя фраза «не повторяйся» звучит очень просто, в то же время она слишком общая.

В книге The Pragmatic Programmer принцип DRY определяется как «Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы».

Всё замечательно, но… что такое «часть знания»?

Я бы определил это как любую часть предметной области бизнеса или алгоритма.

Если взять популярные аналогии из электронной коммерции, то класс shipment и его поведение будут частью предметной области бизнеса вашего приложения. Отгрузка — это реальный процесс, используемый вашей компанией для отправки товара своим клиентам. Это часть бизнес-модели компании.

Следовательно, логика работы класса shipment должна появляться в приложении однократно.

Причина очевидна: допустим, вам нужно отправить груз на склад. Для этого придётся активировать логику отгрузки в 76 разных местах приложения. Без проблем: вставляем логику 76 раз. Через некоторое время приходит начальник и просит кое-что изменить в логике: вместо одного склада компания теперь распределяет товары по трём складам. В результате вы потратите кучу времени на изменение логики, поскольку вам придётся сделать это в 76 местах! Совершенно впустую потраченное время, хороший источник багов и прекрасный способ выбесить начальство.

Решение? Создайте единственное представление вашего знания. Положите куда-нибудь логику отправки товара на склад, а затем используйте представление этого знания везде, где нужно. В ООП отправка груза может быть методом класса shipment, который вы можете использовать по своему усмотрению.

Ещё один пример: допустим, вы пишете хитрый класс для парсинга В-деревьев. Его тоже можно считать знанием: это алгоритм, который должен быть определён однократно. А уже представление знания можно использовать где вашей душе угодно без повторения самого знания.

DRY и дублирование кода

Так значит DRY относится к knowledge? К бизнес-логике?

Начнём с очевидного:

<?php interface Product
{ public function displayPrice();
} class PlasticDuck implements Product
{ /** @var int */ private $price; public function __construct(int $price) { $this->price = $price; } public function displayPrice() { echo sprintf("The price of this plastic duck is %d euros!", $this->price); }
} $plasticDuck = new PlasticDuck(2);
$plasticDuck->displayPrice();

Этот код выглядит недурно, правда?

Однако Лёха, твой коллега-разработчик, с вами не согласится. Когда Лёха увидит этот код, подойдёт к вашему столу и возмутится:

  1. Слово price повторяется 6 раз.
  2. Метод displayPrice() повторяется в интерфейсе, в реализации, и вызывается во время runtime.

Лёха — начинающий эксперт в вашей компании, и не имеет понятия об ООП.

Я прямо вижу, как вы, замечательный разработчик, смотрите на Лёху, словно опытный садовник на личинку, и отвечаете:

  1. Это переменная (и свойство), и вам нужно повторять её в коде.
  2. Однако логика (отображения цены) представлена однократно, в самом методе. Здесь не повторяется ни знание, ни алгоритм.

Здесь никак не нарушается принцип DRY. Лёха затыкается, подавленный вашей аурой величия, заполняющей комнату. Но вы поставили под сомнение его знания, и он злится. Лёха гуглит приницп DRY, находит другой пример вашего кода, возвращается и тычет вам в лицо:

<?php class CsvValidation
{ public function validateProduct(array $product) { if (!isset($product['color'])) { throw new \Exception('Import fail: the product attribute color is missing'); } if (!isset($product['size'])) { throw new \Exception('Import fail: the product attribute size is missing'); } if (!isset($product['type'])) { throw new \Exception('Import fail: the product attribute type is missing'); } }
}

Лёха торжествующе восклицает: «Ты олень! Этот код не соответствует DRY!». А вы, прочитав эту статью, отвечаете: «Но бизнес-логика и знание всё ещё не повторяются!»

Вы снова правы. Метод проверяет результат парсинга CSV только в одном месте (validateProduct()). Это знание, и оно не повторяется.

Но Лёха не готов смириться с поражением. «А что насчёт всех этих if? Разве это не очевидное нарушение DRY?». Вы контролируете свой голос, тщательно выговариваете каждое слово, и ваша мудрость отражается от стен, создавая эхо осознания: «Ну… нет. Это не нарушение. Я бы назвал это ненужным дублированием кода, а не нарушением принципа DRY».

Вы молниеносно набираете код:

<?php class CsvValidation
{ private $productAttributes = [ 'color', 'size', 'type', ]; public function validateProduct(array $product) { foreach ($this->productAttributes as $attribute) { if (!isset($product[$attribute])) { throw new \Exception(sprintf('Import fail: the product attribute %s is missing', $attribute)); } } }
}

Выглядит лучше, верно?

Подведём итог:

  1. Дублирование знания всегда является нарушением принципа DRY. Хотя можно представить ситуацию, когда это не так (напишите в комментах, если у вас тоже есть такие примеры).
  2. Дублирование кода не обязательно является нарушением принципа DRY.

Но Лёха ещё не убеждён. Со спокойствием высшего духовного наставника, вы ставите точку в споре: «Многие думают, что DRY запрещает дублировать код. Но это не так. Суть DRY гораздо глубже».

Кто это сказал? Дэйв Томас, один из авторов The Pragmatic Programmer, книги, в которой дано определение самого принципа DRY!

Применяем DRY везде: рецепт катастрофы

Бесполезные абстракции

Возьмём более жизненный, интересный пример. Сейчас я работаю над приложением для компании, занимающейся производством фильмов. В него можно легко загружать сами фильмы и метаданные (заголовки, описания, титры…). Всё эта информация затем отображается в VoD-платформе. Приложение построено на основе паттерна MVC, и выглядит оно так:

Но создавать с помощью этого приложения контент могут не только съёмочные группы. К примеру, им может воспользоваться и команда по управлению контентом в моей компании. Почему? Некоторые съёмочные группы не хотят этим заниматься. У съёмочной команды и команда по управлению контентом совершенно разные потребности. Вторые работают с CMS, а первые — нет.

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

Выглядит как очевидное и уродливенькое нарушение принципа DRY: повторяются представления (view) и контроллер.

Другие решения? Можно было бы сгруппировать повторяющуюся логику с помощью своеобразной абстракции. Но тогда пришлось бы объединить контроллеры. Если изменится абстракция, то каждый контроллер должен будет поддерживать это изменение. Мы знали, что в большинстве случаев, в зависимости от использования, эти представления будут отображаться по-разному. Пришлось бы в действиях контроллеров создавать кучу if, а мы этого не хотели. Код стал бы сложнее.

Более того, контроллеры не должны содержать в себе бизнес-логику. Если вспомните определение DRY, то это как раз знание, которое не стоит дублировать.

Иными словами, если пытаться везде применять DRY, то можно прийти к одному из двух:

  1. Ненужному объединению.
  2. Ненужной сложности.

Очевидно, что ни то, ни другое вам не улыбается.

Преждевременная оптимизация

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

Если вы начинаете что-то абстрагировать, потому что «позже это может понадобиться», то не делайте этого. Почему?

  1. Вы потратите время на абстрагирование того, что потом может быть никогда не востребовано. Бизнес-потребности могут изменять очень быстро и очень сильно.
  2. Повторюсь, вы можете привнести в код сложность и объединение ради… пшика.

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

Сначала напишите код, отладьте его, а затем уже держите в уме все эти принципы (DRY, SOLID и прочие), чтобы эффективно рефакторить код. Работать с нарушением принципа DRY нужно тогда, когда знание уже дублировано.

Дублирование знания?

Помните, выше я говорил, что повторение бизнес-логики всегда является нарушением DRY? Очевидно, что это справедливо для ситуаций, когда повторяется одна и та же бизнес-логика.

Пример:

<?php /** Shipment from the warehouse to the customer */
class Shipment
{ public $deliveryTime = 4; //in days public function calculateDeliveryDay(): DateTime { return new \DateTime("now +{$this->deliveryTime} day"); }
} /** Order return of a customer */
class OrderReturn
{ public $returnLimit = 4; //in days public function calculateLastReturnDay(): DateTime { return new \DateTime("now +{$this->returnLimit} day"); }
}

Вы уже слышите, как ваш коллега Лёха нежно вопит вам в ухо: «Это очевидное нарушение всего, во что я верю! А как же принцип DRY? У меня сердце сжимается!».

Но Лёха снова ошибается. С точки зрения электронной коммерции, время доставки товара покупателю (Shipment::calculateDeliveryDay()) не имеет отношения к сроку возврата товара покупателем (Return::calculateLastReturnDay).

Это две разные функциональности. То, что выглядит дублированием кода, на самом деле чистое совпадение. Что произойдёт, если вы объедините два метода в один? Если ваша компания решит, что покупатель теперь может вернуть товар в течение месяца, то вам снова придётся разделить метод. Ведь если этого не сделать, то срок доставки товара тоже будет составлять один месяц!

А это не лучший способ завоевать лояльность клиентов.

DRY — не просто принцип для педантов


Сегодня даже джин может быть DRY!

DRY — это не то, что вам стоит уважать только в своём коде. Не нужно повторять знания в каждом аспекте вашего проекта. Снова процитирую Дэйва Томаса: «Понятие «знания» в системе охватывает далеко не только код. Оно относится и к схемам баз данных, планам тестирования, системе сборки и даже документации».

Суть DRY проста: не нужно обновлять параллельно несколько вещей, если вносится какое-то изменение.

Если ваше знание дважды повторяется в коде, и вы забыли обновить одно представление, то возникнут баги. Это приведёт к заблуждениям и недоразумениям в документации, и к совершенно ошибочной реализации. И так далее, и тому подобное.

DRY — это принцип

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

Я хотел следовать каждой букве этих принципов, чтобы быть хорошим разработчиком.

Однако принципы — не правила. Они лишь инструменты, помогающие идти в правильном направлении.

В разработке у всего есть цена. И DRY не исключение. Очевидно, что не стоит везде повторять бизнес-логику, но также не нужно и тесно объединять всё подряд, экономя на абстракциях. Необходимо искать баланс, который зависит от текущей ситуации.

Показать больше

Похожие публикации

Кнопка «Наверх»