[Перевод] Прощай, чистый код

dsrfkmov-s5b1wgeuy1obsbsj7k.jpeg

Был поздний вечер.

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

Код работал.

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

let Rectangle = {
  resizeTopLeft(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeTopRight(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeBottomLeft(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeBottomRight(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
};

let Oval = {
  resizeLeft(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeRight(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeTop(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeBottom(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
};

let Header = {
  resizeLeft(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeRight(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },  
}

let TextBlock = {
  resizeTopLeft(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeTopRight(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeBottomLeft(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
  resizeBottomRight(position, size, preserveAspect, dx, dy) {
    // 10 однотипных строк вычислений
  },
};


Меня все эти вычисления, все эти почти одинаковые строки, сильно зацепили.

Код не был чистым.

Основной объём однотипных строк наблюдался в тех местах, где задавалось перемещение фигур в одном и том же направлении. Например, в методе Oval.resizeLeft() был код, похожий на тот, который можно было найти в Header.resizeLeft(). Дело было в том, что оба эти метода отвечают за изменения фигуры, выполняемые при перемещении маркера влево.

Похожими были и методы фигур, имеющих одну и ту же форму. Например, Oval.resizeLeft() был похож на все остальные методы фигуры Oval. Все эти методы работали с овалами — отсюда и их сходство. Дублирующийся код можно было найти в объектах Rectangle, Header и TextBlock, так как текстовые блоки представляли собой прямоугольники.

У меня появилась идея.

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

let Directions = {
  top(...) {
    // 5 уникальных строк вычислений
  },
  left(...) {
    // 5 уникальных строк вычислений
  },
  bottom(...) {
    // 5 уникальных строк вычислений
  },
  right(...) {
    // 5 уникальных строк вычислений
  },
};

let Shapes = {
  Oval(...) {
    // 5 уникальных строк вычислений
  },
  Rectangle(...) {
    // 5 уникальных строк вычислений
  },
}


Затем можно прибегнуть к композиции и собрать из этих базовых методов то, что нужно:

let {top, bottom, left, right} = Directions;

function createHandle(directions) {
  // 20 строк кода
}

let fourCorners = [
  createHandle([top, left]),
  createHandle([top, right]),
  createHandle([bottom, left]),
  createHandle([bottom, right]),
];
let fourSides = [
  createHandle([top]),
  createHandle([left]),
  createHandle([right]),
  createHandle([bottom]),
];
let twoSides = [
  createHandle([left]),
  createHandle([right]),
];

function createBox(shape, handles) {
  // 20 строк кода
}

let Rectangle = createBox(Shapes.Rectangle, fourCorners);
let Oval = createBox(Shapes.Oval, fourSides);
let Header = createBox(Shapes.Rectangle, twoSides);
let TextBox = createBox(Shapes.Rectangle, fourCorners);


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

Была уже поздняя ночь (я увлёкся). Я влил результаты рефакторинга в ветку master и отправился спать, гордый тем, как я «причесал» неопрятный код коллеги.

Следующее утро


…прошло не так, как ожидалось.

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

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

Это — одна из ступеней развития программиста


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

Нельзя автоматизировать борьбу с повторяющимся кодом, но эта борьба, совершенно точно, становится легче с практикой. Обычно после каждого изменения можно сказать о том, больше или меньше одинаковых конструкций стало в проекте. В результате избавление от дублирующихся фрагментов программы воспринимается как улучшение некоего объективного показателя качества кода. И, что хуже, это искажает самосознание программиста: «Я из тех, кто пишет чистый код». Это так же сильно влияет на человека, как и любой другой род самообмана.

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

Теперь я понимаю, что у моего «рефакторинга», и у того, как я к нему подошёл, было два серьёзных недостатка:

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


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

Я, конечно, о таких вещах тогда не думал. Я много размышлял о том, как выглядел код —, но не о том, как он развивался вместе с командой, состоящей из людей, которым не чуждо ничто человеческое.

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

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

Пусть идея чистого кода станет вашим ориентиром. А потом — отпустите эту идею.

Уважаемые читатели! Как вы относитесь к «чистому коду»?

© Habrahabr.ru