Это история про то, как нам удалось написать довольно сложную business-critical систему, и добиться, чтобы она была стабильной даже без юнит-тестов (WAT?!).

Представьте: ERP-система в области логистики. Сложная бизнес-логика, составление расписаний, электронный учет рабочего времени, планирование и управление ресурсами со статистическими подсказками на основе сотен миллионов записей геораспределенных исходных данных, мониторинг прогресса >1000 одновременно работающих водителей в реальном времени, интеграция с 7 другими системами (в том числе финансовыми), и т.д., в общем большая и сложная система.

Систему начали с нуля, писали на ASP.Net Web API, Entity Framework и AngularJs (Реакта и Vue тогда еще не было, дело было в далёком 2013), небольшой, но высокопрофессиональной командой, по Scrum, написали за 4 месяца первую базовую продакшн-версию с минимальным функционалом, впоследствии функционал постепенно наращивали, система развивается и по сей день. Через год после начала разработки, система уже была внедрена по всей компании и являлась business-critical, т.е. остановка системы даже на несколько часов могла повлечь довольно существенные убытки для компании.

И возможно, вам сейчас станет немного жутковато, но мы обошлись без unit-тестов и выделенных тестировщиков. При этом, на протяжении нескольких лет, разработка нового функционала шла очень активно, выгрузки на продакшн в среднем 1 раз в неделю, постоянно менялись люди в команде (потому что консалтинг компания), и… система не падала и даже совсем не возникало сколь-нибудь критичных багов, только мелкие баги и косметика. Просто везет, или есть какой-то секрет?

Почему без юнит-тестов?


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

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

Сколько раз я видел эти классические слова: «это не тесты у вас не работают, это вы просто их писать не умеете». Но ведь так можно любую неподходящую технологию оправдать… Нет, я думаю, не всё так просто. Вот с паттернами к примеру. Любой более-менее опытный программист знает: паттерны это не панацея, универсальных решений не бывает, всегда нужно смотреть по ситуации, что конкретно использовать и как именно.

Вот и тесты такие же на мой взгляд — да, есть случаи, когда автоматические тесты подходят идеально: например, если реализуешь какой-нибудь протокол, или пишешь компилятор, или библиотеку со строго заданным API и прочее. В таких случаях есть четкие требования и они описаны на низком уровне — и тесты работают замечательно. А по TDD такие проекты делать и вовсе сплошное удовольствие.

Но есть и бизнес-приложения. Требования вроде и есть, но они описаны на очень высоком уровне, и мапить их в тесты мягко сказать непросто. Вроде, потому что заказчик в начале проекта сам не до конца понимает, что конкретно ему нужно. Это выясняется в деталях только в процессе, на Sprint Demo или на дэйли. Если разработка действительно agile, то требования часто меняются, а знаете что такое изменение требований на высоком уровне, в разрезе тестов? А вот что: удаляем несколько десятков тестов и пишем заново. И это продолжается весь проект. Каждый второй спринт. Примерно так…

И да, с учетом того, что кода тестов при хорошем покрытии значительно больше, чем кода самого решения… Получается, в общем, довольно неэффективно.

Если не юнит-тесты, то что?


На удивление, способов обеспечения качества ПО, помимо автоматических и ручных тестов, немало! Жалко и обидно, что они настолько недооценены, и о них мало кто говорит. Вот посмотришь хабр: тесты, тесты, тесты. Качество софта = всегда только тесты. И все, как будто нет альтернатив. Но ведь это не так: альтернативы есть, и в ряде случаев они подходят гораздо лучше.

В свое время меня эта тема очень интересовала и я изучал самые разнообразные методы. Интересовался, например, как делают космическое ПО, где одна ошибка может повлечь многомиллионные убытки. Наверное многим известно, что там используются такие супер-тяжелые методы, как формальная верификация, тотальное документирование, перекрестное тестирование независимыми командами тестеровщиков и прочее. Но оказывается, есть у них на вооружении и более дешевые средства (подробнее об этом ниже).

Другим «источником вдохновения» стал Брет Виктор, автор множества потрясающих концептов, и в том числе идеи Seeing spaces, и особенно той ее части (начиная примерно с 3:53 на видео), где речь идет о способах расследования и предотвращения багов. Идея в том, что для того, чтобы понять, что происходит внутри системы, нужно как бы «развернуть» эту систему, заглянуть внутрь, визуализировать ее во всех возможных плоскостях, увидеть как она работает изнутри. Если хорошо понимать, как работает система — то многих ошибок можно избежать. Здесь кстати снова пересечение с космической темой: это получается этакий локальный «Центр Управления Полетами» для данной системы.


(картинка взята отсюда: vimeo.com/97903574)

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

Безусловно, в современном вебдеве это решается с помощью логов, трейсинга (OpenTelemetry) и мониторинга, но многие успешные компании в добавление к этим вещам имеют свои собственные, специализированные утилиты, заточенные под визуализацию этого конкретно решения.

Кроме этих вещей, были недели поисков в гугле, десятки прочитанных статей, множество опробованных подходов и утилит, и конечно многие часы работы над кодом. Забегая вперед, одним из главных открытий для меня стал метод «fault tree analysis», но были и другие очень важные вещи. Всё это в совокупности позволило получить надежную и стабильную систему без юнит-тестов и тестировщиков.

«Космический» подход


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

  1. Формальная верификация. Использовать ее на бизнес-проектах конечно не получится (слишком дорого), но понимать — полезно. Понимать проблемы, с которыми формальная верификация сталкивается, понимать почему же так сложно доказать что цикл завершается, и понимать почему формальная верификация — не панацея, даже если есть возможность ее применить. Есть классный David Crocker's Verification Blog, где можно с этой темой ознакомиться, рекомендую. Когда-то давно я читал этот блог как худлит, на ночь, прямо с самой первой записи и дочитал «до наших дней» (причем это единственный блог вообще который я прочитал полностью в своей жизни). Да, часть информации специфична для C/C++, но все равно очень много полезного по теории формальной верификации и статическому анализу.
  2. Программирование по контракту. Контрактное программирование тоже хорошая штука, но снова очень не дешевая и далеко не панацея. Например, в упомянутом выше блоге Дэвида, есть интереснейшая статья о случае с взрывом ракеты Ariane 5 при взлете, где он небезосновательно утверждает, что runtime-проверки могут спасти, но только если: а) программа может сделать что-то полезное, если проверки не прошли; б) это что-то полезное было как следует протестировано. В общем, знать про контрактное программирование очень полезно, но для повсеместного применения в бизнес-решениях оно конечно дороговато.
  3. Статический анализ. Сейчас много мощных средств для статического анализа кода программ. Почему бы их не использовать? Например, эксперт Gerard Holzmann из лаборатории надежного ПО NASA в документе The Power of Ten — Rules for Developing Safety Critical Code пишет, что никаких оправданий просто не может быть чтобы не использовать. Потому что это очень дешево: запустил анализатор и все. А ведь статический анализатор очень хорош для отслеживания некоторых категорий технических багов, которые сложно заметить на глаз.
  4. Простой код. Простой код проще поддается статическому анализу, и в нем меньше вероятность сделать ошибку. Опять же согласно документу из предыдущего пункта, методы и функции желательно умещать в один лист печатного текста ака 60 строк кода: иначе психологически не воспринимается, как логическая единица. И желательно избегать рекурсии: рекурсивный алгоритм обычно сложнее понять, чем тот же алгоритм развернутый в цикл (и опять же статические анализаторы лучше с циклами работают чем с рекурсией).

Давайте рассмотрим последние два пункта немного подробнее.

Статический анализ


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

Кстати, самый простой тип статического анализа — это intellisense, то, что мы каждый день используем в IDE (ну и компиляция кода, как частный случай). Очень важно, чтобы ваш проект полностью покрывался интеллисенсом. Например, если не использовать Entity Framework, а писать magic strings stored procedures, то вероятность сделать ошибку при очередном рефакторинге резко возрастает.

Кроме стандартного intellisense, есть конечно же линтеры, такие как Stylecop или более современные и продвинутые, типа SonarQube.

А если хочется написать что-то совсем кастомное, заточенное под конкретный проект (и мне кажется, это очень даже полезно делать), то в C# есть ещё даже более крутая штука: Live Code Analyzers!

Live Code Analyzers


Начиная с Visual Studio 2015, с появлением Roslyn, были добавлены Live Code Analyzers — статические анализаторы кода, которые запускаются в IDE по мере создания кода. Иными словами, простая и доступная возможность создать кастомный intellisense.

В Live Code Analyzer есть доступ ко всему, с чем работает компилятор: лексическая свертка, AST, результаты семантического разбора. Можно комплексно анализировать код и обнаруживать довольно сложные ошибки.

В этой статье не хочется погружаться слишком глубоко в Code Analyzers, но давайте рассмотрим хотя бы вот такой простой пример — обнаружить все методы в solution больше 100 строк:

private void CheckMethodsAreShortEnoughToComprehend(SyntaxNodeAnalysisContext context)
{
    var methodDeclaration = (MethodDeclarationSyntax)context.Node;

    // собственно проверка
    if (methodDeclaration.Body == null || methodDeclaration.Body.GetText().Lines.Count <= 100)
        return;

    // если проверку не прошли, кидаем ошибку
    var diagnostic = Diagnostic.Create(ShortMethodsRule, methodDeclaration.Identifier.GetLocation(), methodDeclaration.Identifier.Value);
    context.ReportDiagnostic(diagnostic);

}


Полный код класса анализатора
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MyAnalyserAnalyzer : DiagnosticAnalyzer
{
    private static DiagnosticDescriptor ShortMethodsRule = new DiagnosticDescriptor(
        "MyAnalyser.ShortMethodsRule",
        "Method is too long.",
        "Method '{0}' is more than 100 lines long.",
        "Database",
        DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "Long methods are hard to read and comprehend. Statistics shows, that long methods have much more mistakes. Please refactor.");

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
    {
        get
        {
            return ImmutableArray.Create(ShortMethodsRule);
        }
    }

    public override void Initialize(AnalysisContext context)
    {
        // для каждой ноды "method declaration" в solution будет запущен наш метод проверки
        context.RegisterSyntaxNodeAction(CheckMethodsAreShortEnoughToComprehend, SyntaxKind.MethodDeclaration);
    }

    private void CheckMethodsAreShortEnoughToComprehend(SyntaxNodeAnalysisContext context)
    {
        var methodDeclaration = (MethodDeclarationSyntax)context.Node;

        // собственно проверка
        if (methodDeclaration.Body == null || methodDeclaration.Body.GetText().Lines.Count <= 100)
            return;

        // если проверку не прошли, кидаем ошибку
        var diagnostic = Diagnostic.Create(ShortMethodsRule, methodDeclaration.Identifier.GetLocation(), methodDeclaration.Identifier.Value);
        context.ReportDiagnostic(diagnostic);

    }

}



Как видите, все довольно просто. Некоторое количество обвязки, но сама проверка — буквально одна строка. Впрочем, более полезные проверки потребуют, конечно, намного больше кода.

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

Например, мне удалось создать анализатор, который ругается, если поместить запрос к БД за пределы методов Web API контроллера (чтобы работа с БД не размазывалась по всему решению, поскольку очень часто проблемы с производительностью возникают после того, как кто-нибудь использует метод, дергающий базу данных в цикле).

Но вот сделать более сложный анализатор, который бы следил, чтобы внутри endpoint'а типа PUT /user/{id}, невозможно было бы случайно изменить пользователей с другим ID (чтобы предотвратить случайный data corruption), уже не вышло.

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

Простой код


Простой код — это самый надежный способ борьбы с логическими ошибками. Простой, понятный, лаконичный код — это прекрасно! Но непросто.

Блоггеры и докладчики меня поймут: вот пишешь статью, или готовишь доклад, и хочется привести пример, который был бы одновременно и простым, и полезным. И вот на то чтобы изобрести такой пример, несколько дней может уйти (и еще не факт что придумаешь в итоге)! Найти те заветные 20 строчек кода, которые хорошо бы демонстрировали идею, делали бы что-то действительно полезное, и были бы простыми и понятными при этом — на практике оказывается очень нелегко.

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

Есть по этому поводу знаменитая цитата от Blaise Pascal (перевод с французского):
Это письмо получилось таким длинным потому, что у меня не было времени написать его короче.

И в этой цитате, безусловно, — очень много правды!

Вот какие подходы и методы лично я использую (весьма успешно) для создания простого кода:

  1. Инкрементный рефакторинг. Этот подход позволяет избежать излишнего усложнения кода еще на стадии разработки проекта.
  2. Простые оптимизации. Подход к решению проблем с производительностью, чтобы сохранить качество кода.
  3. Fault tree analysis. Это способ для определения наиболее критичного кода, который можно себе позволить сделать совершенным, и это окупится.
  4. Maintainability Index. Вычисление метрик кода позволяет легко найти участки кода низкого качества.

Давайте рассмотрим их подробно.

1. Инкрементный рефакторинг


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

Если грубо, идея заключается в том, чтобы не создавать детальную архитектуру кода проекта заранее. Накидали план в общих чертах, — и поехали. Как только какой-то класс или метод разрастается слишком сильно — ага, пора рефакторить. Видим, что слишком много контроллеров — ага, пора делать новый микросервис. И так далее. Таким образом, проект растёт инкрементно и «натурально», именно в тех местах, в которых действительно нужно.

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

Во-первых, такой подход прекрасно работает с заказчиком, который вначале проекта чётко не знает, чего хочет (т.е. с почти любым заказчиком). Во-вторых, нарисовать идеальную архитектуру для сложного проекта с первого раза в любом случае невозможно, в процессе обычно всплывает много такого, о чем на стадии планирования даже не задумывались.

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

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

Поэтому, исходя из моего опыта, лучше совсем уж кропотливо ничего не планировать, а планировать только в общих чертах, и «открывать» детали архитектуры уже по мере создания проекта. Для этого, конечно, необходимы две вещи:

  1. Threshold. Нужен способ понять, когда пора остановиться и проревизить архитектуру.
  2. Рефакторинг. Рефакторинг бывает разный. Некоторые понимают под рефакторингом «переписать половину проекта нафиг». Такое бывает, если всё совсем запустили и накопилась целая гора технического долга — т.е. когда thresold или отсутствовал, или был выбран неудачно. Зато при правильно выбранном thresold'е рефакторинг происходит почти безболезненно, настолько, что его можно даже делать в проекте без тестов.

Я использую простой threshold, состоящий из двух условий:

  1. Копипастить код можно (о ужас!), но не более одного раза (т.е. максимум 2 «копии»). Если копипастишь в третий раз — пора писать абстракцию.
  2. Если файл достиг 200 строк, пора рефакторить — разбивать его на несколько файлов, и заодно ревизить общую архитектуру.

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

Правильная архитектура автоматически ведет к более простому коду.

2. Простые оптимизации


Когда программисты сталкиваются с проблемами с производительностью, они начинают оптимизировать код. Частенько настоящую проблему поймать довольно сложно, поскольку она происходит только на продакшене, который так просто не подебажить. Так что оптимизируется всё что только можно, и по максимуму.

У нас был в случай, когда человек, не очень хорошо разбиравшийся в MS SQL, «соптимизировал» систему путем перевода почти всех запросов к БД с Entity Framework на stored procedures. Потом пришлось переводить обратно, потому что поддерживать T-SQL код, не покрытый интеллисенсом, довольно сложно (повторюсь, речь о бизнес-проекте, требования меняются постоянно, а с ними вместе и модель базы данных). Да, и к слову, перевод на stored procedures оказался бессмысленным — проблем это не решило. Решение было в добавлении индексов и предпросчёта.

К чему веду: очень легко переусердствовать с оптимизациями. Код при этом очень сильно усложняется, и соответственно сложнее становится избежать багов. Везде нужен компромисс, также и с оптимизациями.

Например, если выяснилось, что Entity Framework генерирует неоптимальный запрос, или не умеет генерировать запрос который вам нужен, это не повод переходить на хранимые процедуры. Покрутите проблему в голове, почитайте умные статьи, решение всегда найдётся. В частности, сложные T-SQL запросы это в любом случае зло — так что и не нужно пытаться их генерировать. Вместо этого, например, вытащите данные по отдельности простыми запросами и склейте их в памяти.

Вообще, 90% проблем с производительностью БД на средних объемах данных и не супер высокой нагрузке решаются пятью простыми способами:

  1. Индексы
  2. Простые запросы и склейка данных в памяти
  3. Кэш
  4. Предпросчёт
  5. Пейджинг

Для абсолютного большинства обычных проектов, этого достаточно. И даже в сложных проектах с Big Data и с миллионами пользователей онлайн, дальше оптимизировать нужно только наиболее нагруженные участки системы.

Мораль: Когда чините производительность, не увлекайтесь, а то сломаете себе носcodebase.

3. Fault tree analysis


Fault tree analysis — это по сути, просто метод для поиска возможных отказов, но его действенность, сложно переоценить.

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

И дальше начинаем с этих страниц, и думаем: что может привести к тому, что страница не будет работать? И как это можно решить?

Например, примерно так я анализировал одну из критических страниц нашего сервиса — страницу со списком заданий и водителей, которая отображалась на InfoTV:

  1. Если нет связи с сервером — страница просто не загрузится.
    • Решение: Можно использовать server workers, и в случае отказа сети, хотя бы показывать старый снапшот данных. Конечно, при этом желательно вывести уведомление, что данные старые, и показать дату, когда они последний раз были синхронизированы с сервера.

  2. Если ошибка в Javascript, то страница не сможет отобразить список заданий или же не сработает периодическое обновление страницы и она «застынет»
    • Решение: максимально покрыть фронтенд-код интеллисенсом, вынести в отдельный файл главный код, который отображает список и обновляет страницу, упростить его, некритичные функции обернуть в try-catch, чтобы ошибка в этих функциях не повлияла на отображение списка

  3. Если API возвращает ошибку или данные в неожиданном формате
    • Решение: продолжать показывать старые данные, как если бы система была оффлайн. Выводить соответствующее сообщение об ошибке.
    • Кроме того, можно улучшить стабильность бэкенда. Для этого нужно проанализировать, в каком случае может возникнуть ошибка на бэкенде?
      1. Если допущена ошибка в коде и возникло исключение
      2. Если сервер упал
      3. Если нет связи с БД
      4. Если БД перегружена и запрос вылетел с таймаутом
      5. Если на сервере БД вышел из строя жесткий диск


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

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

Что хочу сказать про fault tree analysis: несмотря на то, что более менее все архитекторы знают про reliability, про то, как его улучшать, но оценку проблем как правило проводят очень ситуативно, бессистемно, часто пытаясь применить обобщенное решение (что работает довольно плохо). Например, я работал в двух больших Unicorn-ах, и даже там подход к reliability всё ещё бессистемный, и только в Big Tech этим уже занимаются серьёзно.

Fault tree analysis позволяет сконцентрироваться на действительно критичных участках и провести полный, системный анализ возможных отказов на этих участках. Если вы так ещё не делаете — попробуйте, это действительно круто работает!

4. Maintainability Index


Предположим, что мы проанализировали возможные отказы, улучшили код, и всё стало хорошо! Но ведь код имеет свойство постоянно меняться, практически жить своей жизнью. Изменения могут быть небольшими, но постоянными, и на code review, когда ты видишь только diff, бывает нелегко оценить суммарную получившуюся сложность всего метода в целом.

И вот здесь, для поддержания кода в хорошем состоянии, очень хорошо подойдёт вычисление метрик кода, таких как Cyclomatic Complexity — это отлично умеет делать Visual Studio для C#, да и для других языков полно утилит, которые умеют это делать.

В Visual Studio основные метрики суммируются в характеристике под названием Maintainability Index (от 0 — совершенно невозможно этот код поддерживать, до 100 — очень легко поддерживать), а полный набор отображаемых метрик выглядит примерно так:



Строится дерево по всему решению, дерево можно развертывать и искать конкретные классы и методы, где все плохо. Это, даже без fault tree analysis, — первые кандидаты на рефакторинг, особенно если они относятся к важным частям системы.

Интересный факт: когда мы впервые вычислили метрики кода нашего решения, самое худшее качество кода было в самом главном, самом критическом методе всей системы (который если упадёт — то всё, это «full stop»). И я уверен, что такой феномен — совсем не редкость.

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

Заключение и выводы


Это довольно старая история, но всё ещё очень актульная. С тех пор, например, я основал свой стартап, ему уже 5 лет, десятки клиентов (B2B), десятки тысяч пользователей, и ни одного теста. Несмотря на кучу pivot-ов и глобальных рефакторингов, очень стабильная система, и я в состоянии поддерживать и развивать её в одиночку.

Конечно, с тестами было бы ещё лучше, ещё надежнее, — в идеале, все возможные способы улучшения надежности ПО должны использоваться в комбинации друг с другом.

Но в этой статье я хотел обратить внимание на то, что все эти «второстепенные» способы, на самом деле дают удивительно хороший результат, и могут использоваться даже без тестов. О них мало кто пишет, мало кто считает их серьезным подспорьем для улучшения надежности ПО. А зря, на самом деле, они отлично работают!

Комментарии (50)


  1. ermouth
    30.04.2022 19:28
    +1

    высокой нагрузке решаются пятью простыми способами

    Ещё частичная денормализация, ей бывает п.2 (склейку) ускоряют. Позволяет быстрее получить все данные, но без гарантии целостности, иногда этого достаточно. Да и при склейке такие гарантии всё равно очень легко потерять.


    1. omlin Автор
      30.04.2022 21:10

      Вы совершенно правы, денормализация является неплохим методом для улучшения производительности ????

      Я почему-то воспринимаю её как частный случай предпросчета (т.е. подготовки данных таким образом, чтобы потом было легко их доставать из БД и они уже в нужном формате), но наверное можно было вынести в отдельный пункт.


  1. andy128k
    30.04.2022 20:08
    +2

    Чаще всего за уверенностью в надёжности кода стоит недоинформированность. Тесты может и не единственный способ и даже может не самый лучший, но единственный измеримый.


    1. omlin Автор
      30.04.2022 20:34
      +6

      Измеримый, вы имеете в виду покрытие кода тестами?

      Ну смотрите, ведь простота кода - тоже в общем-то измеримый показатель, то о чём я пишу в статье под заголовком "Maintainability Index". Так что, всё-таки рискну не согласиться, не единственный измеримый.

      И кстати обе эти метрики, они не решают например проблему логических ошибок. В бизнес-приложениях, никто не знает полное количество use-case'ов, и поэтому невозможно оценить, написали ли мы достаточное количество интеграционных тестов.

      Даже сам заказчик часто не знает, например, нужна ли та или иная кнопка или нет. Это надо напрямую с конечными пользователями общаться (т.е. с водителями, если брать систему из статьи), и нет никакой гарантии, что все они используют систему одинаково.

      Помню, у нас был смешной случай (в другом продукте). Пользователи жалуются: система не работает, всё плохо. Покрытие кода тестами по этой фиче - гордые 100%. Тестируем вручную: всё отлично. Думаем, может это только на продакшене? Создаем тестовый аккаунт, проверяем - да всё нормально же. Ну думаем, наверное проблема в данных + продакшене. Копируем данные на тестовый аккаунт в продакшене, т.е. полная копия, идеально. И всё равно работает!

      В конце концов, устраиваем звонок с пользователями, говорим, покажите пожалуйста баг. Они открывают браузер, зажимают Ctrl (я даже не знал, что так можно), и открывают 10 документов со страницы в 10 разных вкладках. К этому моменту у всей команды уже шевелятся волосы на всех частях тела...

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


    1. DistortNeo
      01.05.2022 18:05
      +5

      Тест не доказывает надёжность работы программы. Тест просто фиксирует поведение программы на определённых входных данных.


  1. JordanCpp
    30.04.2022 20:23
    +1

    Два раза прочитал статью, что бы не упустить мысли автора. Но так и не понял, что это за утилиты мониторинга, визуализирующая процессы. И как по этому выводу понять, это ошибка или корректное проведение. Хотя бы пример бы привели. Эта утилита автоматом понимает, что это ошибка именно в том модуле? Второй момент, юнит тесты помогают тестировать систему постоянно, нет ручной работы. Сделал коммит. Тесты запустились. Если тест упал, всегда понятно в каком именно модуле, классе и конкретном методе, что то сломалось. Интеграционные менее показательны в этом смысле. Так как они тестируют конкретный процесс который содержит много функционала, этапов и т.д к примеру регистрация юзера. Состоит из множества проверок, есть ли такой логин, почта, валиден ли логин, и т.д Потом запись в базу. При разделении кода для возможности написания юнит тестов, возможно проверить все этапы + интеграционные для записи в базу. Но если тестировать как единую операцию, не понятно, что сломалось. База, валидация и т.д Суровые вы ребята, без юнит тестов))


    1. omlin Автор
      30.04.2022 20:57

      Спасибо, что прочитали статью дважды! К сожалению, система проприетарная, всё под NDA, привести скриншоты или совсем детальное описание, не представляется возможным.

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

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

      Функционально похоже на Honeycomb, но более визуально, с заточкой под нашу конкретную систему.

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


      1. JordanCpp
        30.04.2022 21:39

        Возможно я безвозвратно испорчен юнит тестами. Но окей есть дебаг режим есть логи. У вас есть эталонные логи, с которыми должны сходиться текущие логи? Это же тоже тест. Или вы именно когда юзер пишет, что то сломалось вы в ручную вычитывает логи и находите отклонение отталкиваясь от знания бизнес процессов и что вот конкретно на строке лога 20567 должно быть число 1 а не ноль. Кучу отладочных данных тяжело читать, понимать. Возможно вы управляете вселенной и для вас это на изи.))


        1. omlin Автор
          30.04.2022 21:53
          -1

          Люди очень хороши в "pattern matching". Ручной мониторинг во время релиза, визуализация системы хотя бы через стандартные Graphana-дашбоарды - это мне кажется прям очень сильно увеличивает вероятность обнаружить ошибку. Даже если каждый индивидуальный график выглядит "ок", нет резких скачков метрик, и 0 ошибок в логах, человек всё равно может заметить, что что-то не так, по совокупности графиков, на уровне шестого чувства, и начать разбираться.

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


          1. Vasyutka
            01.05.2022 02:10
            +2

            выглядит это так, что тесты бы всеравно не помешали. Но вы будто фокусируетесь на "ты хоть смотрел как это работает?". И отсутствием тестов принуждаете смотреть. Что кхм... тесты полезны. Думать головой и смотреть как оно работает - должно быть. Это как в Ералаш с бразильской системой. Не поймал - окно разбил. Не, на поле играть не будем. только под окном парикмахерской.


    1. funca
      30.04.2022 21:35
      +1

      Сейчас Observability стала популярной темой. Например вот статья у NewRelic, где решения во многом перекликаются с подходами автора (ну может кроме отказа от юнит тестов). Вещь определенно полезная - у людей гораздо выше восприимчивость к тому, что они получают в непосредственных ощущениях. Это позволяет оперативнее реагировать на проблемы, не дожидаясь момента когда их кто-то увидит и зарепортает.


  1. amarao
    30.04.2022 21:08
    +7

    Моя гипотеза: кто-то у вас работал гибридом PM'а и QA'я. Я это наблюдал несколько раз - кому-то надо, чтобы софт работал, а ресурсов на QA и формализацию процессов нет. И он лично всё проверяет, и знает как должно быть. Супер быстро, супер точно, супер правильно.

    Но!

    а) человека надолго не хватает, и как только фокус смещается, всё идёт куда-то.

    б) не масштабируется.


    1. omlin Автор
      30.04.2022 21:17

      Нет, такого человека не было :)

      Конечно, мы вручную проверяли после выгрузки, что визуально всё работает, но очень поверхностно. По сути, проверялось только несколько основных, самых критических страниц.

      Кроме того, мы постоянно показывали систему заказчику на Sprint Demo и даже на daily, но это обычное локальное тестирование разработчиком фичей, которые он разрабатывает.

      Тестировщик в команде был вроде около месяца, в самом начале проекта, но от него как-то быстро отказались.


      1. amarao
        30.04.2022 21:23
        +4

        Т.е. тестировали силами программистов и клиента. Тоже стратегия.


        1. JordanCpp
          30.04.2022 21:49
          +2

          Если идёт работа с денежными средствами? Особо не потестируешь. Особенно если есть зубодробительная логика, по переводу, учёту, работой с датами, учитывая разные модификаторы и т.д Как написать юнит тесты и проверить кейсы понятно. Допустим извратиться можно проделать на интеграционных, просто не вызывая последний шаг, непосредственно запуск реальной транзакции. Я не представляю, что это должен быть за инструмент, баг режим, который позволит забить на тесты. Откройте секрет)


          1. omlin Автор
            30.04.2022 22:05

            Все перечисленные в статье методы дополняют друг друга, дело далеко не только в мониторинг-утилите. Я бы сказал, Fault tree analysis намного сильнее улучшил для нас надежность кода.

            Вот например, допустим у вас есть интеграция с финансовой системой. Вы хотите сделать её надежной. Вы покрыли её тестами, но в один прекрасный день эта финансовая система просто взяла и поменяла формат отдаваемых данных. Или просто упала. Как тесты помогут в этом случае? :)

            А вот если был сделан fault tree analysis, архитектор подумал над всеми этими проблемами заранее, использовал например defensive programming для защиты от небольших изменений формата результатов, спроектировал механизм фоллбэка на резервную систему и алертов в случае полного отказа или же критичного изменения формата отдаваемых данных, и т.д.


            1. JordanCpp
              30.04.2022 22:29
              +3

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


              1. omlin Автор
                30.04.2022 22:41

                Безусловно, в разных проектах - разные особенности, и конкретные решения будут разными.

                Но сам факт того, что ситуация была заранее проанализирована и нужные меры были приняты превентивным образом, вот это важно :)


                1. PrinceKorwin
                  01.05.2022 09:36

                  Превентивный подход тоже так себе. Обычно он приводит к оверинжиниринг и избыточному коду который не соответствует спекам (это же превентивное решение того, с чем не сталкивались), а значит удорожание обслуживание этого кода.


          1. amarao
            30.04.2022 22:18
            +6

            Вот всюду, где "логика", как раз тесты и нужны, и их обычно проще всего писать. Если сложно - код г-но.

            Интеграционные тесты (особенно, с внешними сущностями), да, тяжело. e2e - тяжело. А уж логику изолировать в виде чистых функций и тестировать по самые помидоры - это ж азы нормального программирования.


            1. JordanCpp
              30.04.2022 22:31
              +2

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


    1. sshikov
      30.04.2022 21:32
      +4

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

      >а) человека надолго не хватает, и как только фокус смещается, всё идёт куда-то.
      Ну, как бы тесты тоже нужно поддерживать, т.е. ресурсы на них тратятся постоянно. А эффект зачастую бывает сомнительным. И как только фокус смещается, тесты устаревают, перестают поддерживаться, и становятся скорее обузой, чем подмогой. Такое бывает с любыми инструментами и процессами.


  1. lxsmkv
    30.04.2022 21:16
    +1

    А для отлавливания функциональных багов можно ввести форму обратной связи для пользователей :)


    1. JordanCpp
      30.04.2022 21:43
      +3

      При таком подходе, форма может быть вполне сломана и багрепортов как бы и нет))


  1. AlexDevFx
    30.04.2022 22:34
    +2

    Недавно слушал выступление на счёт языка F# и функционального программирования. Так вот автор утверждал, что использование F# позволило сократить количество ошибок в приложение за счёт более таких подходов как чистые функции, неизменяемость и в довесок сам язык позволяет выявить большее количество ошибок на стадии компиляции. Мне кажется, эта стать перекликается с его подходом к разработке.


    1. JordanCpp
      30.04.2022 22:43
      +6

      Самые дорогие ошибки логические. К примеру перепутали условие И и Или. И все. Чистые функции и в других языках есть, это подход к написанию, не самый эффективный конечно. Сам язык ищет системные ошибки, типа выход за границы массива, нулевые указатели и т.д Приятно конечно, но. Если то произошло, сервис упал, исправили запустили. Но если в логике Ошибка и даже тестов нет. Это прям беда. Ошибку выявить возможно, но только непосредственном мониторинге на рабочей системе. И одно дело юзер не смог зарегистрироваться на сайте знакомств. Другое дело лярд рублей немного не тому юзеру ушел или выплаты миллионам человек, ушли другим.


      1. 0xd34df00d
        01.05.2022 05:35
        +3

        не самый эффективный конечно

        Почему?


        Но если в логике Ошибка и даже тестов нет.

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


        1. JordanCpp
          01.05.2022 06:21
          +2

          Неэффективность заключается при обработке тяжёлых структур. Вместо модификации входной структуры, возвращается новая копия. Лишний new. Если GC туп, будет адовая фрагментация. Особенно если new в цикле. Возможно продвинутая система типов, может решить часть проблем. Но это уже требует рассмотрения конкретного случая.


          1. 0xd34df00d
            01.05.2022 10:38
            +4

            Вместо модификации входной структуры, возвращается новая копия.

            Если вы не используете старую версию структуры, то компилятор нередко может соптимизировать это в in-place-мутации.


            Если GC туп

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


        1. funca
          01.05.2022 09:22

          Это проще, чем писать тесты?


          1. 0xd34df00d
            01.05.2022 10:38
            +4

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


        1. Inspector-Due
          01.05.2022 19:35

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

          Давайте на примере! Допустим, у нас есть банк, но у нас есть система отслеживания подозрительных платежей. Так вот платёж считается подозрительным, если платеж превышает 100'000 у.е. И/ИЛИ тот, кто отправлял деньги, совершил за день платежей на 1'000'000 у.е. Такие платежи должны проходить ручную модерацию, соответственно, нам их надо замечать. Допустим, программист устал, вчера вечером и ночью в клубе был, сегодня на работе у него завал был, а эту часть кода писал сильно вечером, потому что сроки горят. Программист написал "(ЭтотПлатёж > 100'000) ИЛИ/И (Сумма(Платежи, ЭтотДень) > 1'000'000)" (в общем, перепутал И и ИЛИ. Требованием было написать И, он написал ИЛИ, или, наоборот, просили ИЛИ, а он написал И)

          Как защититься от таких ошибок?

          Я предложу своё решение на Haskell для случая, когда просили И, а программист написал ИЛИ.

          Решение!

          Очевидно, в силу вступает произведение типов (для продвинутых можно было бы, наверное, пойти в уровень kinds, но и моя реализация, на мой взгляд, спасает от ошибок). Для лучшего эффекта надо вынести некоторые конструкторы в отдельный файл и не экспоритровать их, чтобы не допустить ошибки (случайно не создать их). Извините за длинные названия и за ужасное качество кода, но думаю, что я смог минизировать ошибку перепутать И с ИЛИ. Кстати, заметьте, здесь нет ни &&, ни ||!

          Вот возникает вопрос по поводу второго случая (когда надо ИЛИ, а программист пишет по ошибке И). Это можно реализовать, использовав тип-сумму, но у меня вопрос: допустим, нам надо отправить пользователю причину, по которой его платёж оказался на ручной модерации. Если платёж превышает 100'000, то причина "платёж превышает 100'000". Если сумма за день больше 1'000'000, то причина "сумма за день больше 1'000'000". А если и то, и другое, то "платёж превышает 100'000" и "сумма за день больше 1'000'000". Вот как это изящно отразить последний случай в системе типов? Не писать же data Reason = A | B | A_And_B?

          module Main where
          
          import Text.Read ( readMaybe )
          import System.Environment ( getArgs )
          
          main :: IO ()
          main = do
            args <- getArgs
            case parseArgs args of
                Just (aop, somftd) -> 
                    case isUserSuspicious aop somftd of
                      Just _ -> putStrLn "The user is suspicious"
                      Nothing -> putStrLn "The user is NOT suspicious"
                Nothing -> putStrLn "Usage: ./program <amount of payment> <sum of money for this day>"
          
          parseArgs :: [String] -> Maybe (AmountOfPayment, SumOfMoneyForThisDay)
          parseArgs [aop, somftd] = (,) <$> (AmountOfPayment <$> readMaybe aop) <*> (SumOfMoneyForThisDay <$> readMaybe somftd)
          parseArgs _ = Nothing
          
          
          newtype AmountOfPayment = AmountOfPayment Int deriving (Eq, Show)
          
          newtype SumOfMoneyForThisDay = SumOfMoneyForThisDay Int deriving (Eq, Show)
          
          -- The constructor should be hidden
          data ThisPaymentIsSuspicious = ThisPaymentIsSuspicious deriving (Eq, Show)
          
          isThisPaymentSuspicious :: AmountOfPayment -> Maybe ThisPaymentIsSuspicious
          isThisPaymentSuspicious (AmountOfPayment x)
            | x > 100000 = Just ThisPaymentIsSuspicious
            | otherwise = Nothing
          
          -- The constructor should be hidden
          data SumOfMoneyForThisDayIsSuspicious = SumOfMoneyForThisDayIsSuspicious deriving (Eq, Show)
          
          isSumOfMoneyForThisDaySuspicious :: SumOfMoneyForThisDay -> Maybe SumOfMoneyForThisDayIsSuspicious
          isSumOfMoneyForThisDaySuspicious (SumOfMoneyForThisDay x)
            | x > 1000000 = Just SumOfMoneyForThisDayIsSuspicious
            | otherwise = Nothing
          
          
          data TheUserIsSuspicious = TheUserIsSuspicious ThisPaymentIsSuspicious SumOfMoneyForThisDayIsSuspicious deriving (Eq, Show)
          
          isUserSuspicious :: AmountOfPayment -> SumOfMoneyForThisDay -> Maybe TheUserIsSuspicious
          isUserSuspicious aop somftd = TheUserIsSuspicious <$> isThisPaymentSuspicious aop <*> isSumOfMoneyForThisDaySuspicious somftd


          1. 0xd34df00d
            02.05.2022 02:38

            Тут, увы, всё самое интересное (проверки на суммы и всё такое) всё равно на уровне термов. Более того, если бы была какая-нибудь гипотетическая функция


            payIfNotSuspicious :: User -> ??? -> IO ()

            то совершенно непонятно, как на уровне типов проверить, что она вызывается только для неподозрительных товарищей (и что конструктор, например, спрятан и не экспортируется из модуля).


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


            data User : Type where ...
            data Payment : Type where ...
            data TxHistory : User → Type where ...
            
            data Suspicious : (u : User) → Payment → TxHistory u → Type where
              PaymentTooBig : (p : Payment) → (value p ≥ 10000) → Suspicious u p h
              SumTooBig : (h : TxHistory u) → (sum h ≥ 1000000) → Suspicious u p h
            
            decideSuspicious : (u : User)
                             → (p : Payment)
                             → (h : TxHistory u)
                             → Either (Suspicious u p h) (Suspicious u p h → Void)
            decideSuspicious = ...
            
            pay : (u : User)
                → (h : TxHistory u)
                → (p : Payment)
                → (Suspicious u p h → Void)
                → IO ()

            Здесь Suspicious u p h — свидетельство (которое можно получить одним из двух способов), что пользователь u, пытающийся сделать оплату p с историей транзакций h, является подозрительным. Suspicious u p h → Void же означает, что эта тройка точно не подозрительная (потому что иначе можно было бы получить значение типа Void, а для консистентных систем типов это невозможно).


            Теперь мне достаточно одного взгляда на тип pay (и на определение типа Suspicious), чтобы понять, что эту функцию можно вызвать, только если пользователь доказуемо не подозрителен.


            1. 0xd34df00d
              02.05.2022 02:55
              +2

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


              Скажем, что можно ожидать от любого предиката Suspicious? Например, что если какая-то оплата отмечается как подозрительная, то и большая оплата помечается как подозрительная:


              susMonoPayment : (u : User)
                             → (p1 p2 : Payment)
                             → (value p2 ≥ value p1)
                             → (h : TxHistory u)
                             → Suspicious u p1 h
                             → Suspicious u p2 h

              — это теперь теорема, которую можно доказать и убедиться, что ваше определение разумно, и вы, например, не перепутали ≥ 10000 и ≤ 10000.


              Или, аналогично, если оплата подозрительна с какой-то историей транзакций, то разумно ожидать, что она подозрительна и с более длинной историей транзакций:


              susMonoTx : (u : User)
                        → (p : Payment)
                        → (h1 h2 : TxHistory u)
                        → (h1 `IsSubsequenceOf` h2)
                        → Suspicious u p h1
                        → Suspicious u p h2


            1. Inspector-Due
              02.05.2022 08:27

              С отрицанием, конечно, ситуация становится много интереснее, но я не могу понять, как можно сконстуировать тип Either a (a -> Void)? Это же закон исключённого третьего, с которым в интуиционистской логике, есть какие-то подоводные камни. И ещё: как, например, считать данные откуда-то, если, например, у value p тип не условныйInt, а непосредственно само число? То есть надо грязное значение из IO представить в виде чистого типа.


              1. 0xd34df00d
                02.05.2022 08:50

                С отрицанием, конечно, ситуация становится много интереснее, но я не могу понять, как можно сконстуировать тип Either a (a -> Void)? Это же закон исключённого третьего, с которым в интуиционистской логике, есть какие-то подоводные камни.

                Тип сконструировать можно :] Есть проблемы с термом, дающим значение этого типа. То есть, терма


                aom : (a : Type)
                    → (P : a → Type)
                    → (x : a)
                    → Either (P x) (P x → Void)

                действительно не существует, потому что он эквивалентен аксиоме исключённого третьего — он умеет для любого типа a и предиката P на этом типе определять, выполняется ли P на произвольном x.


                Но это не мешает существовать аналогичному терму для тех конкретных P, которые разрешимы. Например, (не)равенство натуральных чисел разрешимо (и, как следствие, разрешим Suspicious выше). А вот неравенство [вычислимых] вещественных уже неразрешимо. Лишний повод не представлять деньги вещественными числами.


                И ещё: как, например, считать данные откуда-то, если, например, у value p тип не условныйInt, а непосредственно само число? То есть надо грязное значение из IO представить в виде чистого типа.

                Ну как обычно, вытаскиваем из IO внутри >>= и передаём в уже чистые функции.


  1. funca
    30.04.2022 23:50
    +2

    Fault tree analysis позволяет сконцентрироваться на действительно критичных участках и провести полный, системный анализ возможных отказов на этих участках

    Можете рассказать подробнее как это у вас встроено в процесс разработки? Сколько всего человек причастны к разработке, что у них на входе, что на выходе, кто за все отвечает и как контролируется результат.


    1. omlin Автор
      01.05.2022 01:14
      +1

      На старте, в те четыре месяца перед первым продакшеном, в команде было, если мне не изменяет память, 6 человек - архитектор и ведущий разработчик (ваш покорный слуга) - отвечал за архитектуру и помогал с разработкой, фронтендер и бэкендер писали веб-морду, ещё один программист - мобильное приложение, которое использовалось водителями, также в команде были дизайнер и менеджер проекта (он же выполнял функции скрам мастера). Где-то на месяц к нам присоединялся тестировщик.

      Также, активно учавствовал в процессе представитель заказчика (в качестве Product Owner'а).

      Я как архитектор был ответственнен за планирование технической части, в том числе за обеспечение отказоустойчивости. Совместно с Product Owner, мы выделили критический функционал и где-то за месяц до дедлайна я произвёл анализ по модели fault tree analysis и добавил в бэклог задачи призванные повысить отказоустойчивость системы. Мы их приоритизировали и взяли в следующий спринт.

      После первого продакшена, размер команды разработки был снижен до двух человек, которые обеспечивали багфиксы, доработку и поддержку внедрения. После успешного внедрения, заказчик оценил бизнес-KPI (которые мы предварительно разработали), запланировал вторую итерацию, и т.д. Впоследствии, команда разработки редко когда переваливала за 2-3 разработчиков, project manager был на проекте part-time и помогал только на Scrum-церемониях. Это довольно типичная ситуация для консалтинг-компаний.

      Я пропустил вторую итерацию (был задействован на другом проекте), но учавствовал в третьей, в процессе которой я предложил дальнейшие улучшения по отказоустойчивости, в том числе написание той самой мониторинг-утилиты, которая позволила разобраться в некоторых накопившихся к тому времени проблемах с производительностью. Кроме того, был произведен повторный анализ критического функционала системы по методу fault tree analysis, и, как результат, мы добавили оффлайн режим и ещё кое-какие улучшения.

      Если говорить не про консалтинг, а про продуктовую разработку, обеспечение отказоустойчивости в команде - это обычно задача техлида, а практики и рекомендации по улучшению отказоустойчивости разрабатываются архитекторами и/или командой платформы и внедряются через Global Technical Roadmap.


  1. apro
    01.05.2022 03:10
    +3

    Эти все подходы это конечно замечательно, но все равно непонятно как

    же избавились от юнит тестов. Допустим в расчете стоимости у вас плавающая ошибка в пару тысяч рублей или в расчете расстояния перевозки 10-20 километров. Юнит тест обычно ловит это элементарно и главное автоматически проверяет каждый новый коммит. Здесь же только если повезет и при ручном тестировании кто-то попадется въедливый,

    и главное такой человек должен появляться регулярно, но у вас даже QA нет.

    А не один из описанных методов не защищает от опечаток в арифметических выражениях.


    1. funca
      01.05.2022 10:35
      +4

      Насколько я понял, проект писался с нуля 4 месяца вчетвером (фронтенд, бекенд, мобайл и тимлид). Каждый знает свою область, в юнит тестирование не умеют, к моменту релиза все требования ещё умещаются в одной голове. Есть один замотивированный на результат клиент. Можно спокойно обойтись без автотестов и регрессии, делая проверки вручную. Раньше веб студии только так и работали, и все были довольны.


  1. staticY
    01.05.2022 11:32
    +6

    Не понял совершенно, как без тестов можно гарантировать правильность поведения сложной логики, например?

    Пользователь не был на сайте неделю, и у него рейтинг больше, чем 3 - тогда шлем ему красивый пуш. Но если при этом пользователь не пользовался комментариями или напокупал за последний месяц на 10тр - то шлем другой и некрасивый пуш.

    Как вы подобное будете без тестов проверять? В той же ЕРП подобной логики - вагон и маленькая тележка. А если рефакторинг прошел - опять все перепроверять? Базу руками подсовывать и в постмане ручки дергать?

    А если код на стейдже даже запустить проблематично (чтобы 1 в 1 было с реальностью)? Ну например те же пуши, которые ходят по расписанию разными списками для разных типов пользователей. Да тут без тестов просто сединой покроешься... А если у вас 31го декабря должен отправляться особый праздничный пуш.. то вы будете время на компьютере двигать туда-сюда при проверке?) Ну 1 раз подвигаете, потом забъете и 12го января в БД будут выявлены артефакты в данных..

    Логика со сложными условиями - имхо первый кандидат на покрытие тестами, ничего другого тут ну просто не придумано.

    А если например в проекте несколько языков программирования и куча народа пилит код? Я вот например понятия не имею, чтот там в PHP произойдет, если вместо пустой строки в джейсоне прилетит явный null (условно). Я в таких случаях просто пишу тест и не думаю про это...

    А если вы захотите версию языка или библиотек обновить? И что делать, если вообще ничего тестами не покрыто? Полнейший регресс делать?


    1. omlin Автор
      01.05.2022 15:54
      +2

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

      Например, вы смотрели Modelling Time Эрика Эванса? Это же просто удивительный подход "outside of the box", на мой взгляд. И вместе с тем, очень разумный.

      Кстати, если бы меня спросили, "назови самую сложную проблему для программиста", я бы без раздумий назвал временные зоны. Любая запутанная бизнес-логика отдыхает по сравнению с взрывом мозга, который происходит, когда работаешь с таймзонами. Даже в тестах легко запутаться с ними.

      Кстати, все почему-то забывают, что есть разряд ошибок, которые очень легко совершить. Все же знают про оптические иллюзии (например, вот эти). Неважно, сколько раз смотреть на оптическую иллюзию, всё равно будет казаться неправильно. Человеческий мозг так устроен. Мы не роботы.

      Если вы сделали ошибку в программе, вы можете сделать такую же ошибку на code review, и вы можете сделать такую же ошибку в тестах. От этого никто не застрахован, и на моём опыте я такое видел неоднократно. Если код запутанный, то и правильный тест для него - очень сложно написать.

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

      Или другой классический случай. Функция с двумя строковыми параметрами. Код полностью покрыт тестами, тесты проходят, всё идеально. Но при вызове функции из реального кода, параметры перепутали местами. У нас был такой баг в продакшене, причём оно не падало, а вело к неверному поведению в некоторых случаях, и отловить эту проблему было просто невероятно сложно. Хотя статическая типизация + линтер с правилом на недопущение параметров одинаковых примитивных типов, решают весь этот класс проблем со стопроцентной вероятностью.

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


  1. stgunholy
    01.05.2022 14:54
    +2

    Добрый день! Это прекрасно! Именно РОВНО ТАК ЖЕ и работаю со всеми проектами уже лет пять. От юнит-тестов совсем не отказался, оставил только для тестирования абсолютно самодостаточных кусков что работают с датами и с числами, потому что эта часть точно не меняется раз в неделю. Остальное все, из за того что у бизнеса хотелки всё время разные с прыжками в ширину, покрывать тестами бесполезно. Стек немножко другой - Котлин на бэке, но на самом деле роли не играет. Запросы по возможности все строить с KProperty, количество стринговых запросов и тд свести к минимуму, или убрать совсем. и всё... замечательно развивается и поддерживается. Спасибо за статью - просто вся моя боль и непонимание переусложнения проектов структурированы и разложены по полочкам.


    1. omlin Автор
      01.05.2022 15:21
      +1

      Спасибо за отзыв!

      Котлин на бэке, но на самом деле роли не играет

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

      Более того, в плане интеллисенса, реализовал полное покрытие - фронтенд (+JSX для темплейтов), бэкенд, ORM (MongoDB запросы в TS полностью покрываются интеллисенсом из коробки), и даже коммуникацию между фронтендом и бэкендом, т.е. нет magic strings в виде "/api/users", и даже можно нажать F12 из фронтенда и прыгнуть в бэкенд. Это очень круто кстати, правда требует, чтобы и бэкенд и фронтенд были написаны на одном языке (ну или нужен какой-то плагин к IDE, что уже сложнее, т.к. разные люди используют разные IDE).


  1. VFaland
    02.05.2022 15:20

    Хех, у нас в Big Tech fault tree analysis это просто часть ежедневной работы каждого программиста. Начинается на уровне дизайна системы, а потом постоянно на уровне кода, в каждом коммите. Если нормально все точки отказа не проработаешь, код просто не пройдет code review. И это не отменяет юнит тестов, наоборот - большинство нестандартных ошибок в тестовом кластере могут не встретится никогда ("пожар в дата центре"), и хоть как то убедится что код делает то что надо можно с помощью тестов (ну может за исключением совсем тривиальных случаев).


    1. omlin Автор
      02.05.2022 15:33

      согласен, как я уже писал выше, все методы должны применяться вместе для максимального результата, просто основная масса программистов о том, что есть что-то кроме тестов, даже не подозревает...


    1. PrinceKorwin
      02.05.2022 21:58

      Если нормально все точки отказа не проработаешь, код просто не пройдет code review.

      Как этого достигаете? Есть чеклист? Можете его показать? Действительно интересно.


      1. VFaland
        03.05.2022 03:37

        Обычно в каждом подразделении / продукте своя специфика, свой док по код стайлу и код ревью, где в том или ином виде могут быть чеклисты для ошибок/отказов, best practices для retry, какие-то специфичные для домена вещи итп. Строгого регламента обычно нет, скорее это все сайд-эффект высокой инженерной культуры, никого не надо пинать, народ сам инициативу проявляет по улучшению процессов.


  1. zloddey
    03.05.2022 14:52

    В статье подробно расписаны технические методы работы с кодом. Это и правда полезно, спасибо. Но, к сожалению, не совсем раскрыта "бизнесовая" сторона вопроса. Если бизнес-требования меняются быстро и в разных местах, то высока вероятность того, что ожидаемое поведение системы для новой фичи может начать конфликтовать с ожидаемым поведением для какой-то из старых. При наличии правильных (business-faced) автотестов эту ситуацию можно отлавливать на раннем этапе. А как вы справлялись без них? Было бы интересно узнать.


  1. zloddey
    03.05.2022 15:14

    Несмотря на кучу pivot-ов и глобальных рефакторингов, очень стабильная система, и я в состоянии поддерживать и развивать её в одиночку

    В одиночку поддерживать консистентность кода как раз легче, чем в команде. Чем больше команда, тем выше риски расхождения в понимании тех или иных аспектов работы системы. Я в своих личных проектах тоже помню всевозможные нюансы, которые важны для корректной работы, но не обязательно явным образом выражены в коде или доках. Это знание позволяет принимать правильные решения. Но если в этот код придёт кто-то другой, очень сомневаюсь, что в его голове появится такое же понимание. Чем больше людей в команде, тем больше этих "чёрных ящиков" - голов, в которых принимаются решения, порой по неясным для других голов принципам. Быстрый и автоматизированный фидбек - это действенный способ отделить неправильные решения от правильных и скорректировать поведение "чёрных ящиков". Настроенный intellisense и "статический анализ" (хотя по описанию это больше похоже на architectural fitness functions, имхо) - это дело полезное. Но лично я бы не рискнул полагаться только на них в команде из нескольких человек. Особенно распределённой или меняющейся.