Неизменные изменения

Давайте начнем с тривиального, но неоспоримого факта: программное обеспечение постоянно развивается – устаревает и обновляется, видоизменяется и дает дорогу новому.

Заметным исключением является наборная система TeX, разработанная Дональдом Э. Кнутом (D.E.Knuth). Предполагалось, что эта система должна быть совершенной, но даже в ней можно найти свои недочеты. Тем не менее, это уже отдельная тема для другой статьи.

Как измерить сложность разработки?

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

Опытный программист может написать более 500 строк качественного кода в день, что дает минимум 10 тысяч строк кода в месяц и 100 тысяч строк кода в год. А если у нас есть команда из 5 таких специалистов, то это уже два миллиона строк кода за пару лет. Однако, ответ на последний вопрос не может быть однозначным.

Действительно, несправедливо измерять индивидуальную продуктивность количеством написанных строк кода. Однако, если рассмотреть в комплексе всю систему, которую создают сотни разработчиков за несколько лет, то можно сделать вывод, что динамика роста кодовой базы является вполне разумной метрикой. И она будет существенно ниже теоретически возможных 500 строк кода в день на разработчика. Может быть 10-20% от этой величины.

Закон увеличения сложности

Второй закон термодинамики утверждает, что энтропия изолированной системы всегда будет возрастать. То же самое относится к программному обеспечению. В 1974 году Мейр М. Леман сформулировал второй закон эволюции ПО — закон увеличения сложности.

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

Понимание увеличения сложности

В качестве примера рассмотрим аспект моделей данных. Если в базе данных есть порядка 10 таблиц, это еще не делает ее сложной. Однако, если увеличить количество таблиц до сотни, сложность БД заметно повысится — более, чем в 10 раз. При этом сложность не зависит от количества элементов в системе напрямую, а скорее от количества возможных косвенных связей между элементами. И эта метрика стремительно растет! Она не экспоненциальная, не факториальная, а O(N^N). Все очень плохо:

2³ = 8 | 3! = 6 | 3³ = 27

2⁶ = 64 | 6! = 720 | 6⁶ = 46 656

2⁹ = 512 | 9! = 362 880 | 9⁹ = 387 420 489

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

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

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

Ограниченный контекст и слои

Подход к решению сложных задач в разработке ПО не является секретом. Вместо того, чтобы постепенно развивать систему как единое целое, ее следует разделить на отдельные области, которые связаны друг с другом. Это называется ограниченный контекст (bounded context) – ключевой, предметно-ориентированный, подход в проектировании ПО. Кроме того, система должна быть организована слоями, что позволит работать с концепциями более высокого уровня в ходе прогресса.

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

Что такое снижение сложности?

Снижение сложности стоит дорого. Одним из ключевых аспектов является то, что доменные области (bounded contexts) должны быть ограничены, а их элементы общие элементы должны отображаться (маппинг) друг на друга, но не ссылаться.

Простое введение модели «clients» и требование всем ее использовать не уменьшит сложности системы. Вместо этого необходима денормализация. В разных областях должны быть отдельные модели «clients», которые сопоставляются с моделью «clients» верхнего уровня. Такой подход позволит каждой области развиваться независимо, изолируя сложность в отдельном модуле.

Изоляция сложности окупает затраты усилий на отображение (маппинг)  одной и той же модели в различные доменные области.

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

Микросервисы лучше модулей?

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

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

Подводные камни при постепенных улучшениях

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

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

Следовательно, когда требуется разделить значительную часть программного обеспечения на области (bounded contexts) или слои, то когда вообще это можно сделать? Ведь это не согласуется с небольшими изменениями, которые обычно вносятся во время итеративной разработки. Из-за этого несоответствия подход с небольшим инкрементальным изменением способствует общему увеличению сложности системы.

Внедрение высокоуровневых итераций

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

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

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


  1. PereslavlFoto
    18.07.2023 09:31
    +2

    Давайте начнем с тривиального, но неоспоримого факта: программное обеспечение постоянно развивается… Заметным исключением является наборная система TeX, разработанная Дональдом Э. Кнутом.

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


    1. astenix
      18.07.2023 09:31
      +2

      TeX развивается, или LaTeX, XeLaTeX и тд?


      1. PereslavlFoto
        18.07.2023 09:31

        Действительно, вы правы. MS Office 2003 достиг полного совершенства и больше не развивается.


    1. dm_wrike Автор
      18.07.2023 09:31
      +1

      Надстройки (LaTeX), инструменты - да. Но не сам TeX. Его смысл наоборот, в том чтобы быть стабильным. Это не случайность что версия TeX сходится к pi: 3.141592653 - текущая, если верить Википедии.


      1. PereslavlFoto
        18.07.2023 09:31
        +1

        Пожалуй, надо ответить на это отдельной статьёй. Постараюсь сделать.



  1. z250227725
    18.07.2023 09:31
    +6

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


    1. dm_wrike Автор
      18.07.2023 09:31
      +1

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

      Разделение продукта на bounded contexts позволяет снизить сложность за счёт того что каждый такой домен ограничен. И это работает.

      Однако, такой подход приводит к тому что не все становится возможным сделать "как хочется", что может вызывать критику и сопротивление. Также, это долгосрочная история, что усложняет дело ещё сильнее.


  1. Sadler
    18.07.2023 09:31
    +1

    Я долго пытался бороться со сложностью, но на данный момент несколько опустил руки.

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

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

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