Копипаста
Два участка похожего кода вызывают у многих разработчиков панику и желание тут же вынести их в одно место. Но если убрать эмоции и включить прагматическое мышление, все окажется не так однозначно. Когда дублирование полезно, а когда его стоит избегать?
Мой бывший коллега из Яндекса подсказал мне отрезвляющую шутку с большой долей правды: два участка похожего кода — это норма, из трех уже надо делать функцию. Позже я узнал, что это Rule of Three. Доля правды заключается в следующем: больше повторений — больше информации о том, что, куда и как выносить. Рефакторинг также становится "рентабельным" с точки зрения потраченных усилий.
Когда надо выносить код в общее место:
- Главный вопрос, который стоит себе задать, — "Если я поменяю дублирующийся код только в одном месте, что-то сломается?" Если сломается, значит между участками кода есть неявная связанность (coupling). Это может быть связанность по формату данных или по глобальным объектам. Такой общий код однозначно надо выносить в общее место, если есть такая возможность. Безопасность внесения изменений критична.
- Ничего не сломается, но вы не первый раз вносите аналогичные исправления в похожие участки кода. Например, потому что это все равно имеет конкретную пользу. В этом случае всё равно высока вероятность, что это произойдет еще раз. А в следующий раз вы что-то забудете. Или уже забыли в прошлый. В ревью неизмененный код, куда забыли внести изменения, никто не увидит. Об этом можно только вспомнить, поэтому на ревью такие проблемы легко пропустить.
- Следует делать функцию из вызовов, которые всегда должны идти один за другим, не имеют смысла по отдельности или неявно связанны иным образом.
На практике обычно следует выносить участки прикладного кода длиннее трех строк, между которыми нет очевидной связанности. Пару повторяющихся в общем случае вызовов строк прикладного кода следует оставить.
Когда и зачем надо оставить похожие участки кода в покое:
- Дублирование помогает избавиться от зависимостей. Например, приходится создавать отдельный компонент ради небольшой функции или помещать эту функцию рядом с другими, не имеющими с ней ничего общего. Добавляется дополнительная зависимость, которая не объясняется логически. В таком случае имеет смысл оставить это дублирование.
- Несколько участков кода похожи между собой, но каждый из них имеет небольшие отличия. Если такие участки выносить в одну функцию, в ней образуется нагромождение параметров и условий. Возникнет связанность по контролю (control coupling). Если написать элегантную функцию с четко определенной ответственностью не удается, нестрашно оставить все как есть.
Также следует скептически относиться к вынесению в общее место кода, который не зависит от предметной области. Например, работа с струтурами из контейнеров (list, array, set, map, dict...), повторяющимися проверками (retry), потоками (thread), вводом-выводом. Если код не зависит от предметной области, то нет никаких предпосылок, чтобы он повторялся. Чем может быть вызывано это повторение?
- Привычки разработчика. Индивидуальных привычек не должно быть видно в общем коде. Если привычки настолько заметны и при этом являются индивидуальными, то нужно что-то делать именно с ними, а не с кодом. Не создавать же ради привычек разработчика отдельный компонент?
- Попытка обобщить отдаленно похожие задачи. Например, обобщенная функция
flatten_array
, чтобы в одном случае сделать плоским двумерный массив целых чисел, а в другом — найти элемент в массиве неограниченной вложенности. В первом случае те, кто читает код, будут испытывать диссонанс, потому что часть кода будет указывать на двумерный массив, а часть — на массив неограниченной вложенности. Во втором случае это ещё будет попросту неэффективно. - Похожий паттерн, но не код. Например, функция
retry
. Вначале она будет принимать только другую функцию. Потом надо будет передавать аргументы, количество и интервал повторений, исключения, которые надо игнорировать, сообщения, которые надо писать в лог. В итоге функция со своими параметрами распухнет, пользоваться ей станет неудобно, её вызовбудет занимать больше кода, чем если бы этой функции не было совсем.