В МоемСкладе мы уже больше года создаем функционал, который помогает нашим пользователям покупать и продавать маркированные товары. Новость о маркировке уже много раз проскакивала на Хабре, так что кратко: с 2019 года товары маркируются обязательно. Не все и сразу, но уже сейчас нужно маркировать сигареты, обувь, духи и автомобильные шины. При этом мы работаем в ситуации неопределенности, когда API систем госорганов постоянно меняется.
Поэтому у нас есть всего два способа выполнить интеграцию с госорганами: ждать пока всё устаканится и упустить первенство на рынке или разработать систему в мире меняющихся требований.
Мы выбрали второе — как раз в духе гибких методологий. Думаю, Agile действительно может помочь в решении прикладных задач. И жизнь в мире постоянно меняющихся требований — это то поле, на котором можно развернуться.
Меня зовут Максим Сухаренко, я тимлид команды Платформа сервиса МойСклад. И сегодня я расскажу вам, как мы работаем с меняющимися требованиями.
От классической системы до Scrum
Первоначальный срок разработки системы со стороны регулирующих органов планировался на апрель 2019 года, но вы всё понимаете. В итоге сроки сдвинули на октябрь. А где новые сроки, там и новые требования, и новые форматы. Мы не могли ждать, когда API будет зафиксировано — потом не успели бы реализовать интеграцию. Поэтому решили «колебаться с линией партии».
Подумали о классическом подходе, который предлагает нам сделать адаптер для внешней системы. Дополнить его заглушкой с переключателем и время от времени подтягивать функциональность заглушки и адаптера до актуального состояния внешней системы.
Логика подсказывает, чтобы разработать систему с постоянно меняющимся интерфейсом, надо зафиксировать требования на какой-то момент времени. Но только на какой? Пока не разработаем какую-то часть функционала?
Согласитесь, неприятно оставлять недоделанным функционал, по которому не проходят тесты, еще и начинать ломать его по новой. Можно же фиксировать требования на период разработки фичи. Но что делать с большими фичами, которые могут жить в разработке несколько месяцев? Фиксировать требования на такой длительный срок просто невозможно.
Так мы обратились к Scrum. Он говорит нам, что к концу спринта мы должны предоставить работающий продукт с новой функциональностью. Но как это бьется с фиксацией требований и большими фичами? На эту тему есть хороший анекдот:
— Доктор, когда я вот-вот так вот делаю у меня болит.
— А вы вот-вот так вот не делайте.
Кто не понял, не делайте больших фич. Можно фиксировать требования на одну или две недели, чтобы совпасть со спринтом, а в цели спринта ставить реализацию функционала по требованиям. И функционал надо бить на такие куски, чтобы они укладывались в один или два спринта. Думаете, слишком хитро? А то! Вы просто тикеты нарезать не умеете. Пилите, Шура, пилите, они золотые.
Конечно, это не так просто, к этому надо целенаправленно двигаться — чтобы уйти в разработке от «полгода будем делать, потом выкатим новую версию на тест».
Секрет в том, чтобы иметь стабильную версию в конце каждого спринта. Конечно, может показаться, что так увеличится длительность разработки из-за роста накладных расходов на стабилизацию ветки и что разработка будет неоптимальной. Но тут ситуация почти такая же, как с тестами, поговорим об этом ниже.
К боевым задачам
А теперь к реальным ситуациям. У нас была задача настроить шифрование между нашей системой и API, чтобы пользователь подписывал все запросы ключом. Думаю, многие из вас сталкивались с КриптоПро, вот и нам пришлось.
Если использовать стандартный подход, сформируется одна изолированная задача — настройка криптографии для сервиса. Повесим ее на одного человека и целый месяц будем снимать статус и негодовать, почему она никак не закончится. А разработчик постепенно превратится в дикое существо из потустороннего мира, с пеной у рта и бешеными глазами.
Мы же распилили на небольшие задачи и раскидали по всей команде. Я сравниваю криптографию с алкоголем: в компании и умеренной дозе намного лучше, чем много и в одиночку.
Так как мы разрабатываем облачный сервис, нам было проще использовать КриптоПро ЭЦП Browser plug-in (это не реклама, это безысходность). Он позволяет средствами КриптоПро CSP протянуть ключи и методы шифрования до веб-страницы.
Сначала пользователь выбирает, каким ключом будет пользоваться в работе с сервисом, потом происходит аутентификация для получения токена. А уже потом с помощью шифрования и токена осуществляются вызовы API. Вроде всё просто (нет).
Первым делом мы сделали MVP в отрыве от нашего сервиса — для настройки взаимодействия плагина с API. Знаете, сколько есть документации по браузерному плагину КриптоПро от производителя? Нисколько! Только реверс-инжиниринг примеров, только хардкор. Бессонные ночи и попытки определить, на что влияют те или иные параметры.
И только потом мы смогли попытаться встроить его в нашу экосистему. Один человек приводит внешний вида компонента прототипа в человеческий вид, второй подключает его к бизнес-логике, третий пишет инструкцию для потомков по его настройке. Каждая задача имеет конкретную цель и относительно изолирована от других. А людям есть что обсудить и с кем поделиться болью.
Так ситуация стала достаточно стабильной. Небольшими итерациями мы добавляем новый функционал, время от времени актуализируем внешний интерфейс и распространяем изменения вглубь нашей системы. Но возникает вопрос: жить в отдельной ветке до последнего или пытаться постоянно мержить в мастер маленькими фичами?
Тестируем функционал
Предлагаю разобраться, как быть с тестированием и что делать, если мы добавляем интеграцию к живому проекту.
Начнем с тестирования. Для начала определимся, что мы можем называть протестированным функционалом. Предлагаю называть так функционал, который прошел приемочные тесты, в том числе и регрессионные. Только договоримся, что не будем прибегать к лайфхаку «нет тестов — значит, всё протестировано». Нам ведь нужно поддерживать в продуктиве наш код, а чем больше тестовое покрытие, тем лучше. Чем больше процент автоматизации таких тестов, тем дешевле каждая итерация тестирования функционала и тем чаще его можно проводить.
У нас есть некоторый набор приемочных и регрессионных тестов, часть из них автоматизирована, часть проводят руками.
Если подходить формально, мы должны проводить тестирование после каждого изменения кода. Например, разбили вашу фичу на шесть тикетов и в процессе тестирования нашли десять ошибок. А каждое тестирование пусть занимает четыре часа: автоматическое не в счет, а на ручное уйдет как раз четыре часа. Получается, при базовом и формальном способе работы мы потратим 64 часа.
Теперь попробуем проводить тестирование не после каждого изменения кода, а через одно. Логика подсказывает, что так мы потратим только 32 часа. А если проводить тестирование только после разработки функционала и после исправления всех дефектов. Но это при условии, что все дефекты изолированы друг от друга, чего в жизни не бывает.
В жизни же оказывается, что после разработки функционала проводится тестирование, и несется первая волна багов. Потом баги чинят, функционал ломают, тестируют по скоупу бага. Проводят второе глобальное тестирование, и появляется новая волна багов. Эти баги были скрыты и появились, когда исправляли первую волну и меняли функционал. И так несколько раз.
Обычно получается три-пять полных прогона тестов — в зависимости от запутанности функционала и прямоты рук. Но процесс можно ускорить еще сильнее — если бить на маленькие фичи.
Например, если разбить одну фичу с тестирование в четыре часа на две с тестированием в три с половиной и полтора часа, получится пять часов вместо четырех. Кажется, что профита нет. Но он проявляется в уменьшении сложности выпускаемого функционала и снижении вероятности возникновения цепочек связанных дефектов. В итоге итераций полного тестирования становится меньше.
Добавляем к проекту
Теперь разберем ситуацию, когда вы делаете фичу в проекте, на котором трудятся еще разработчики. Допустим, мы используем относительно стандартную концепцию ведения исходного кода Branch Per Feature (Branch Per Ticket).
Очевидно, что количество затрат на поддержание актуальности бранча пропорционально его времени жизни. Чем меньше пересечений кода в текущих тикетах, тем меньше проблем на их мерже. Это косвенно зависит от времени жизни: чем больше прошло времени, тем выше шансы, что появятся связанные тикеты. Соответственно, чем меньше живет бранч, тем меньше сил мы тратим на его актуальность.
Осталось понять, как выкатывать частично рабочий функционал на прод. Ответ достаточно простой: он не должен быть доступен пользователю. Если хочется спросить, а зачем так делать, можете вернуться на несколько абзацев выше.
Кажется, держать мертвый код в мастере не очень хорошо. Всё верно, но ведь он и не мертвый. Вы можете сделать тайную страницу с ручками, которые будут включать такой функционал. Или специальную последовательность действий, которая, как пасхалки в играх, будет выводить вас на этот функционал. К тому же вы сможете протестировать его сразу в деле.
Конечно, существуют и другие способы бороться с изменчивостью требований. И у данного подхода существуют свои ограничения и условия применений. Как известно, серебряной пули всё еще нет.
Комментарии (6)
vav180480_2
22.07.2019 14:16Это все замечательно конечно, но автоматический перевод из килограммов в литры без а-ля 1С костыля в виде упаковки, вы надеюсь уже сделали, да?:)
VolCh
24.07.2019 08:52Я вот не заметил уменьшения сумарного времени тестирования при разбиение на мелкие таски. Чем они меньше, тем больше вероятность, по-моему, что они будут пересекаться. Оптимум не лежит в крайностях.
maximsukharenko Автор
24.07.2019 14:51Вы правы, но видимо у нас возникла проблема в терминологии. В статье я говорю про фичи — некоторый функционал, который можно протестировать, т.е. он что-то но делать будет. Таски на мой взгляд — это больше про конкретные задачи исполнителю, и способ их дробления во многом зависит от проекта и его стадии. Например, на старте проекта нормально иметь таски с эстимейтом в пару дней.
Так же стоит оговориться относительно релизной политики. У нас заведено следующее — мастер всегда полностью работоспособен и готов к выпуску (используем слека накрученный git-flow). Если что-то протестировали — оно льётся в мастер. Соотвествено чем меньше времени живёт таска, тем менее вероятно, что она с кем-то пересечётся по коду.
cross_join
Задача адаптации ПО к меняющемуся API фукционально-техническая, способов для ее решения известно немало с незапамятных времен, прежде всего параметризация (декларативный) и скриптовый язык настройки (императивный). Поэтому не вполне понятно, при чем здесь скрам.
maximsukharenko Автор
Любая фукционально-техническая задача на маштабе выливается в задачу управления. Agile тут про то, чтобы понять как дожить до светлого будущего с параметризацией и настройками, а не помереть по дороге.