Это случается хотя бы раз в жизни каждого программиста, менеджера проекта или тимлида. Вы получаете целую кучу парного навоза. Если повезёт, то всего несколько миллионов строк. Первоначальные авторы давно улетели в тёплые страны, а документация, если она имеется, безнадёжно устарела.
Ваша задача: выбраться из этого бардака.
После того, как отпустила первая инстинктивная реакция (сбежать подальше), вы начинаете работать над проектом, отлично понимая, что руководители компании следят за вашими успехами. Провал не вариант. Но пока что, судя по раскладу, именно провал кажется наиболее вероятным исходом. Так что делать?
Мне (не) повезло оказаться в такой ситуации несколько раз. И мы с небольшой группой друзей выяснили, что при должных навыках это очень выгодное дело — брать на себя такие кучи дымящегося убожества и превращать их в здоровые поддерживаемые проекты. Вот некоторые хитрости, которые мы используем:
Резервная копия
Перед началом каких-либо действий сделайте резервную копию всего, что может иметь отношение к проекту. Это для гарантии, что не потеряется никакая информация, которая может пригодиться в будущем. Всегда может возникнуть какой-нибудь глупый вопрос, на который вы не в силах ответить через день или два после того, как сделаны изменения. Такого рода проблемы особенно часто возникают с данными конфигурации, их обычно не закладывают в схему версионности, так что будет удачей хотя бы восстановить что-то из периодического бэкапа. Так что лучше сохраниться, чем потом сожалеть. Скопируйте всё в самое безопасное место и никогда в жизни не трогайте его, если оно не в режиме read-only.
Важное необходимое условие, убедитесь в наличии процесса сборки, который действительно производит то, что работает в продакшне
Я полностью упустил этот шаг, предполагая его очевидность и наличие процесса сборки почти у всех, но многие комментаторы на HN указали на него и были абсолютно правы. Первым делом следует убедиться, что работает в продакшне в данный момент. Это значит, что вы должны быть способны собрать версию программного обеспечения, которая — если ваша платформа работает таким образом — будет байт к байту совпадать с текущим билдом в продакшне. Если вы не можете добиться этого, то готовьтесь к некоторым неприятным сюрпризам, когда попробуете отправить какой-то коммит в продакшн. Приложите все усилия для тестирования, и когда всё необходимое будет на месте, и вы будете уверены в работоспособности, выкатывайте в продакшн. Будьте готовы немедленно вернуться на предыдущую версию и убедитесь в тотальном журналировании всего, что может пригодиться в течение — неизбежного — разбора полётов.
Не трогайте БД
Если это возможно, заморозьте схему базы данных до тех пор, пока не закончили первый этап улучшений. Вы будете готовы изменить её, когда появится полное понимание кодовой базы, а legacy-код полностью останется позади. Поменяете схему раньше этого момента — и могут возникнуть реальные проблемы, поскольку вы потеряете возможность запуска старой и новой кодовой базы бок о бок с надёжным фундаментом из базы данных, на которой всё построено. Сохранив БД в полной неприкосновенности, вы можете сравнивать эффект нового кода бизнес-логики со старым. Если всё работает как заявлено, то не должно быть отличий.
Напишите свои тесты
Перед любыми изменениями напишите как можно больше end-to-end и интеграционных тестов. Убедитесь в корректной выдаче этих тестов и протестируйте так много сценариев, как вы только сможете придумать относительно того, как вы думаете, работает старый код (будьте здесь готовы к сюрпризам). Эти тесты будут иметь две важные функции: они помогут устранить любые неправильные представления на самой ранней стадии и они послужат защитным ограждением, если вы начнёте писать новый код взамен старого.
Автоматизируйте всё ваше тестирование, если у вас уже есть опыт CI, то используйте его, и убедитесь, что ваши тесты достаточно быстро работают, чтобы запускать полный набор тестов после каждого коммита.
Оснащение и журналирование
Если старая платформа ещё доступна для разработки и оснащения. Сделайте это в полностью новой таблице БД, добавьте простой счётчик на каждое событие, какое только придумаете, и простую функцию, которая будет увеличивать эти счётчики, основываясь на имени события. Таким способом вы можете реализовать журнал событий с временными метками всего несколькими дополнительными строчками кода и получите хорошее представление, как много событий одного типа ведут к событиям другого типа. Один пример: пользователь открывает приложение, пользователь закрывает приложение. Если два события ведут к неким вызовам бэкенда, то между этими двумя счётчиками в долговременной перспективе должна оставаться постоянная разница, эта разница представляет собой количество приложений, которые открыты в данный момент. Если вы видите намного больше открытий, чем закрытий приложения, вы знаете, что должен быть способ, которым приложение завершает работу (хотя бы сбой). Для каждого события, какое вы найдёте, существует какая-нибудь связь с другими событиями. Обычно вы будете прилагать усилия к достижению постоянных взаимоотношений, за исключением очевидной ошибки где-нибудь в системе. Вы будете стремиться уменьшить те счётчики, которые соответствуют ошибкам, и максимально увеличить счётчики вдоль по цепочке до показателя, заданного в начале. (Например, пользователи, которые пытаются оплатить покупку, должны привести в итоге к полученным платежам).
Этот очень простой трюк превращает любое бэкенд-приложение в систему бухгалтерского учёта, и точно как с настоящей системой бухгалтерского учёта, числа должны совпадать, чтобы у вас нигде не было проблем.
Со временем такой подход станет бесценной в налаживании здоровья программы и отлично дополнится журналом изменений системы управления исходным кодом, в котором вы можете определить точку во времени, когда появился баг, и какой эффект он произвёл на разнообразные счётчики.
Я обычно устанавливаю эти счётчики с пятиминутным разрешением (так что выходит 12 бакетов в час), но если у вас приложение генерирует меньше или больше событий, то вы можете изменить интервал, с которым создаются бакеты. Все счётчики делят одну таблицу БД, так что каждый счётчик — это всего лишь столбец в этой таблице.
Меняйте только одну вещь за раз
Не попадайтесь в ловушку улучшения одновременно эксплуатационной надёжности кода или платформы, на которой он работает, и в то же время добавления новых функций или исправления багов. Это доставит вам огромную головную боль, потому что теперь придётся на каждом шагу спрашивать, каков желаемый исход действия, и это аннулирует некоторые из сделанных ранее тестов.
Изменения платформы
Если вы решили перенести приложение на другую платформу, то сделайте это, но оставьте всё остальное без изменений. Если хотите, можете добавить документации или тестов, но не более того, вся бизнес-логика и взаимозависимости должны сохраниться прежними.
Изменения архитектуры
Следующий этап — изменения в архитектуру приложения (если требуются). К этому моменту вы можете свободно менять высокоуровневую структуру кода, обычно сокращая число горизонтальных связей между модулями и тем самым уменьшая объём кода, активного при каждом взаимодействии с конечным пользователем. Если старый код был монолитным по своей природе, теперь самое время сделать его более модульным, разбить большие функции на меньшие, но сохранить названия переменных и структур данных как были.
Пользователь HN mannykannot обратил внимание — справедливо — что не всегда есть такая возможность, и если вам особенно не повезло, то придётся копнуть глубже, чтобы иметь возможность произвести какие-либо архитектурные изменения. Я с этим согласен и должен был упомянуть об этом, так что поэтому здесь это маленькое дополнение. Ещё хотел бы добавить, что если вы производите одновременно высокоуровневые и низкоуровневые изменения, хотя бы постарайтесь ограничить их одним файлом или в худшем случае одной подсистемой, чтобы ограничить объём изменений насколько возможно. Иначе будет очень тяжело, когда придётся производить дебаггинг изменений, которые вы только что сделали.
Низкоуровневый рефакторинг
К этому моменту вы должны были хорошо понять, что делает каждый модуль, и вы готовы для настоящей работы: рефакторингу кода для улучшения эксплуатационной надёжности и подготовки кода к новой функциональности. Это бывает той частью работы, которая отнимает больше всего времени, пишите документацию на ходу, не изменяйте модуль, пока вы основательно не задокументировали его и не получили чувство полного понимания его функциональности. Не стесняйтесь переименовывать переменные и функции, также как структуры данных, для повышения ясности и последовательности, добавляйте тесты (также юнит-тесты, если ситуация предписывает это).
Исправление багов
Теперь вы готовы производить изменения, видимые реальным конечным пользователям. Первым порядком битвы станет длинный список багов, накопленных за годы в очереди билетов. Как обычно, сначала убедитесь, что проблема по-прежнему существует, напишите тест с этой целью, а затем исправьте баг, ваша CI и написанные end-to-end тесты должны уберечь вас от ошибок, которые вы можете сделать из-за недостаточного понимания или какого-то постороннего вопроса.
Обновление базы данных
Если всё необходимое сделано и у вас снова цельная и поддерживаемая кодовая база, то появляется вариант изменить схему базы данных или вообще заменить базу данных другой моделью, если вы планировали сделать это. Всё что вы сделали к этому моменту поможет вам произвести это изменение ответственным образом без каких-либо сюрпризов, вы можете досконально протестировать новую БД с новым кодом, и все тесты есть в наличии и гарантируют, что ваша миграция пройдёт без сучка без задоринки.
Выполнение по дорожной карте
Поздравляем, вы вне опасности и готовы к внедрению новой функциональности.
Никогда даже не думайте о масштабной переделке
Масштабная переделка — такой тип проекта, который почти гарантированно провалится. Во-первых, вы начинаете на абсолютно неизведанной территории, так что неизвестно с чего начинать, во-вторых, вы отодвигаете все проблемы на самый последний день — день прямо перед тем, как вы выпустите в жизнь свою новую систему. И вот тогда вы ужасно провалитесь. Предположения бизнес-логики окажутся ошибочными, неожиданно вы поймёте, почему та старая система делала определённые вещи так, как как она делала, и в целом вы придёте к заключению, что парни, собравшие старую систему, может быть, не такие идиоты, в конце концов. Если вы действительно хотите развалить компанию (и свою собственную репутацию в придачу) во всех смыслах, начинайте масштабную переделку, но если вы умны, то такая возможность даже не рассматривается.
Так что, как альтернатива, работайте пошагово
Чтобы распутать один из этих клубков наиболее быстрым образом, нужно взять любой элемент из кода, который вы понимаете (это может быть периферийный бит, но может быть и ключевой модуль), и попытаться пошагово улучшить его, оставаясь в старом контексте. Если старые инструменты сборки больше недоступны, вам придётся использовать некоторые хитрости (см. ниже), но по крайней мере попытайтесь оставить в живых как можно больше кода, который доказанно работает, начиная свои изменения. Таким образом, по мере улучшения кодовой базы будет расти и ваше понимание, что она на самом деле делает. Типичный коммит должен состоять из пары строчек.
Релиз!
Каждое сделанное изменение выпускается в продакшн, даже если изменение не видимо для конечного пользователя, важно делать наименьшие возможные шаги, поскольку у вас не достаёт понимания системы, есть большая вероятность, что только в рабочем окружении вы осознаете существование проблемы. Если эта проблема возникла сразу после вашего маленького изменения, вы получаете несколько преимуществ:
- вероятно, будет тривиальным выяснить, что пошло не так
- вы будете в отличной позиции для улучшения процесса
- и вы немедленно обновите документацию, чтобы отобразить только что открытые факты
Используйте прокси для своей выгоды
Если вы занимаетесь веб-разработкой, восславьте богов и поставьте прокси между конечными пользователями и старой системой. Теперь у вас есть поурловый контроль, какие запросы идут в старую систему, а какие вы перенаправите в новую систему, что позволяет намного проще и более точно контролировать, что запускать и кто увидит это. Если у вас достаточно умный прокси, вы могли бы использовать его, вероятно, для отправки части трафика в новую систему для индивидуальных URL, пока вы не будете удовлетворены тем, что всё работает как положено. Если ваши интеграционные тесты подключаются к этому интерфейсу, то так даже лучше.
Да, но всё это займёт слишком много времени!
Ну, как посмотреть. Действительно, если следовать этим шагам, то нужна некоторая повторная работа. Но это действительно работает, и любая оптимизация этого процесса предполагает, что вы знаете о системе больше, чем вы вероятно знаете на самом деле. У меня есть репутация и я действительно не хочу негативных сюрпризов во время работы вроде этой. Если не повезёт, это может привести компанию к провалу или может возникнуть реальная угроза внести беспорядок в работу клиентов. В такой ситуации я предпочитаю полный контроль и железобетонный процесс, а не попытку сэкономить парочку дней или недель, что подвергает угрозе успешный исход. Если вы более ковбойского характера — и ваш начальник согласен — тогда может быть приемлемым взять больше рисков, но большинство компаний выберет слегка более медленную, но намного более верную дорогу к победе.
Автор: m1rko