Разработка

Не сушите код, нарушайте принцип единственной ответственности, переписывайте всё: (без)вредные советы программистам

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

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


Справка: выражение «сушите код» произошло от акронима DRY — Don’t repeat yourself, не повторяйся.


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

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

Игнорируйте принцип DRY, чтобы находить нужные абстракции

Рекомендация соблюдать принцип DRY — трюизм, общая фраза, которая не имеет смысла вне контекста.

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

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

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

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

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


Как точно заметила Сэнди Мэтц, повторение намного дешевле неправильных абстракций.


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

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

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

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

Объединяйте ответственности, чтобы управлять взаимодействиями между ними

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

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

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

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

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

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

Модульность — это не просто механическое разделение программы на минимально возможные части

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

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

Ещё один наглядный пример принципа единственной ответственности — Git. С помощью git checkout вы можете выполнить шесть разных операций, но все они используют похожие операции под капотом. То есть компоненты можно использовать разными способами, даже если они выполняют одну функцию.

Слой небольших компонентов без общих функций создаёт потребность в дополнительном слое, где функции пересекаются. Если такого слоя нет, пользователь создаст его с помощью алиасов Bash, скриптов или даже с помощью таблицы, из которой можно копипастить данные.

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

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

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

Суть модульности заключается в ограничении вариантов роста

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

Когда у единицы нет правил, определяющих, какой код можно в неё добавить, эта единица принимает в себя больше и больше частей системы. Это всегда верно для модулей с названием util. Также это объясняет, почему в модели MVC большая часть кода попадает в Controller.

В теории модель MVC предполагает объединение кода приложения в три взаимосвязанных блока. Первый блок для баз данных, второй для пользовательского интерфейса, а третий связывает два первых блока. На практике MVC часто представляет собой монолит с двумя подсистемами: первая для базы данных, вторая для пользовательского интерфейса. И обе подсистемы находятся внутри третьей: контроллера.

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

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

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

Переписывайте всё

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

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

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

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

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

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

Если замена не принесла успеха спустя три месяца, она уже никогда не будет успешной.

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

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

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

null === true, запретов не существует

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

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

Принцип единственной ответственности в этом ключе можно перефразировать так: разделение кода на модули полезно, если интерфейсы модулей простые.

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

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

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

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

Адаптированный перевод статьи Repeat yourself, do more than one thing, and rewrite everything by tef. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».

Дмитрий Дементий 14 сентября 2019
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →