Alek�ei Matiu�hkin

сделано с умом



История создания одной библиотеки

Friday, 5 Jan 2024 Tags: 2024tech

Если бы я не ненавидел слова «евангелист» и «амбассадор»… да нет, даже тогда я бы отрекомендовывался, как ярый сторонник вынесения любого отчуждаемого подмножества функций в библиотеки.

“I’m a yoga ambassador.”
“Ambassador is about states, not about stretching.”
— Isn’t It Romantic?

Библиотеки

У библиотеки есть сразу три преимущества, в сравнении с просто куском кода в проекте:

  • библиотека не протечет абстракциями в остальной код;
  • библиотеку гораздо легче протестировать до блеска;
  • библиотеку можно открыть в OSS и получить полезную обратную связь от сообщества.

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

Абстракции

Библиотека — это то, что программистами воспринимается, как идеальный черный ящик, по умолчанию правильный. Не могу найти причин для объяснения этого, но это так. Подавляющее большинство пользователей не будет даже пытаться сделать что-то в обход экспортируемого API, а те, которые будут — пришлют pull request (в крайнем случае — создадут issue) с описанием проблемы. Вот есть API, который никогда не изменится (если автор библиотеки — адекватен, то он не поломается даже между мажорными выпусками). Даже если это неудобно, пользователи будут звать задокументированные функции, а не пытаться вытащить внутренний идентификатор процесса грязными хаками.

Кусок кода в проекте почему-то воспринимается всеми, как огромный плакат «добро пожаловать со своей косой в наш огород». Много раз за свою карьеру я сталкивался с претензией «твой модуль не работает, как положено», подкрепленной ссылкой на код, который переливается всеми цветами радуги под натиском команды git blame. Пример буквально из вчера: я не уследил (я получаю примерно 500 писем в день только от гитхаба, веду не меньше десяти проектов на четырех языках и чисто технически не могу за всем уследить), и в мой любовно выстроенный конечный автомат кто-то добавил возможность откатиться в начальное состояние простым изменением поля в базе. Ладно.

В библиотеку люди не снимая калош не полезут. В худшем (и лучшем по совместительству) случае — меня попросят добавить такой откат, и я его реализую, не поломав все гарантии FSM.

Тестирование

Мы убиваем сразу двух зайцев, вынося изолированный код в библиотеку. Во-первых, при написании тестов самой библиотеки, мы сразу поймем, буквально — на личном опыте, — что надо улучшить в нашем API. Если вам не очевидно, как написать тест для экспортируемой в API функции — с вероятностью примерно 102% эта функция не должна вообще быть доступна извне. Если вы сами не понимаете, как ей предсказуемо пользоваться — никто не поймет. А те, кто поймут — станут вашей головной болью до тех пор, пока вы эту функцию не объявите устаревшей и вредной.

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

OSS

Я открываю в OSS буквально весь код, в котором не содержится наша внутренняя бизнес-логика. Тут поля недостаточно широки, чтобы описать весь букет преимуществ этого; упомяну два основных. Посторонние люди мгновенно найдут за вас баги, о существовании которых вы и помыслить не могли, потому что они появляются только на польской версии виндоуз (кто узнал аллюзию — тот молодец, зачем вы меня-то читаете тогда?). Посторонние люди обязательно найдут самые обидные слова для указания вам на вашу несостоятельность в качестве девелопера — и это тоже несомненный плюс. Розовые гладиолусы плохо приживаются в тех коллективах, в которых имеет смысл работать.

Enfiladex

Настало время перейти к частностям. Приложения, которыми я в основном занимаюсь, — кластерные. Высокая нагрузка, многонодная среда, ECS этот мерзкий, который контейнеры триста раз в день перезапускает. Нам нужны распределенные юнит-тесты, и до недавнего времени мы использовали мою же библиотеку, написанную несколько лет назад, буквально на коленке, для разворачивания кластера в тестах в GHA.

Пару недель назад я обновил среду разработки и увидел много неприятных сообщений (deprecation warnings) по поводу того, что мой подход к разворачиванию кластера безнадежно устарел. Отлично, подумал я, вселенная подталкивает меня к тому, чтобы, наконец, сделать все правильно.

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

Оказывается, эрланг приходит с фреймворком common_test. Более того, Фред уже давно о нем написал так, как умеет только он — тысяча знаков, и все — по полочкам. Я, естественно, захотел принести ct в мир эликсира.

Итак, вырисовалась постановка задачи: библиотека, позволяющая тестировать код в кластере и интегрированная, насколько возможно, с существующим решением от отцов-основателей. Как бы вы подошли к решению этой проблемы?

Я начал с того, что сравнил тестовый фреймворк в эликсире (ExUnit) с ct — структурно. И предчувствие меня не обмануло: сколько раз колесо не изобретай, оно получится круглым. Инициализация групп тестов, опциональное выполнение кода по завершении, контексты… Все оказалось примерно одинаковым. Поэтому я просто поподробнее почитал код ExUnit и убедился повторно: для интеграции с ct буквально все готово.

Я обернул макросы типа setup/1 и setup_all/2 — своими, нагенерировал оттуда дополнительный код, который нужен common test, проверил пограничные случаи… Что ж, теперь вся наша тестовая база подверглась в некоторых местах следующей модификации:

defmodule Foo.Bar.Test do

  use ExUnit.Case

  use Enfiladex.Suite # ⇐ добавление этой строки превращает
                      #   весь файл в common test suite

  test "foo bar baz" do
  end

  ...

И все. Enfiladex.

Мораль

Я, разумеется, написал весь этот текст с единственной целью — похвастаться. Но для галочки скажу важную вещь: при проектировании любого программного обеспечения, необходимо думать о пользователях (можете цитировать). К сожалению, когда мы пишем код для клиентов, хер их там разберет, чего они хотят. Создаются целые отделы дармоедов, типа продактов, проджектов и прочих менеджеров. Но когда мы пишем код типа того, про который я говорю, для таких же программистов, как мы сами, — мы можем дать себе труд понять, чего они хотят. Они хотят минимум телодвижений при максимальной отдаче.

Поэтому я не стал писать свою элегантную обертку вокруг common_test, а обучил существующий ExUnit его понимать. Хотя, признаюсь, после подробного ознакомления с некоторыми архитектурными решениями в оном, позывы переписать все к херам с нуля правильно было крайне трудно сдержать.

Если вы хотите, чтобы вашей библиотекой пользовались — сделайте ее удобной. Прикладные библиотеки ищут, когда проект уже разросся. Поэтому любая библиотека должна удовлетворять двум основным критериям: понятна точка входа и не требуется переписать половину проекта для ее использования. Ну, помимо того, что она должны решать понятную всем задачу и …ну, это, как его… работать.

«Drop-in and use», или «plug and play», как сказал бы седобородый виндовоз.


  ¦