Skip to content

Latest commit

 

History

History
169 lines (118 loc) · 10.9 KB

07-duplication.md

File metadata and controls

169 lines (118 loc) · 10.9 KB

Дублирование кода

Главная цель рефакторинга — сделать код более читабельным. Один из способов этого достичь — уменьшить в нём количество шума.

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

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

Не любое дублирование — зло

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

Но бывает, что две части кода «вроде похожи», а спустя время оказываются совершенно разными. Если поспешить и объединить их слишком рано, то распиливать такой код будет сложнее, чем объединять действительно одинаковый код позже.

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

/** @duplicate Применяет купон на скидку к заказу. */
function applyCoupon(order, coupon) {}

/** @duplicate Применяет купон на скидку к заказу. */
function applyDiscount(order, discount) {}

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

К слову ⏰
Регулярные аудиты в своих проектах я воспринимаю как часть выплаты технического долга. Для подобных периодических задач я завожу списки дел. Внутри списка я указываю, что надо сделать в рамках той или иной задачи. Техника регулярных аудитов и её польза хорошо описана у Максима Дорофеева в «Джедайских техниках».1

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

Переменные для данных

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

// Если условия расположены близко,
// или используются рядом:

if (user.age < 18) toggleParentControl();
// ...
if (user.age < 18) askParents();

// Мы можем вынести выражение в переменную:

const isChild = user.age < 18;

if (isChild) toggleParentControl();
// ...
if (isChild) askParents();

Иногда дублирование может быть менее очевидным, и заметить его сложнее:

// Второе условие «вывернуто»
// и использует переменную `years`,
// а не поле объекта напрямую.

if (user.age < 18) askParents();
// ...
const { age: years } = user;
if (years >= 18) askDocuments();

// Но мы всё ещё можем избавиться от него,
// вынеся выражение в переменную:

const isChild = user.age < 18;

if (isChild) askParents();
// ...
if (!isChild) askDocuments();
Подробнее 🔬
Об упрощении и распутывании сложных условий мы детальнее поговорим в одной из следующих глав.

Функции для действий

Повторяющиеся действия или преобразования данных удобно выносить в функции и методы. Определять дубликаты помогает «проверка на одинаковость»:

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

В примере ниже фрагменты кода такую проверку проходят:

// - Цель: добавить поле со абсолютным значением скидки к заказу;
// - Область: объект заказа;
// - Прямые данные: заказ без скидки, относительное значение скидки;
// - Непрямые данные: конвертер из процентов в абсолютное значение.

// a)
const fromPercent = (amount, percent) => (amount * percent) / 100;

const order = {};
order.discount = fromPercent(order.total, 50);

// b)
const order = {};
const discount = (order.total * percent) / 100;
const discounted = { ...order, discount };

// Действия одинаковые, мы можем вынести их в отдельную функцию:

function applyDiscount(order, percent) {
  const discount = (order.total * percent) / 100;
  return { ...order, discount };
}

В другом примере цель и прямые входные данные фрагментов кода одинаковые, а вот зависимости отличаются:

// Первый фрагмент считает скидку в процентах,
// а второй использует «скидку дня» — `todayDiscount`.

// a)
const order = {};
const discount = (order.total * percent) / 100;
const discounted = { ...order, discount };

// b)
const todayDiscount = () => {
  // ...Подбор скидки к сегодняшнему дню.
};

const order = {};
const discount = todayDiscount();
const discounted = { ...order, discount };

В примере выше у фрагмента “b” среди зависимостей есть функция todayDiscount. Из-за неё наборы действий отличаются достаточно, чтобы считать их «похожими», но не «одинаковыми».

Мы можем использовать @duplicate-метки и проследить за развитием событий, чтобы получить больше информации о том, как работает предметная область. Когда мы точно знаем и уверены, как должны работать эти фрагменты, мы можем действия «обобщить»:

// Обобщённая функция будет принимать
// абсолютное значение скидки:

function applyDiscount(order, discount) {
  return {...order, discount}
}

// Отличия в подсчёте (процент от суммы, «скидка дня» и т.д.)
// соберём в виде отдельного набора функций:

const discountOptions = {
  percent: (order, percent)  => order.total * percent / 100
  daily: daysDiscount()
}

// В результате получим обобщённую функцию
// и словарь со скидками разных видов.
// Тогда применение любой скидки станет единообразным:

const a = applyDiscount(order, discountOptions.daily)
const b = applyDiscount(order, discountOptions.percent(order, 40))
Подробнее 🔬
Детально об обобщённых алгоритмах, их использовании и параметризации мы поговорим в отдельной главе.

Footnotes

  1. «Джедайские техники» Максим Дорофеев, https://www.goodreads.com/book/show/34656521