Методы борьбы с legacy-кодом на примере GitLab

Можно бесконечно холиварить о том, является ли GitLab хорошим продуктом. Лучше посмотреть на цифры: по итогам раунда инвестирования оценка GitLab составила 2,7 млрд долларов, в то время как предыдущая оценка была $1,1 млрд. Это означает бурный рост и то, что компания будет нанимать все больше и больше фронтенд-разработчиков.

Так выглядит история появления фронтенда в GitLab.

cyuquhlxnzithh35ujtiswr5upy.png

Это график количества фронтендеров в GitLab, начиная с 2016 года, когда их не было вообще, и заканчивая 2019-м, когда их стало уже несколько десятков. Но сам GitLab существует 7 лет. Значит, до 2017 года основной код на фронтенде писали бэкенд-разработчики, хуже того, бэкенд-разработчики на Ruby on Rails (ни в коем случае никого не хотим обидеть и ниже поясним, о чем идет речь).

За 7 лет любой проект, хотите вы того или нет, устаревает. В какой-то момент рефакторинг становится невозможно больше откладывать. И начинается полный приключений путь, конечный пункт которого никогда не достигнуть. О том, как это происходит в GitLab, рассказал Илья Климов.


О спикере: Илья Климов (xanf) senior frontend инженер в GitLab. До этого работал в стартапе и аутсорсе, руководил небольшой аутсорсинговвой компанией. Потом понял, что еще не успел позаниматься продуктом, и пришел в GitLab.

Статья основана на докладе Ильи на FrontendConf, поэтому не столько структурирует информацию, сколько отражает опыт и впечатления спикера. Может показаться излишне разговорной, но от того не менее интересной с точки зрения работы с legacy.

В GitLab как и в многих других проектах постепенно мигрируют со старых технологий на что-то более актуальное:

  • CoffeeScript на JavaScript. Разработчики на Ruby on Rails, когда стартовали проект, не могли не обратить внимание на CoffeeScript, который выглядит очень похоже на Ruby.
  • JQuery на Vue. Пришло время осознать, что невозможно поддерживать большую систему исключительно на JQuery. При этом GitLab не SPA. Используется server-side rendering и progressive enhancement, то есть отдельные элементы запускаются в виде Vue-приложений. По сути, реализованы микросервисы на фронтенде: на странице одновременно несколько Vue-приложений, которые общаются между собой.
  • Karma на Jest. Jest стал де-факто стандартом в мире тестовых фреймворков. Karma работает местами очень странно и, главное, долго.
  • REST на GraphQL или, в контексте фронтенда, Vuex на Apollo. Мы переписываем внутреннее хранилище с Vuex, который является аналогом Redux в мире Vue, на Apollo local state для того, чтобы не было двух хранилищ. То есть используем GraphQL и как локальное хранилище, и как инструмент обращения к серверу.


Одновременно происходят замены сразу в нескольких направлениях, в проекте одновременно существует legacy-код на разных стадиях.

Теперь представьте себе, что вы приходите в проект, который находится посередине всех этих миграций. Эталоном и точкой отсчета для меня стала ситуация, когда открываешь редактирование проекта, нажимаешь кнопку сохранить — и как вы думаете, что приходит? Если вы подумали, что мы такие олдфаги, что приходит HTML, то нет. Приходит JavaScript, который надо «эволнуть», чтобы отобразилось всплывающее окошко. Это нижняя точка в моей картине legacy.

Дальше по нарастанию: самописные классы на JQuery, Vue-компоненты и, как высшая точка, новые современные фичи, написанные с Vuex, Apollo, Jest и т.д.

Вот так выглядит мой contribution-graph на GitLab.

ewmliousc_qzjvrr0pfshtqlrj0.png

В нем — и это очень важно для понимания сути рассказа и всех моих болей — можно выделить несколько отрезков:

  • Онбординг в районе апреля. «Медовый месяц», когда я только начал работать в GitLab. В это время новичкам дают задачи попроще.
  • С конца апреля до середины мая всего несколько коммитов — период отрицания: «Нет, не может быть, что все так сделано!». Я пытался понять, где я чего-то не понимаю, поэтому коммитов так мало.
  • Вторая половина мая — гнев: «Плевать на все — мне надо двигать продакшен, деливерить фичи, попытаюсь что-то с этим делать».
  • Начало июня (ноль коммитов) — депрессия. Это не был отпуск, я смотрел и понимал, что у меня опускаются руки, я не знаю, что с этим делать.
  • После этого я договорился с собой, решил, что меня ведь наняли как профессионала, и я могу сделать GitLab лучше. В июне и июле я предлагал огромное количество улучшений. Не все из них находили отклик по причинам, о которых мы еще поговорим.
  • Сейчас я на стадии принятия и четко понимаю: как, куда, зачем и что со всем этим делать.


Расскажу подробнее, что я делал с августа по октябрь. Честно говоря, в небольшой аутсорсинговой компании или в стартапе меня бы за эти три месяца раз пять уволили с такой продуктивностью.

Итак, за три месяца я сделал:

  • Segmented control — три кнопочки.
  • Строку поиска, которая хранит локальную историю — чуть-чуть более сложный компонент.
  • Спиннер. И этот компонент еще не замержен.


p6fwuwfxi5dxzpyuewu9vn7_zc4.png

Дальше шаг за шагом разберем, почему так произошло и как с этим жить. Если вам кажется, что я преувеличиваю, вот скриншот некоторых задач, которые висят на мне на GitLab (можете посмотреть непосредственно в GitLab, он открыт).

emaxo9j3iwkxpdefee2ghsokjpi.png

Видите: missed 12.1, missed 12.2, missed 12.3. Спринт у нас длится месяц, и segmented control — 3 спринта. Спиннера все еще нет, он-то и будет нашим главным героем.

Проблема рефакторинга и философии рефакторинга стоит перед человечеством очень давно — тысячелетиями. Сейчас докажу:

«И никто не вливает молодого вина в мехи ветхие;, а иначе молодое вино прорвет мехи, и само вытечет, и мехи пропадут;, но молодое вино должно вливать в мехи новые; тогда сбережется и то, и другое.

И никто, пив старое вино, не захочет тотчас молодого, ибо говорит: старое лучше».

Новый Завет

Библия говорит нам о том, как совмещать старую и новую функциональность. Вторая часть цитаты ценна с точки зрения управления: сколько бы вы не выходили с инициативами, будете встречать огромное сопротивление.

В фазе депрессии я смотрел большое количество докладов о рефакторинге больших проектов, но приблизительно 70% из них напоминали мне анекдот.

Разговор джавистов:
 — Как нам ускорить наше Java-приложение?
 — О, так у меня был доклад про это! Хочешь, расскажу?
 — Рассказать и я могу, мне бы ускорить!

Если вы все-таки решите встать на опасный и шаткий путь рефакторинга, у меня есть несколько простых рецептов, которые я вывел для себя и которые работают в условиях, приближенных к реальности.

1. Изоляция


Чтобы что-то ускорять, улучшать, рефакторить, нужно разрезать слона на бифштексы, то есть разделить задачу на части. GitLab очень большой, у нас есть Slack-канал «Is this known», где люди задают вопросы типа «Это баг или фича, кто может объяснить?» — и не всегда находится ответ.

Простой пример: скриншоты одного и того же места в GitLab, сделанные с разницей в один день.

_ie7t3qfxsof8p9kwrdzy2wsymi.png

Мне было очень обидно, потому что я занимался этой кнопочкой, а это всё так или иначе проблемы с кнопкой.

Что же случилось? Все просто: мы разрабатываем дизайн-систему, и в рамках отдельного инструмента story book для тестирования дизайн-системы отключили глобальный CSS GitLab, чтобы проверить, насколько CSS компоненты изолированы от глобального CSS.

Резюмируя: CSS уже не спасти, по крайней мере в GitLab.

Я 14 лет работаю с JavaScript и еще ни разу не видел, чтобы проект длиной хотя бы в год-два сохранял бы полностью управляемый CSS. Кстати, HTML тоже не спасти (в GitLab точно).

GitLab разрабатывался давно и бэкендерами. Они приняли спорное решение использовать Bootstrap, потому что Bootstrap предлагал понятную бэкендерам систему для верстки.

Но что такое Bootstrap с точки зрения философии компонентной изоляции? Это порядка 600–700 глобальных классов (по сути каждый класс CSS является глобальным), которые пронизывают всё приложение. С точки зрения управляемости, ничего хорошего из этого не выйдет.

Следующее действие (не будем называть его ошибкой) — GitLab взял Vue.js. Выбор был разумным, потому что из трех фреймворков именно Vue позволяет наиболее плавно переписывать что-то. Не нужно все сразу выкидывать и пилить большой Single Page Application, а можно переписывать отдельные мелкие узлы. Сейчас это можно сделать и на Angular, но 3–4 года назад, когда появился Angular 2, он не мог сосуществовать на странице больше, чем в одном экземпляре. На React сейчас тоже можно, но вся эта магия с отсутствием build step и прочее склонили чашу весов в сторону Vue.

В итоге одно зло совместили со вторым. Это плохо, потому что стили Bootstrap ничего не знают про компонентную систему, а компоненты Vue писались на первых порах как попало. Поэтому было принято волевое решение делать свою дизайн-систему. У нас она называется Pajamas, но никто не смог мне объяснить почему.

Я вижу, что сейчас собственных дизайн-систем становится все больше, и это приятно.

Дизайн-система предполагает изоляцию, но поскольку GitLab уже был написан на Bootstrap, приблизительно 50–60% нашей дизайн-системы является оберткой над Bootstrap/Vue-компонентами с уменьшением их функциональности. Это нужно для того, чтобы дизайн-система не давала вам использовать компонент неправильно. Если говорить об абстрактной библиотеке, то там важна гибкость, чтобы, например, возможность сделать какую угодно кнопку. Если в GitLab спиннеры могут быть четырех утвержденных дизайнерами размеров, то нужно физически не давать делать другие.

Когда-нибудь добро победит, и у нас будет важный инструмент, с помощью которого, если, конечно, вы забили на поддержку IE и Edge, можно эффективно рефакторить фронтенд-проекты — это Shadow DOM. Shadow DOM решает проблему протекания глобальных стилей в компоненты. Пожалуйста, не берите Polymer, который даже Google уже закопал. Используйте lit-element и lit-HTML, и сможете строить изолированные стили, используя свой любимый фреймворк.

Вы можете сказать, что в React есть CSS-модули, во Vue есть scoped styles, которые делают то же самое. Будьте очень аккуратными с ними: CSS-модули не обеспечивают 100% изоляции, потому что работают только с классами. А со scoped styles во Vue может реализоваться очень интересный сценарий, когда стили верхнего компонента попадают в корневой элемент родительского, и там используются data-атрибуты, которые тормозят.

Несмотря на то, что я три года ругал Angular, теперь вынужден признать, что на данный момент в нём это реализовано лучше всего. В Angular, чтобы обеспечить хорошую изоляцию стилей, достаточно просто переключить режим работы изоляции и, если надо, использовать Shadow DOM, иначе обычную эмуляцию.

Вернемся к спиннеру. Из трех месяцев, которые я с ним воевал, некоторое время я занимался увлекательным делом: чисткой.

s8zvu0h5yauznhgfqthij_96rkc.png

Класс loading-container является implementation detail спиннера, то есть это класс внутри реализации спиннера. Мы решили, поскольку CSS не спасти, в Pajamas сделать отдельный CSS, основанный на Atomic CSS. Мне лично не совсем нравится концепция Atomic CSS, но имеем, что имеем.

То есть я занимался тем, что вычищал в коде основного продукта стили, которые были навешаны на элементы, которые являются деталями реализации. Выглядит это все очень просто — ведь, конечно же, в GitLab есть тесты.

2. Тесты


Тесты в GitLab покрывают весь код, обеспечивают надежность. И поэтому pipeline у нас проходится за 98 минут.

fluzbboaa7on25ueq9z6pkvcdb4.png

40% времени публичных раннеров на GitLab.com GitLab собирает сам GitLab, потому что pipeline проходит на каждый merge request.

Я был очень вдохновлен: наконец-то я попал на проект, где все покрыто тестами! Покрытие бэкенд-кода близко к 100%, а фронтенд-код на момент моего прихода был покрыт на 89,3%.

К сожалению, оказалось, что большая часть этого покрытия мусорное, потому что:

  • ломается, когда вносятся изменения, не связанные с компонентами;
  • не ломается, когда изменения вносятся.


Поясню на примерах. Мы взяли Jest, потому что подумали, что он позволит нам в определенных ситуациях не писать assertions, а использовать снэпшоты. Проблема в том, что если вы не донастроили Jest и не добавили правильный сериалайзер, то Vue Test Utils просто выводит props в HTML. Тогда, например, получается props с именем user, у которого в параметрах был props с именем data, в который был передан object object. Любые изменения формата передаваемых данных не приводят к провалу снэпшота.

Разработчики на Ruby привыкли делать тесты, грубо говоря, покрывающие все методы.

Когда мы делаем тесты на компоненты Vue или React, мы должны тестировать, как ведет себя публичный API.

Таким образом, у нас были огромные тесты на вычисляемые свойства, которые в некоторых сценариях не использовались, а в других наоборот было физически невозможно дойти до состояния, когда этот computed будет вызван. Отдельное спасибо Vue, в котором шаблоны являются строками, поэтому вычислить test coverage шаблона нельзя. В Vue 3 появятся Source Maps и возможность это исправить, но это будет нескоро.

К счастью, есть один простой навык, который позволит вам эффективно рефакторить legacy. Это умение писать то, что в мире большого тестирования называют pinning test.

Pinning test


Это тест, который пытается зафиксировать поведение, которое вы рефакторите. Обратите внимание, что pinning test, скорее всего, в итоге не будет закоммичен в репозиторий. То есть вы путем всевозможных изощрений, например, использования staging environment, пишете для себя тест, который описывает, как рендерится ваш компонент. После рефакторинга pinning test должен либо генерировать тот же HTML, и это, скорее всего, хороший признак, либо вы должны понимать, какие изменения произошли.

Приведу пример из жизни. Несколько месяцев назад я проводил ревью merge request с рефакторингом выпадающего списка. Контекст legacy такой: раньше, чтобы в выпадающем списке отделить ветки друга от друга черточкой, просто передавалась текстовая строка «divider». Поэтому, если ваша ветка называлась divider, то вам не повезло. В процессе рефакторинга человек поменял в HTML-узле два класса местами, это ушло в продакшен и развалило его. Справедливости ради, конечно, не совсем продакшен, а в staging, но тем не менее.

В итоге, когда мы начали писать такие тесты, обнаружили, что, несмотря на классный показатель тестового покрытия, тесты написаны неправильно. Потому что, во-первых, у нас были тесты на Karma, то есть старые. Во-вторых, почти все тесты делали предположения о внутренностях компонента. То есть притворялись unit-тестами, а работали по сути как end-to-end, проверяя, что рендерится конкретный тег с конкретным классом, вместо проверки того, что рендерится конкретный компонент с конкретными props. Понимаете разницу: классы — компоненты?

В итоге мои 18 merge requests с рефакторингом тестов суммарно на 8–9 тысяч строк, общий changelog получился порядка 20 тысяч, потому что 11 тысяч было выпилено.

ayq_qsjbluc3lyjaiwft9oxkefo.png

При этом формально все эти тесты я переделывал ради одного: чтобы убрать assertions относительно классов спиннера, и вместо этого проверять, что там рендерится спиннер с правильными props.

На первый взгляд, это неблагодарная работа. Но переписывание тестов на правильную архитектуру было довольно легко продать бизнесу. GitLab — коммерчески прибыльный продукт. Конечно, если вы скажете продакт-менеджеру, что вам нужно три итерации на переписывание 20 тестов, угадайте, куда вас отправят. Другое дело: «Мне нужно три итерации, чтобы переписать тест. Это позволит нам более эффективно внедрить спиннеры и ускорит будущее внедрение новых элементов дизайн-системы». И тут мы подходим к важному.

3. Сопротивление


Есть еще одна функциональность, которую в дизайн-системе GitLab ждут больше моих спиннеров — это обычные иконки SVG.

yutoccm3twkiswgiyqe2flvse_w.png


У нас есть иконки, отрисованные дизайнером, которые используются в основном проекте, но их нет в дизайн-системе, потому что у GitLab тяжелое детство. Например, в 2019 CSS собирается не через Webpack, а штукой под названием Sprockets — это pipeline Ruby, потому что нам надо переиспользовать один и тот же CSS на бэкенде и на фронтенде. Из-за этого иконки должны подключаться в разные проекты по-разному. Поэтому кто-то три месяца рефакторил основную кодовую базу, чтобы можно было подключить иконки из дизайн-системы в смежные проекты.

Здесь есть важный момент, с которым вы неизбежно столкнетесь. Рефакторинг — это процесс непрерывного улучшения. Но рано или поздно вам придется остановиться.

Абсолютно нормально остановиться, не доведя рефакторинг до конца, но получив конкретные измеримые улучшения.

Но если вы работаете на legacy проекте, вы неизбежно столкнетесь с людьми, которые делают так.
tobxixkebgoqq2lvl-2fijjeqme.png
Это означает, что они пишут по-старому, потому что так привыкли. Например, наши бэкендеры говорят: «Не хочу этот ваш Jest учить. Я три года писал тесты на Karma, мне надо запилить новую функциональность, а поскольку без тестов функциональность не возьмут — вот тест на Karma».

Ваша задача максимально этому сопротивляться. С этим относительно легко бороться, но есть еще больший грех, чем это. Иногда в процессе рефакторинга вы натыкаетесь на какую-то проблему, и возникает желание вообще уйти в сторону.
pzvast70b3gpomnqsaic1iignns.png
То есть подставить новый костыль просто потому, что по определенным причинам не получается довести рефакторинг до конца. К примеру, если у нас есть проблемы с интеграцией иконок в основную кодовую базу, можно оставить служебный класс, который будет подтягиваться из глобального Application CSS. Формально бизнес-задача будет решена, но на практике, как в истории про лернейскую гидру: было 8 багов, 4 пофиксили, 13 осталось.

Рефакторинг, как ремонт в доме — его невозможно закончить, можно только прекратить.

Первые 80% рефакторинга отнимают 20% времени, остальные 80% рефакторинга (именно так) отнимают еще 80% времени.

Важно не вводить новые хаки в процессе рефакторинга. Поверьте, в процессе разработки они и так сами появятся.

4. Инструменты


К счастью, еще до моего прихода GitLab встал на праведный путь внедрения хороших инструментов: Prettier, Vue Test Utils, Jest. Хотя Prettier внедрили криво.

Объясню, о чем идет речь. Пока я разбирался, что и почему так исторически сложилось, 80% моих поисков натыкались на коммит в 37 тысяч строк prettify-кода. Пользоваться историей при этом было практически невозможно, и мне пришлось настроить плагин для VS Code так, чтобы он при поиске истории изменений исключал этот коммит.

Несомненно, инструменты важны, но выбирать их надо аккуратно. К примеру, у нас Vue, а у Vue есть хороший инструмент для тестирования — Vue Test Utils. Но если Vue 2 вышел 2–3 года назад, то Vue Test Utils до сих пор не вышли из беты. Более того по инсайдерской информации, на данный момент единственный разработчик Vue Test Utils не пишет на Vue.

В процессе выбора инструментов вы играете в орлянку с судьбой, и очень постарайтесь выиграть.

У GitLab была детская травма с CoffeeScript. Именно поэтому невозможно продавить в GitLab даже теоретическую идею написания на TypeScript. Всё разбивается об один простой аргумент:, а не будет ли также как с CoffeeScript, когда язык, который компилируется в JavaScript, умер.

При выборе инструментов старайтесь, чтобы инструмент можно было заменить, либо, в крайнем случае, поддерживать самостоятельно.

Мы в GitLab используем крутую штуку под названием Danger.
lvolohfoqnrzwiqzur-ziqhaaxw.png
Это реальный скриншот их сайта в 2019 году. Но, коллеги сказали, что на самом деле в 2019 году сайт может выглядеть как угодно.

Danger — это бот, который занимает промежуточное состояние между линтером в вашем CI и написанными гайдлайнами. Этот бот можно расширять и он будет приходить в pull request или, как они правильно называются у нас, merge request и оставлять комментарии типа:

  • «В этом файле есть комментарий ESlint disable, исправь».
  • «Этот файл раньше правил этот человек. Возможно, тебе надо поставить ревью на него».


На мой взгляд, это очень хороший, важный и расширяемый фреймворк для мониторинга состояния кодовой базы.

5. Абстракции


Начну с примера. Несколько месяцев назад я увидел новость: «GitHab избавился от jQuery. Мы проделали большой тяжелый путь и теперь не используем jQuery». Естественно, я подумал, что нам в GitLab тоже нужно избавиться от jQuery.

e33d4zkjkbo05u2iz2ehkjp9oh8.png

Быстрый поиск показал: jQuery у нас используется в 300 файлах. Выглядит страшненько, но ничего — глаза боятся, руки делают. Но нет! jQuery является неотъемлемым клеем в кодовой базе GitLab, потому что у нас Bootstrap.

Bootstrap изначально написан на jQuery. Это означает, что если вам нужно, например, перехватить событие открытия dropdown в Bootstrap — это jQuery-событие. Вы не можете перехватить его в нативном виде.

Это первое, что вы должны абстрагировать при работе с legacy-кодом. Если у вас есть jQuery, который вы не можете выкинуть, напишите собственный Event Emitter, который спрячет внутри работу с jQuery-событиями.

Когда наступит светлое будущее, мы сможем убрать jQuery, а пока, простите, говнокод надо концентрировать. В обычном legacy-проекте он равномерно размазан по всему коду. Собирайте его в узкие места, помеченные флажками «без костюма химзащиты не входить».

6. Метрики


Нельзя делать то, результат чего нельзя измерить. Мы в GitLab измеряем все, что делаем, чтобы объективно знать, что код стал работать лучше.

me0gqbigji1jssyvv6be_ve_bjq.png

Например, у нас есть график миграции с Karma-тестов (синий) на Jest (зеленый):

Видите, что есть постепенный прогресс. И таких графиков у нас очень много. Но важно понимать, что не всегда все заканчивается хорошо.

Приведу еще один пример (демка в докладе начинается с этого момента).

6sazt7jddhpg2kshoo0sodo-hbu.png

Перед вами обычный интерфейс merge request в продакшене GitLab. Очевидно, у нас можно сворачивать файлы, клик по заголовку и файл начнет сворачиваться.

Как вы думаете, сколько займет сворачивание файла в 50 строчек, при том что машина с Core i7 восьмого поколения выкручена на максимальную производительность? Сколько займет разворачивание?

Время, за которое свернется файл, колеблется в интервале от 7 до 15 секунд. Разворачивание происходит мгновенно. А до рефакторинга и то, и другое работало одинаково быстро.

Именно поэтому очень важно иметь метрики.

Расскажу, что здесь происходит. Это Vue, его система реактивности отслеживает значение: если оно поменялось, вызываются все зависимости, которые зависят от этого значения. Каждая строка — Vue-компонент, состоящий из многих вещей, потому что вы можете ставить к строке комментарии, комментарии могут динамически подгружаться с сервера и т.д. Все это подписано на Vue-store, который тоже является Vue-компонентом.

Когда вы закрываете merge request, все, скажем, 20 тысяч подписок store надо обновить, когда store обновится. Если строка удалена, ее надо удалить из зависимостей. А дальше простая математика: нужно просмотреть массив из 20 тысяч элементов, чтобы найти те, которые надо удалить. Допустим, таких строк 500, и каждая строка — это несколько компонентов. В итоге получается математическая операция O (N), то есть O (20 000)*500. Все это время работает JavaScript.

Разворачивание же происходит мгновенно, потому что добавление зависимости — это просто пуш в массив, т.е. математическая операция O (1).

Иногда улучшение качества кода приводит к деградации производительности и других метрик. Очень важно их измерять.

Резюмируя: изолируйте плохой код и следите за метриками.

Работа с legacy — тема неисчерпаемая. С одной стороны, есть общие подходы к рефакторингу — о них поговорим на TechLead Conf — конференции, посвященной собственно инженерным практикам. С другой — есть своя языковая специфика, поэтому legacy не останется без внимания и на конференциях по Python, и по PHP.

А если вас гораздо больше интересуют хайповые технологии и новинки из мира фронтенда, то для вас в календаре Онтико фестиваль РИТ++, который круто меняет формат и уходит в онлайн, и FrontendConf. И на оба этих мероприятия еще можно заявить собственный доклад.

© Habrahabr.ru