Был поздний вечер.
Мой коллега только что записал в репозиторий код, над которым работал целую неделю. Мы делали тогда графический редактор, а в свежем коде были реализованы возможности по изменению фигур, находящихся в рабочем поле. Фигуры, например — прямоугольники и овалы, можно было модифицировать, перемещая небольшие маркеры, расположенные на их краях.
Код работал.
Но в нём было много повторяющихся однотипных конструкций. Каждая фигура (вроде того же прямоугольника или овала) обладала различным набором маркеров. Перемещение этих маркеров в разных направлениях по-разному влияло на позицию и размер фигуры. А если пользователь, двигая маркеры, удерживал нажатой клавишу 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
и отправился спать, гордый тем, как я «причесал» неопрятный код коллеги.
Следующее утро
…прошло не так, как ожидалось.
Руководитель пригласил меня на разговор за закрытыми дверями, в ходе которого вежливо попросил меня откатить мои изменения. Меня это потрясло. Ведь старый код — это же сплошной бардак. А мой код был чистым.
Я нехотя исполнил просьбу, но мне понадобились годы для того, чтобы понять правоту руководителя и коллеги.
Это — одна из ступеней развития программиста
Зацикленность на «чистом коде» и стремление к удалению дубликатов — это одна из ступеней развития программиста, через которую проходили многие из нас. Когда мы не чувствуем уверенности в своём коде, возникает желание привязать наше восприятие самооценки и профессиональной гордости к чему-то такому, что можно измерить. Набор строгих правил линтинга, схема именования сущностей, структура файлов проекта, минимизация дублирующегося кода.
Нельзя автоматизировать борьбу с повторяющимся кодом, но эта борьба, совершенно точно, становится легче с практикой. Обычно после каждого изменения можно сказать о том, больше или меньше одинаковых конструкций стало в проекте. В результате избавление от дублирующихся фрагментов программы воспринимается как улучшение некоего объективного показателя качества кода. И, что хуже, это искажает самосознание программиста: «Я из тех, кто пишет чистый код». Это так же сильно влияет на человека, как и любой другой род самообмана.
Как только программист узнаёт о том, как создавать абстракции, он вполне может этим увлечься. Он, видя дублирующийся код, будет находить абстракции там, где их нет. А после нескольких лет такой практики дублирующийся код будет обнаруживаться абсолютно везде. Абстрагирование станет новым талантом программиста. Если кто-то скажет ему, что абстрагирование — это добродетель, он это примет. И он начнёт осуждать тех, кто не преклоняется перед «чистотой».
Теперь я понимаю, что у моего «рефакторинга», и у того, как я к нему подошёл, было два серьёзных недостатка:
- Во-первых, я не поговорил с тем, кто написал код. Я переписал код и залил его в репозиторий, не обсудив изменения с автором кода. Даже если мои изменения улучшали бы программу (я больше так не считаю), это — демонстрация совершенно неправильного подхода к подобным делам. В основе здоровой команды программистов лежит постоянное укрепление доверия. А если переписать код одного из членов команды и с ним не посоветоваться — это нанесёт удар по возможности эффективной совместной работы над проектом.
- Во-вторых, за всё надо платить. Мой код пожертвовал возможностью менять требования в угоду сокращения объёма дублирования. Цена этой жертвы была слишком высока. Например, позже нам понадобилось обрабатывать множество особых условий и вариантов поведения для различных маркеров разных фигур. Мою абстракцию для поддержки подобных требований пришлось бы основательно усложнить. А вот в исходную «неаккуратную» версию кода подобные изменения вносились легче лёгкого.
Говорю ли я о том, что вам нужно писать «грязный» код? Нет. Я предлагаю лишь хорошо подумать над тем, что имеют в виду под понятиями «чистый» и «грязный». Что-то приводит вас в негодование? Вы чувствуете в чём-то правильность, красоту, изящество? Уверены ли вы в том, что можете применять подобные понятия, описывая конкретные результаты работы программистов? Каким образом эти понятия влияют на то, как пишут и модифицируют код?
Я, конечно, о таких вещах тогда не думал. Я много размышлял о том, как выглядел код — но не о том, как он развивался вместе с командой, состоящей из людей, которым не чуждо ничто человеческое.
Программирование — это путь. Подумайте о том, как далеко вы продвинулись от написанной вами первой строчки кода, до того места, где находитесь сейчас. Я полагаю, что вы, в первый раз извлекая функцию или подвергая класс рефакторингу, с восхищением наблюдали за тем, как эти приёмы упрощают сложный код. Если вы чувствуете гордость за своё дело, то у вас появится стремление к преследованию чистоты кода. Поддайтесь на некоторое время этому стремлению.
Но не останавливайтесь на этом. Не становитесь фанатичным приверженцем чистого кода. Чистый код — это не цель. Это — попытка как-то осмыслить огромную сложность систем, с которыми мы имеем дело. Это защитный механизм, который программист применяет тогда, кода ещё не уверен в том, как некое изменение повлияет на кодовую базу проекта, но ищет ориентиры в море неизвестности.
Пусть идея чистого кода станет вашим ориентиром. А потом — отпустите эту идею.
Уважаемые читатели! Как вы относитесь к «чистому коду»?
Автор: ru_vds