Блог Reshape Analytics
Консалтинг

Почему разрабатывать ПО – сложно или как жить проще

Программное обеспечение, в отличие от физического мира, непрерывно меняется и развивается. Каждый раз, когда мы, как пользователи, обновляем программное обеспечение на наших устройствах, мы вносим в него изменения: устранение ошибок, добавление новых функций, улучшение производительности и обеспечение совместимости с новыми технологиями. Принципы Agile утверждают, что изменения - даже поздние - являются ценными, поскольку они предоставляют конкурентное преимущество.
Одним из заметных исключений является TeX, система компьютерной вёрстки, разработанная Дональдом Кнутом. Кнут создал TeX с намерением создать что-то «стабильное», что «не будет меняться», и в некотором роде ему это удалось, так как основные версии TeX не обновлялись с 1990-х годов. Тем не менее, даже такая система как TeX, несмотря на свою стабильность, не избежала изменений. Например, различные расширения, такие как LaTeX, были разработаны для упрощения использования TeX и добавления новых возможностей.

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

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

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

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

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

Рост сложности можно сдерживать

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

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

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

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

Решение сложности в разработке программного обеспечения зачастую заключается не в простом масштабировании системы, а в ее разделении на более мелкие, управляемые части, связанные друг с другом. Это приводит нас к понятию «ограниченного контекста» или «bounded context» - ключевому принципу в проектировании ПО, особенно в таких подходах, как предметно-ориентированное проектирование (Domain-Driven Design).

Более того, система обычно организуется слоями, что позволяет разработчикам работать с более высокоуровневыми концепциями по мере того, как они прогрессируют в работе. Это, однако, не должно путаться с простым разделением системы на модули. Модульность важна, но разделение системы на отдельные области недостаточно, чтобы реально снизить сложность. Система по-прежнему остается единым целым, но теперь она более структурирована.

Снижение сложности

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

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

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

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

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

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

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

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

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

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

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

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

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

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