Переиспользование кода

Что это такое

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

Почему это важно

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

Ключевые идеи

Два появления — ещё не доказательство

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

// два похожих блока — ещё не обязательно одна функция
const price = base * 1.2;        // розница
const cost = base * 1.07;       // опт

Выделяйте поведение, а не форму

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

// одна форма, разные правила — держите их порознь
const isAdult = (age) => age >= 18;         // правовые нормы
const isSenior = (age) => age >= 65;       // ценообразование

Общая функция — это контракт с каждым вызывающим

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

Для переиспользования предпочитайте композицию наследованию

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

class RetryingClient {
  constructor(private inner: Client) {}
  send(request) { /* цикл повторов с делегированием this.inner */ }
}

Дублирование в тестах обычно нормально

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

Практика

Найдите настоящее дублирование и докажите ось

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

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

Отмените чрезмерную абстракцию

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

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

Частые ошибки

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

Что читать дальше

  • Чистый код (01-cs-fundamentals/08-clean-code) — первыми идут имена и размер функций; общая функция с плохим именем хуже дублирования.
  • Рефакторинг (01-cs-fundamentals/19-refactoring) — безопасная механика слияния и разделения кода, когда тесты на месте.
  • Принципы SOLID (01-cs-fundamentals/09-solid-principles) — правила проектирования, которые оставляют общую абстракцию открытой для новых вызывающих.