Разработка
Прощай, чистый код
Не будьте фанатиком чистого кода. Чистый код — это не цель. Это попытка найти хоть какой-то смысл в огромной сложности систем, с которыми мы имеем дело.
Был поздний вечер.
Мой коллега только что отправил код, который они писали всю неделю. Мы работали над графическим редактором, и они реализовали возможность изменять размеры таких фигур, как прямоугольники и овалы, перетаскивая маленькие переключатели по их краям.
Код работал.
Но он был повторяющимся. У каждой фигуры (например, прямоугольника или овала) был свой набор изменяемых размеров, и перетаскивание каждого в разных направлениях по-разному влияло на положение и размер фигуры. Если пользователь удерживал Shift, нам также нужно было сохранять пропорции при изменении размера. Было много математики.
Код выглядел примерно так:
let Rectangle = { resizeTopLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeTopRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, }; let Oval = { resizeLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeTop(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottom(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, }; let Header = { resizeLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, } let TextBlock = { resizeTopLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeTopRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomLeft(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, resizeBottomRight(position, size, preserveAspect, dx, dy) { // 10 repetitive lines of math }, };
Эта повторяющаяся математика меня очень беспокоила.
Она не была чистой.
Большинство повторений было между похожими фигурами. Например, Oval.resizeLeft()
имел сходство с Header.resizeLeft()
. Это объясняется тем, что они оба работают с перетаскиванием переключателя с левой стороны.
Другое сходство было между методами для одной и той же фигуры. Например, Oval.resizeLeft()
имеет сходство с другими методами Oval
. Это объясняется тем, что все они работают с овалом. Также было некоторое дублирование между Rectangle
, Header
и TextBlock
, потому что текстовые блоки были прямоугольниками.
У меня возникла идея.
Мы можем убрать все дублирование, сгруппировав код следующим образом:
top(...) { // 5 unique lines of math }, left(...) { // 5 unique lines of math }, bottom(...) { // 5 unique lines of math }, right(...) { // 5 unique lines of math }, }; let Shapes = { Oval(...) { // 5 unique lines of math }, Rectangle(...) { // 5 unique lines of math }, }
А затем составлять композиции их поведения:
let {top, bottom, left, right} = Directions; function createHandle(directions) { // 20 lines of code } 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 lines of code } let Rectangle = createBox(Shapes.Rectangle, fourCorners); let Oval = createBox(Shapes.Oval, fourSides); let Header = createBox(Shapes.Rectangle, twoSides); let TextBox = createBox(Shapes.Rectangle, fourCorners);
Код стал вдвое меньше, а дублирование полностью исчезло! Очень чисто. Если мы хотим изменить поведение для определенного направления или формы, мы можем сделать это в одном месте, а не обновлять методы по всему периметру.
Была уже глубокая ночь (я увлекся). Я проверил свой рефакторинг в мастере и отправился спать, гордясь тем, что распутал запутанный код своего коллеги.
Следующим утром
…Все пошло не так, как ожидалось.
Мой босс пригласил меня на личную беседу, где вежливо попросил меня вернуть изменения. Я был в ужасе. Старый код был беспорядочным, а мой — чистым!
Я нехотя согласился, но мне потребовались годы, чтобы понять, что они были правы.
Это просто такой этап
Зацикленность на «чистом коде» и устранении дублирования — это этап, через который проходят многие из нас. Когда мы не чувствуем уверенности в своем коде, возникает соблазн привязать чувство собственного достоинства и профессиональной гордости к чему-то, что можно измерить. Набор строгих правил линтинга, схема именования, файловая структура, отсутствие дублирования.
Вы не можете автоматизировать удаление дублирования, но с практикой это становится проще. Обычно после каждого изменения вы можете определить, стало его меньше или больше. В результате удаление дублирования выглядит как улучшение объективной метрики кода. А наличие дублирования нарушает чувство идентичности людей: «Я из тех, кто пишет чистый код». Это так же сильно, как и любой другой вид самообмана.
Как только мы научимся создавать абстракции, возникает соблазн получить кайф от этой способности и извлекать абстракции из воздуха всякий раз, когда мы видим повторяющийся код. После нескольких лет работы над кодом мы видим повторы повсюду — и абстрагирование становится нашей новой суперспособностью. Если кто-то скажет нам, что абстракция — это недостаток, мы его съедим. И начнем осуждать других людей за то, что они не поклоняются «чистоте».
Теперь я понимаю, что мой «рефакторинг» был катастрофой в двух отношениях:
- Во-первых, я не поговорил с людьми, которые его написал. Я переписал код и проверил его без их участия. Даже если это было улучшение (во что я уже не верю), это ужасный способ действовать. Здоровая инженерная команда постоянно выстраивает доверие. Переписывание кода товарища по команде без обсуждения — это огромный удар по вашей способности эффективно работать над кодовой базой вместе.
- Во-вторых, ничто не бывает бесплатным. Мой код обменял возможность изменять требования на уменьшение дублирования, и это была не очень хорошая сделка. Например, позже нам понадобилось множество особых случаев и моделей поведения для разных переключателей на разных формах. Моя абстракция должна была стать в несколько раз более запутанной, чтобы позволить себе это, в то время как в первоначальной «грязной» версии такие изменения были бы простыми.
Говорю ли я, что вы должны писать «грязный» код? Нет. Я предлагаю глубоко задуматься над тем, что вы имеете в виду, когда говорите «чистый» или «грязный». Возникает ли у вас чувство недовольства? Праведности? Красоты? Элегантности? Насколько вы уверены, что можете назвать конкретные инженерные результаты, соответствующие этим качествам? Как именно они влияют на то, как пишется и изменяется код?
Я тогда точно не задумывался ни об одной из этих вещей. Я много думал о том, как выглядит код, но не о том, как он развивается в команде слабых людей.
Программирование — это путешествие. Подумайте, как далеко вы продвинулись от своей первой строчки кода до того места, где вы находитесь сейчас. Я считаю, что для меня было радостью впервые увидеть, как извлечение функции или рефакторинг класса могут упростить запутанный код. Если вы гордитесь своим ремеслом, то очень соблазнительно стремиться к чистоте кода. Делайте это некоторое время.
Но не останавливайтесь на достигнутом. Не будьте фанатиком чистого кода. Чистый код — это не цель. Это попытка найти хоть какой-то смысл в огромной сложности систем, с которыми мы имеем дело. Это защитный механизм, когда вы еще не уверены, как изменение повлияет на кодовую базу, но вам нужен ориентир в море неизвестных.
Позвольте чистому коду направлять вас. А потом отпустите его.