Я очень хорошо помню свой первый опыт работы с устаревшим кодом. Я была младшим разработчиком и совершенно не представляла, что делаю.
Само приложение было чем-то вроде 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 и других систем контроля версий вы можете легко отследить это.
Читайте также:
- Топ-5: непреднамеренная ложь программистов
- Какие ошибки можно допустить в описании пользовательских сценариев и как их исправить
- Как украсть секреты разработчиков через Websocket?
Перевод статьи Meriam Kharbat: “How I Failed to Deal With Legacy Code”