Привет, Хабр! Сегодня мне хотелось бы поговорить о такой интересной метрике, как Maintainability - возможность вести доработки и улучшения при создании сложных систем. Ведь при развитии любого программного продукта возникает вопрос, сколько будет стоить его поддержка и развитие. Мы в Киберпротекте разрабатываем линейку продуктов для защиты данных и сегодня это — миллионы строк кода, требующие ощутимых затрат как на поддержку, так и на расширение возможностей или исправление найденных ошибок. В этой статье я делюсь своими мыслями о том, как оценить Maintainability, из чего она состоит, можно ли ее измерить, и как принимать правильные решения при работе с кодом.

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

Проблемы неидеального кода

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

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

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

Кроме этого, чем хуже обстоят дела с Maintainability, тем хуже мотивация разработчиков. Это значит, что они могут дополнительно повышать сложность кода или просто уходить в другие проекты, где работать проще. Таким образом, в коде остаются плохо документированные и неавтоматизированные фрагменты, о которых знает только узкий круг людей. В таком случае уход всего 2-3 человек еще больше увеличивает стоимость поддержки кода. А при увольнении еще пары человек уже может быть риск для ведения бизнеса. Придется нанимать любых разработчиков с рынка, возможно втридорога, чтобы закрыть эту дыру.

Что делать в такой ситуации? Я не раз слышал призыв: “Давайте выкинем кодовую базу и перепишем ее с нуля”. Но, если честно, я не знаю, при каких условиях этот подход себя оправдает. Насколько показывает мой личный опыт, а также мнение моих коллег и экспертов, с которыми приходилось общаться — на практике это никогда не работает.

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

Удалить это нельзя. Жить с этим невозможно. Что делать?

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

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

Метрика Modifiability включает в себя оценку так называемого Coding Style. Если все написано в одном стиле, оставлены комментарии для будущих поколений, то дорабатывать код будет проще.  Точно также для улучшения Modifiability нужно стремится к низкой цикломатической (или структурной) сложности кода — то есть избегать большого количества ветвлений в рамках одного сегмента, а также к компактности (отсутствию "портянок" на несколько страниц), атомарности и простоте (речь идет в том числе про отсутствие смешивания разного функционала) единицы компиляции (функции, класса, методов класса). И это еще не все — в понятие Maintainability входит много других приемов улучшения читабельности кода.

Однако в реальном мире при стремлении к хорошему Modifiability нужно вовремя себя остановить. Как говорил Рид Хоффман "Если вам не стыдно за первую версию вашего продукта, вы запустились слишком поздно"? 

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

Что касается креативности разработчика, тут тоже есть свои нюансы. С одной стороны, оригинальный подход — это хорошо. Но чем более креативно написан код, тем выше требования к креативности будущих читателей этого кода. Поэтому вместо использования семантически красивой конструкции, зачастую лучше использовать что-то простое, пусть даже с меньшей эффективностью (конечно, если она не критична). В этом случае можно пожертвовать даже компактностью кода, потому что “красивая” конструкция тоже не имеет смысла, если она будет не читаема. Так вы гарантируете, что потом его смогут прочитать больше людей без сверхнапряжения мозга. Да и на самом деле "семантически красивая конструкция" зачастую даже менее эффективна, чем "простой" код.

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

При хорошей Modularity большая часть изменений проходит локально, внутри одного модуля — микросервиса или библиотеки. Впрочем, даже в монолите, и в рамках одного модуля возможна хорошая Modularity. Чаще всего даже библиотека или микросервис не состоят из совсем уж атомарного функционала (утрируя, из одной функции или класса). В этом случае, как и для монолита, важна хорошая структурированность "внутри". Фактически Modularity важна на всех уровнях: макро-части продукта, модулей, единиц компиляции. При таком подходе изменения кодовой базы будут происходить быстрее и стоить дешевле.

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

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

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

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

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

Наличие модульного тестирования, кроме того, облегчает исправление ошибок: выделяется минимальный сценарий, под него пишется тест, запусками которого контролируется исправление (т.е. частично применяется всем известный принцип TDD aka "разработка через тестирование").

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

По большому счету Supportability складывается из ответов на подобные вопросы:

  1. Может ли пользователь починить проблему самостоятельно или с подсказками службы поддержки и сводится ли проблема к предыдущим кейсам? (впрочем, это возможно только при наличии достаточной документации, построенной на базе информации от разработчиков и тестировщиков)

  2. Какая диагностическая информация нужна для разрешения ситуации, и удается ли ее собрать?

  3. Можем ли мы сказать, в каком компоненте произошла ошибка?

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

Support: Ваня, это твоя проблема?

Ваня: Не, вообще не моя. Спросите Валеру.

Support: Валера, это твоя проблема?

Валера: Нет, Васина. Я точно знаю

Support: Вася, посмотри, что там сломалось?

Вася: Ну ок, сейчас...

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

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

Тут снова играет роль степень покрытия тестами (особенно когда мы говорим об автотестах и регрессивном тестировании), потому что без этого невозможно развитие кода (Modifiability, "чистка мусора" aka рефакторинг и т.д.). Я бы даже сказал, что изменение кода, предварительно не покрытого тестами, может быть успешным только случайно, если мы, конечно, говорим не про "Hello, world".

Да, важным источником данных об ошибках выступает служба поддержки. Но она получает ее от пользователей, хотя о потенциальных проблемах можно было бы узнать и раньше. А сделать это можно именно за счет покрытия тестами. 

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

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

  1. Падения системы

  2. Зависания системы

  3. Снижение производительности

  4. Нештатное поведение системы

Так какой же уровень Maintainability нужен?

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

Интересное мнение по поводу оценки технического долга высказывает Мартин Фаулер, один из ведущих идеологов в этой сфере, почитать его можно здесь. Основная мысль заключается в том, что любой код содержит в себе “мусор”, и из-за этого разработка ведется медленнее. Но каждый раз нам нужно делать выбор, расчистить часть мусора, скажем, за один день, чтобы сократить время разработки дополнительного компонента на 2 дня или весь мусор за 4 дня, чтобы разработка велась быстрее на 3 дня. Как показывает практика, стремиться к идеальному коду и идеальному Maintainability не имеет смысла. Вместо этого нужно выбрать уровень Maintainability который обеспечивает максимальный вклад в сокращение стоимости поддержки и развития кода. 

Часто Maintainability ассоциируют с техническим долгом. Но эта ассоциация не верна: улучшение Maintainability может быть частью долга, но синонимом не является. Конечно, в общем случае наличие практически любого технического долга замедляет разработку, что описано, например, в agile методологиях, говорящих о соотношении velocity с technical debt.

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

Кстати, интересно, а вы используете в своей практике какие-либо методы поддержания или улучшения Maintainability? Применяете методы или инструменты для его измерения и оценки? Поделитесь, пожалуйста, в комментариях, если у вас есть такой опыт.

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


  1. laatoo
    02.12.2022 15:05
    +1

    Как измерить каждый параметр? Какое значение ок, а какое не ок? Что делать, если не ое? Как отличить каждый перечисленный *able от not *able? На глазок? На чей глазок? Почему его?


    1. CyberArchitect Автор
      02.12.2022 17:47

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


      1. mvv-rus
        04.12.2022 21:01

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

        К примеру, настоящие программисты (те самые, которые не используют Паскаль ) будут работать эффективно по опредлению, не обращая внимания на метрики ;-) .