Введение 

Принцип “Не повторяйся” (Don't Repeat Yourself, или DRY), то есть избегай дублирования кода, часто относят к обязательным практикам в программировании. Однако в реальности часто можно увидеть, как в общем коде оказываются концептуально разные блоки, которые похожи только по внешним параметрам. Это неминуемо приводит к ухудшению кода и появлению "костылей", без которых он не работает. Именно поэтому слепое следование принципу DRY не всегда целесообразно! В этой статьей я расскажу про типичные ошибки при использовании этого правила и способы их избежать.

Принцип DRY

DRY – это принцип разработки программного обеспечения, призванный минимизировать дублирование информации в коде. Согласно DRY, «Каждая единица знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы» («Every piece of knowledge must have a single, unambiguous, authoritative representation within a system»). На практике это значит, что повторяющиеся части кода должны объединяться в общие функции или модули. 

Подход DRY нацелен на оптимизацию процесса разработки: снижение количества ошибок, упрощение процесса обновления, а также улучшение читаемости кода. Разработчики используют его для создания приложений с повторно используемыми компонентами, упрощая поддержку и обновление программного обеспечения.

Когда принцип DRY работает как надо

DRY часто используют для создания общих функций или методов, которые выполняют в системе повторяющуюся задачу. Рассмотрим простой пример, где применим DRY: нахождение площади квадрата и прямоугольника.

# Нахождение площади квадрата

side_length = 5

area_square = side_length * side_length

print(f"Площадь квадрата: {area_square}")

#: Нахождение площади прямоугольника

length = 5

width = 10

area_rectangle = length * width

print(f"Площадь прямоугольника: {area_rectangle}")

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

Здесь легко применить DRY, создав общую функцию для расчета площади и квадрата, и для прямоугольника сразу. Для этого достаточно создать функцию, которая считает площадь квадрата, если  указан только один параметр, и прямоугольника, если указаны два параметра (длина и ширина).

def calculate_area(length, width=None):

      if width is None:

        width = length

    return length * width

# Использование функции для расчета площади

area_square = calculate_area(5)

print(f"Площадь квадрата: {area_square}")

area_rectangle = calculate_area(5, 10)

print(f"Площадь прямоугольника: {area_rectangle}")

Объединив два разных случая вычисления площади в одну универсальную функцию, можно упростить процесс поддержания кода.

Когда принцип DRY ведет к проблемам 

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

Представим, что мы разрабатываем стартап. Мы моделируем сущности “космический корабль” и “наноробот размером с атом”. Для обеих сущностей необходимо реализовать функцию расчета скорости. Учитывая, что наш стартап находится на ранней стадии развития и высокая точность расчетов нам не требуется, формула для расчета скорости космического корабля и наноробота совпадает. Поэтому кажется логичным выделить функцию расчета скорости в общий модуль.

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

=002c2 

Где 0– скорость объекта в инерциальной системе отсчета, а с – скорость света.

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

А так как модуль поиска скоростей у нас общий, нам придется итерация за итерацией совмещать в нем релятивистскую и квантовую скорости, что будет вызывать появление “костылей” чтобы объединить две принципиально разных физических теории. Также, эти “костыли” будут нужны и в точках его вызова модуля. По мере роста проекта количество “костылей” в коде будет непременно расти, делая его поддержание, обновление и дебаггинг все более сложным и ресурсозатратным процессом. В какой-то момент проект станет настолько сложным, что будет проще его выкинуть и сделать новый с нуля, чем пытаться исправить плохо работающий код с большим количеством “костылей”. А повторный запуск проекта сам по себе процесс недешовый!

Accidental Duplication и различие между дублированием кода и знаний

Таким образом, мы выяснили, что слепое использование DRY может навредить проекту. Именно поэтому рефакторинг или объединение кода должны опираться на глубокое понимание причин и природы дублирования – без них оптимизировать программное обеспечение невозможно.

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

Однако, не всегда дублирование кода означает необходимость выноса его в общие модули. Иногда возникает случайное дублирование (accidental duplication), когда модули, описывающие разные знания (разные предметные области или разные процессы), в какой-то момент времени случайно начинают быть похожими друг на друга. Однако это не означает, что их код будет всегда похожим - различные предметные области будут развиваться в разных направлениях, и вскоре код в них начнет значительно расходиться. Слияние такого кода в общие модули может нанести вред кодовой базе, особенно если такой общий модуль станет одним из базовых элементов приложения. Это может привести к ухудшению качества всей кодовой базы. В таком случае необходимо оставить дублирование кода!

Но как же принцип DRY? Он же не допускает дублирования! Принцип DRY не рекомендует просто слепо объединять одинаковые блоки кода. Он призывает к тому, чтобы каждый фрагмент знания имел только одно, четкое и авторитетное представление в системе. Поэтому важно различать между схожими блоками кода, которые описывают различные аспекты знаний, и действительно одинаковыми блоками кода, которые дублируют одно и то же знание. При применении принципа DRY важно сосредотачиваться на устранении дублирования смысловой информации, а не просто кода.

Что советует литература?

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

Three Strikes And You Refactor ("Три попытки, и ты рефакторишь") 

Это принцип, который гласит: если вы трижды сталкиваетесь с необходимостью внести одно и то же изменение в ваш код или вы обнаруживаете одну и ту же проблему в трех разных местах, это говорит нам о том, что в разных частях кода хоть и отсутствует дублированный код но существует дублирование знаний. Значит, пора произвести рефакторинг, выделив явно это знание в виде нового модуля, который может быть использован другими. Такой подход помогает непрерывно улучшать структуру кода, устраняя дублирование и предупреждая накопление технического долга.

Доменно-ориентированное проектирование (DDD) и Event Storming

Доменно-ориентированное проектирование (DDD) – это подход к разработке программного обеспечения, при котором упор делается на создание софта, который отражает реальные бизнес-процессы и проблемы.

Одной из важных методик DDD является Event Storming, которая предназначена для совместной работы между экспертами предметной области и разработчиками программного обеспечения с целью выявления важных событий (events), команд (command), сущностей (entity), которые играют ключевую роль в бизнес-процессах.

Event Storming обычно проводится в формате рабочей сессии, на протяжении которой участники используют цветные стикеры для обозначения различных элементов домена на большой бумажной дорожке или стене. Каждый цвет стикера представляет собой определенный тип элемента, такой как событие, команда, сущность или ограниченный контекст.

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

Заключение

Принцип DRY — это полезный инструмент разработки, который часто используют неправильно, что приводит к проблемам в процессе разработки. Поэтому для оптимизации кодовой базы крайне важно понимать причины дублирования и его разновидности. А сделать обоснованные решения о структуре и организации кода для его долгосрочной устойчивости и масштабируемости помогут практики моделирования, такие как DDD и Event Storming.

Комментарии (4)


  1. hira
    15.04.2024 11:00
    +5

    Почему SQL? Он даже не упоминается в статье.


    1. pahomovda Автор
      15.04.2024 11:00
      +2

      ошибочка произошла, исправил, спасибо


  1. NightShad0w
    15.04.2024 11:00
    +3

    Использовать DRY, опираясь на реализацию - приводит к описанным проблемам. Использовать DRY, опираясь на контракт и выделяя абстракции как наиболее стабильные части - находится за пределами бытового понимания.


  1. super_botan
    15.04.2024 11:00
    +1

    В DDD есть понятие агрегата. По моему мнению именно разделение кода на агрегаты ведёт к корректному соблюдению DRY.

    SOLID буква I - Interface Segregation обозначает, как мне кажется, в том числе и по зоне применимости. Мне понравился пример со скоростями. Забираю себе закладки как хороший пример для объяснения буквы I :)