Почему разрабатывать ПО действительно сложно?
Неизменные изменения
Давайте начнем с тривиального, но неоспоримого факта: программное обеспечение постоянно развивается — устаревает и обновляется, видоизменяется и дает дорогу новому.
Заметным исключением является наборная система 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) или слои, то когда вообще это можно сделать? Ведь это не согласуется с небольшими изменениями, которые обычно вносятся во время итеративной разработки. Из-за этого несоответствия подход с небольшим инкрементальным изменением способствует общему увеличению сложности системы.
Внедрение высокоуровневых итераций
Один из вариантов решения такой проблемы — внедрение высокоуровневых итераций, таких как квартальные циклы, в дополнение к спринтам. Чтобы поддерживать управляемую сложность при проектировании системы, эти длительные циклы должны решать задачи по развитию системы на высоком уровне.
Это решение кажется логичным, однако на практике не всегда легко осуществимо. Тем не менее, попытка внедрения высокоуровневых итераций в процесс разработки может оказаться хорошим инструментом для поддержания уровня сложности ПО на приемлемом уровне.