Я очень хорошо помню свой первый опыт работы с устаревшим кодом. Я была младшим разработчиком и совершенно не представляла, что делаю.
Само приложение было чем-то вроде Slack, где сотрудники могли создавать рабочие пространства, чтобы автоматически делиться каждым событием расчета с клиентами.
У основателей не было никакого технического образования. У них была идея, как улучшить взаимодействие в команде, и они наняли людей для реализации первой версии. Каждая из последующих функций была реализована разными фрилансерами.
Некоторые части были на AngularJS, в то время как другие использовали Django и Tornado. Во всем этом не было никакого смысла.
Для меня код выглядел ржавая паровая машина из “Ходячего замка Хаула”: нечто едва ли не разваливающееся, медленно ползущее вперед.
Мне было страшно добавить что-нибудь в эту базу кода. Стоило мне исправить одну ошибку, как появлялась другая.
Мне бы следовало знать тогда, что отсутствие структуры, дублирование кода, тесная связанность и трудность добавления новых функций были предупреждающими знаками ужасающего и хрупкого кода.
Поддерживать чужой код — это неприятный процесс. Я решила, что лучше всего сделать из него tabula rasa и начать с нуля с того, что представлялось мне чистой архитектурой.
Этот опыт переписывания оказался очень напряженным и отнял много времени. С одной стороны, я думаю, что пострадала от ошибки планирования: я была слишком оптимистична в отношении того, сколько времени потребуется на подготовку продакшн-версии. С другой стороны, я полностью перепроектировала некоторые части.
Кроме того, поскольку за время переписывания не добавилось никаких новых функций, мой менеджер не видел никаких результатов. Он был разочарован и перенаправлял свой стресс на меня.
Спеша скорее дойти до продакшена, я решила кое-где срезать, что привело к ошибкам в дизайне. Так что пришлось возвращаться к исходной точке.
Когда я думаю об этом, то почти уверена: каждый программист, работавший над этим сокрушающим дух устаревшим приложением, чувствовал то же самое. Каждый из них начинал что-то новое, используя тот фреймворк или язык, с которым они были знакомы. Каждый из них привносил все больше сложностей, и именно поэтому мы оказались в такой неразберихе.
Переписанный вами код не будет лучше, чем существующий.
Код эволюционирует. Существует множество внешних и внутренних сил, которые привнесут в блестящий новый проект конструктивные недостатки. Появятся новые функции, изменятся требования, некоторые части потеряют актуальность, а некоторые конечные точки устареют.
Никак! Вам это не понадобится.
«You aren’t gonna need it» (YAGNI) — это концепция экстремального программирования, которая гласит, что вы не должны создавать что-то в данный момент только потому, что вам это может понадобиться в будущем.
Попытка разработать что-то для будущего варианта использования только усложняет проект. Лучше написать именно то, что вам нужно. Каждая фича должна быть добавлена обдуманно.
И точно так же вы должны неохотно добавлять зависимости и фреймворки. Как однажды сказал мне один старый наставник:
“Всякий раз, добавляя инструмент или зависимость, подумай, сколько усилий уйдет на то, чтобы избавиться от них”.
Этот принцип можно распространить и шире. При написании нового компонента вы должны спроектировать его таким образом, чтобы он мог быть легко удален в будущем, если команда разработчиков продукта в один прекрасный день решит отказаться от этого функционала или изменить требования.
При проверке концепций (Proof of Concept, POC) разработчики обычно пользуются возможностью поразвлечься и исследовать новые стеки технологий. Нюанс в том, что эти временные POC становятся в конечном счете базой для проекта. Все строится поверх них, и фреймворк или язык, выбранные изначально, никогда не меняются.
Именно так и выходит, что в одном и том же стартапе вы можете найти один проект на Go, другой на Python, а третий — на NodeJs.
Даже обходные пути не являются временными. Недавно я наткнулась на комментарий в базе кода, который описывал временную реализацию. По-быстрому поискав в GitHub, я выяснила, что этот временный код существовал в течение вот уже нескольких лет.
Небольшие хаки накапливаются до тех пор, пока код не станет устаревшей системой. В “Getting Real” команда Basecamp пишет:
“Скомпоновали блок кода, который хоть и функционален, но все еще неопрятен — вот вы и набрали долгов. Набросали дизайн по принципу «и так сойдет» — ваши долги выросли опять.Время от времени можно так поступать. Часто такая техника помогает поскорее довести проект до конца и побыстрее. Но все равно нужно признать эти долги и рано или поздно расплатиться с ними — вычистить неопрятный код, переделать ту страницу, которая была сделана так себе”.
В знаменитом посте “Вещи, которых вы никогда не должны делать” Джоэл Сполски пишет:
“Идея о том, что новый код лучше старого, совершенно абсурдна. Старый код использовался. Он был протестирован. Было обнаружено множество багов, и они были исправлены. В этом нет ничего плохого. Код не приобретает ошибок, просто занимая место на жестком диске. Напротив, детка! Неужели программное обеспечение должно быть похоже на старый Dodge Dart, который ржавеет, просто стоя в гараже? Разве программное обеспечение похоже на плюшевого мишку, который выглядит хуже только потому, что не сделан из нового материала?”
К настоящему моменту я согласна, что переписывание редко оправдано и на его завершение может уйти целая вечность, но в остальном я не могу согласиться с Джоэлом. Дело в том, что если вы оставите проект достаточно надолго, он действительно заржавеет.
Пример, который приходит мне на ум, — это веб-приложение, которое было реализовано на Angular1. После внедрения некоторых важных функций у нас не осталось в производстве ничего для этого приложения, и я перешла к работе над другим проектом.
К тому времени вышел Angular2. Хотя Angular1 официально не устарел, библиотеки, которые мы использовали для этого проекта, теперь не обслуживаются.
В данном случае одним из решений было бы использовать шаблон подавления и заменять фреймворки по частям. Недостаток здесь в том, что рефакторинг может занять целую вечность, два стека будут расходиться, и будет сложнее ввести в курс дела людей, приходящих на проект.
Извлеченный урок: фреймворки упорны по определению. Они навязывают свои лучшие архитектурные практики и соглашения о кодировании, и их нелегко отбросить!
На одном проекте нам пришлось разбирать некоторое количество логов. Мы решили положиться на Fluentd и написали плагин, который использовал регулярные выражения для разбора лог-потоков.
Работа с регулярными выражениями может быть нервной. Я постоянно обнаруживала, что работаю с только временами читаемой путаницей. На следующий день или даже сразу после перерыва мне сначала приходилось расшифровывать регулярное выражение.
Если бы я отдавала приоритет удобочитаемости, то могла бы признаться самой себе: да, это решение, похоже, работает, но оно неприемлемо. Время, которое уходило на то, чтобы понять код, оказывалось огромным даже для меня, человека, который его написал! Несомненно, существовали и более простые решения.
Написание чистого кода — это о том, насколько легко будет взаимодействовать с ним спустя шесть месяцев.
Устаревший код — это непротестированный код, который используется в продакшене. Другими словами, это та часть кода, к которой все боятся прикасаться.
Вот несколько подходов к рефакторингу устаревшего кода:
Один старый коллега как-то научил меня, что при написании тестов для устаревшего кода проще работать с ним итеративно.
Из-за жесткой связанности устаревший код часто нелегко оснащать заглушками и изолировать, что затрудняет написание модульных тестов. Зачастую проще начать с интеграционных тестов.
Точно так же легче начинать тестирование с внешних ветвей. Представьте, что у вас есть конечная точка API, которая обновляет некоторые свойства. Конечная точка сначала проверяет, что пользователь прошел аутентификацию и имеет соответствующую пользовательскую роль, а затем переходит к проверке данных на вводе и обновлению.
Самый простой тестовый случай, с которого можно было бы начать, — это тот, в котором пользователь не проходит проверку аутентификации. Затем тестовый случай, когда пользователь проходит проверку, но имеет неправильную роль. Затем тестовый случай, в котором пользователь проходит аутентификацию, имеет правильную пользовательскую роль, но ввод при этом неверный и т. д.
Вы можете медленно выстраивать путь к внутренней части кода, пока не достигнете полного покрытия.
Как раз недавно я перемещала некоторые функциональные возможности из интерфейса в API. У меня было искушение изменить какой-нибудь код в процессе работы. Чтобы избежать путаницы, один коллега дал мне самый лучший совет: рефакторить постепенно, небольшими коммитами.
Рефакторинг не должен изменять поведение кода. Он должен только поменять дизайн. Если вы хотите изменить что-то еще, сделайте это позже в отдельном пулл-реквесте.
Еще один практический совет относительно инкрементного рефакторинга: постепенное устаревание.
Представьте, что вы переименовываете функцию или меняете ее сигнатуру, и все существующие тесты начинают давать сбой. Вместо этого вы можете создать функцию со старым именем, которая просто вызывает новую функцию. Таким образом, тесты не будут провалены, и вы сможете постепенно обновлять их.
Примо Леви в своей книге “Периодическая система” делится анекдотом, который вам, вероятно, знаком:
“Он был химиком, и его завораживал тот факт, что рецепт лака включал в себя сырой лук. А для чего это могло быть нужно? Никто этого не знал, это была просто часть рецептуры. Поэтому он провел исследование и в конце концов обнаружил, что лук начали добавлять много лет назад, чтобы проверить температуру лака: если тот был достаточно горячим, лук поджарился бы”.
При рефакторинге устаревшего кода вы, несомненно, столкнетесь с кое-каким “луком”. Это всего лишь обрывки условий и предположений, но никто уже не помнит, для чего они здесь.
Возможно, у вас возникнет соблазн удалить их сразу же, но важно сначала изучить их и понять, почему и когда они были добавлены. К счастью, с помощью GitHub и других систем контроля версий вы можете легко отследить это.
Перевод статьи Meriam Kharbat: “How I Failed to Deal With Legacy Code”
Комментарии