НОВОСТИ Методы борьбы с legacy-кодом на примере GitLab

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

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

cyuquhlxnzithh35ujtiswr5upy.png


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

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


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

Статья основана на докладе Ильи на , поэтому не столько структурирует информацию, сколько отражает опыт и впечатления спикера. Может показаться излишне разговорной, но от того не менее интересной с точки зрения работы с 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 — тема неисчерпаемая. С одной стороны, есть общие подходы к рефакторингу – о них поговорим на — конференции, посвященной собственно инженерным практикам. С другой – есть своя языковая специфика, поэтому legacy не останется без внимания и на конференциях , и .

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