Эта статья давно висела у меня в списке задач. Но кажется, только сегодня у меня появились силы и время, чтобы материализовать её. Совпадение или нет, но я в том же кафе, где опубликовал недавно свою первую статью. Наверное, в напитки, которые тут подают, что-то подмешивают...


Так что? Бородатый, хороший совет — следовать лучшим практикам? Мы постоянно слышим о них. Мы даже дали им краткие прозвища, типа DRY или KISS, и используем на автомате в технических разговорах. Мы фанатично следуем концепции, и если кто-то случайно захочет или просто по незнанию не станет их соблюдать, мы выливаем на них вёдра грязной критики. Мы пленники этих убеждений и отказываемся отвернуться от них в нужный момент.


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


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


Как распределяется время программиста


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


Хоть я сам и не уверен, что со мной всё в порядке, я уверен, что не пишу код по десять часов подряд. Суть в том, что мы, как программисты, тратим куда больше времени на чтение кода, а не его написание. Я не уверен, что вы когда-то встречали подобную информацию, но исследования и Роберт C. Мартин утверждают, что для такого вида деятельности существует какая-то пропорция. И я, например, вижу поразительную разницу. Оказывается, на каждый час написания кода, приходится десять часов чтения кода (своего или чьего-то другого).


Это крайне важно. Усилия, которые мы вкладываем за день работы уходят в основном на чтение. Конечно, чтения кода не достаточно. Нужно ещё его понимать. А это значит, что мы должны делать все возможное, чтобы создавать ясный, краткий и легко читаемый код. Это выгодно каждому, включая нас самих в будущем. Имейте это в виду, потому что мы вернемся к этой идее позже.


Заметка про DRY


Для тех, кто не знаком с термином DRY, он означает — не повторяться (Don't Repeat Yourself). Этот принцип программирования или, если хотите — лучший приём, выступает за создание абстракций поверх каждой повторяющейся части кода.


У DRY есть масса преимуществ. Первое — абстракции говорят о том, что, если в будущем потребуются изменения, вам придется справляться с ними в одном месте — в самой абстракции.


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


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


Ну, просто потому что...


Цена абстракций


За каждую абстракцию нужно платить. Затраты не всегда можно оценить сразу. Но со временем они всплывают.


Абстракции содержат дополнительный уровень метаданных. Метаданные, существование которых в принципе может быть вам не понятно (особенно, если не вы их писали). Каждая новая порция информации усиливает когнитивную нагрузку на мозг. И, как результат, время, которое вы тратите на чтение такого куска кода, увеличивается.


Глубокая кроличья нора


Проблема с DRY и фанатичным поклонением этому принципу не заметна в небольших проектах. Но заметна в средних и крупных.


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


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


Как сказала Санди Мец:


Уже написанный код имеет мощный авторитет. Само его существование свидетельствует о его правильности и нужности.

Он заставляет вас не хотеть прикасаться к нему.


Теперь новая фича определенно может использовать ту хорошую абстракцию, которая уже существует. Но, как выясняется, абстракция нуждается в небольшой доработке. Она не была рассчитана на этот конкретный пользовательский сценарий. Если бы только вы могли чуток её поменять… Или, может, написать поверх существующей новую абстракцию, которая инкапсулирует дополнительную повторяющуюся логику, м? Да, кажется это верное решение! Вот что подразумевает DRY...


Прекрасно видно, как мы умеем доводить это безумие до абсолютной крайности. Крайности, где всё инкапсулировано в абстракции. А те, в свою очередь, завёрнуты в более сложные абстракции. И так до бесконечности...


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


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


Потому что некорректные абстракции не только бьют вас по яйцам, они ещё в это время загибаются от смеха.

DRYить или не DRYить


Так когда же инкапсулировать повторяющийся код, а когда нет?


Ответ по сути простой. Но с практической точки зрения не совсем верный. Но это тоже приходит с опытом.


Абстрагируйте всегда, если абстракция не обернётся вам дороже дублированного кода.

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


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


Заключительная мысль в защиту себя


Надеюсь, эта статья не кричит "к чертям этот DRY и другое дерьмо!". Я безоговорочно считаю его хорошим принципом программирования. Но всё же, я призываю вас не следовать ему слепо. Рассматривайте всё, что узнаёте, в контексте и всегда ставьте под сомнение обоснованность своих идей и действий. Это единственный разумный путь к повышению профессионализма.


(Перевод Наталии Басс)

Поделиться с друзьями
-->

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


  1. ov7a
    26.09.2016 12:13
    +8

    tl:dr все хорошо в меру.


    1. pandas
      26.09.2016 12:58
      +2

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


      1. xuexi
        26.09.2016 15:14

        Непонятно, что, чем и в чём измеряли, но в целом, фраза хорошая, жизненная.


      1. KvanTTT
        27.09.2016 01:06
        +4

        Эта фраза хорша в меру.


      1. ov7a
        27.09.2016 12:26

        этот комментарий подходит к любому комментарию :)


  1. oxidmod
    26.09.2016 12:19
    +6

    в программировании нет проблем, которые нельзя решить новым уровнем абстракции, кроме проблемы излишнего количества абстракций)


    1. Dreyk
      26.09.2016 13:18
      +2

      тут проблема не в лишнем количестве абстракций, а в неправильном выборе абстракций


  1. Bonart
    26.09.2016 12:22
    +2

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


  1. t13s
    26.09.2016 13:33
    +4

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

    ИМХО, если у разработчика достаточно квалификации, чтобы аргументировать, почему вот здесь дублирование кода лучше — вероятно, ему просто лень, ибо той же самой квалификации должно хватить на грамотное изменение без Copy-Paste существующего кода.


    1. Johan
      26.09.2016 14:13
      +1

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


      Основной посыл статьи такойже как в The Wrong Abstraction — Sandi Metz:


      duplication is far cheaper than the wrong abstraction


      1. t13s
        26.09.2016 14:45
        +2

        Я не обвиняю, мне просто интересно, как подсчитать, что Copy-Paste дешевле, даже учитывая соотношение read/write кода как 10/1.

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

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

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


        1. Johan
          26.09.2016 15:55

          В моём понимании, тут нужно двигаться от понятия "неверная абстракция". Как правило, с большей или меньшей вероятностью, можно предположить как будет развиваться код: будет он расходится или будет изменятся синхронно. В первом случае, как нетрудно догадаться, делать абстракцию будет излишним, во втором, напротив, необходимо её сделать.


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


          Объясняется это тем, что ввести абстракцию будет дешевле, чем убрать ту, которая не заработала.


          По крайней мере, я понял текст таким образом, и это, более или менее, согласуется с моим опытом.


          1. t13s
            26.09.2016 16:11
            +2

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

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

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


            1. Johan
              26.09.2016 17:14

              Ну, опыт у всех разный, тут уж ничего не попишешь. Отсюда, собственно, и разнообразие подходов, и споры об их достоинствах.


      1. grrrrr
        26.09.2016 15:36
        +3

        В моей практике было наоборот. Копии, когда плодятся, значительно увеличивают вероятность ошибки в коде и количество ручной работы… притом нелинейно. А плохая абстракция скорее может вызвать проблемы в будущем в виде затрат на модификацию (иногда очень серьезные). НО пока эта абстракция пригодна — проблем нет… она будет работать в отличии от копий, которые могут потребовать поддержки уже на старте. Минус абстракций как и любой «универсальности» и притом независимо от правильности — меньшие возможности для оптимизации.


  1. 0x9d8e
    26.09.2016 15:36
    +1

    Как раз недавно продублировал пару методов, чтобы во-первых не городить ещё один класс (в родительский их поместить было бы совсем неправильно), а во-вторых сами классы стали быстрее читаться. Они маленькие и методы в них короткие. Конечно из названий понятно что они делают, но не понятно, например, что они лезут в базу и не кешируют результат, а значит ответственность кешировать ли его (и как) лежит на пользовательском коде. Разумеется можно было бы инкапсулировать кеширование минимум двумя способами (по аргументу или создав обёртки/дочерние классы), но это во-первых ограничивало бы применение кое-каких оптимизаций, а во-вторых маскировало бы «цену обращения» и кто-нибудь обязательно бы начал использовать всё это неправильно, сильно повредив производительности, как уже бывало.
    Это я к тому, что действительно нужно искать компромисс между множеством характеристик, нельзя делать из одной главную в ущерб остальным (например отсутствие дублирований превыше всего). А если где-то что-то было корректно тогда, когда оно было сделано, это не значит, что так будет всегда и не исключено, что вышеописанный пример когда-нибудь не потребует ввести эти абстракции и убрать дублирование. Но это же будет уже другая ситуация, возможность которой учтена и это не займёт много времени.


  1. Vjatcheslav3345
    26.09.2016 16:09

    А разве нельзя писать код так, чтобы потраченное время и объём прочитанного кода был минимальным для принятия каких либо обоснованных решений?
    Может этим, специфическими дизайном, отличаются какие то языки программирования, основанные на этих принципах, важных в коммерческом программировании где времени — всегда мало?


  1. zenkz
    26.09.2016 18:18
    +2

    Увлекаясь DRY-ем можно достигнуть предела «совершенства» в виде классов с методами с кучей if или switch/case логики и безумным числом параметров, заполнение которых опционально.

    Правильно было сказано, что всё хорошо в меру.

    Идеальный код, это код в котором легко можно найти и исправить любую ошибку. Это совокупность приципов KISS и DRY. Т.е. код должен быть написан максимально просто и прямолинейно и в то же время, чтобы исправление пришлось делать только в одном месте в коде.

    Многие опытные разработчики пытаются сделать «красиво» и это приводит к тому, что их код с трудом может понять даже другой senior. Если вы хотите проверить, хороший ли ваш код — покажите его Junior-у и посмотрите насколько быстро он в коде разберётся.


    1. Bonart
      26.09.2016 18:49

      Увлекаясь DRY-ем можно достигнуть предела «совершенства» в виде классов с методами с кучей if или switch/case логики и безумным числом параметров, заполнение которых опционально.

      Стратегию и замену условной логики полиморфизмом уже отменили?


      1. zenkz
        26.09.2016 19:29

        Конечно нет. Это отличные решения для избавления от проблемы.
        Но часто в реальной жизни новая логика дописывается к уже существующей и часто нет возможности провести глобальный рефакторинг для добавления новой абстракции или внедрение паттерна (той же Стратегии к примеру). Конечно в идеальном сценарии ничего рефакторить не прийдётся, т.к. этот случай должен быть предусмотрен архитектурой, но мир не идеален…
        С другой стороны не будете же вы добавлять «Стратегию» на каждый создаваемый вами класс?!


        1. Bonart
          26.09.2016 20:34

          А зачем глобальный рефакторинг?
          Рефакторинг делается ровно такой, чтобы модификации сводились к дописыванию кода.
          Стратегия чаще всего реализуется банальным делегатом (или функцией высшего порядка).
          Использование паттерна — это не мегастройка, а чистка зубов.