Три кита сложных систем

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

В своей книге «Совершенный код» С. Макконнелл формулирует главный технический императив разработки ПО — борьба со сложностью [1]. Каждый раз, останавливаясь и просматривая проделанную работу, задаешься вопросом: как можно упростить то, что было сделано? В противном случае последующее развитие приведет к тому, что разрабатываемая система станет неуправляемой. Ответ на этот вопрос не всегда очевиден. Разработчик может принять много различных решений. Но какие из них правильные? Как на раннем этапе распознать, что ты движешься в нужном направлении?

Наблюдая за сложными системами, которые создала природа, не перестаешь восхищаться, с одной стороны, видимой сложностью этих систем, а с другой стороны — их лаконичностью. Простой расчет объема информации, которым закодировано строение человека, показывает всего 1.5 Гб, а отличие одного человека от другого в этом объеме составляет доли процента!

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

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

Процитирую определение сложной системы:

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

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

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

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

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

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

Иерархичность. В большинстве сложных объектов можно выделить уровни, на которых находятся сущности одного класса. Очень простой пример — книга. Уровни иерархии объектов — буквы, слова, предложения, абзацы, главы и т. д. В данном случае объекты более высокого уровня включают низкоуровневые объекты. Однако возможны и другие варианты. Например, иерархия управления. Информация или управляющее воздействие передается последовательно от уровня к уровню. Такое мы можем наблюдать во многих социальных структурах. В коре человеческого мозга можно выделить 6 слоев, отвечающих за определенные функции [4]. Отражение данного принципа при проектировании ПО — очень распространенная слоеная архитектура [3]. Иерархию вполне можно рассматривать как логическое разделение классов объектов.

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

Примеры использования принципа типизации при разработке ПО: микроядерная архитектура, в которой расширение функционала реализуется при помощи плагинов с типовым интерфейсом, шаблоны проектирования «Стратегия», «Цепочка обязанностей», «Абстрактная фабрика» и т. д. [5]

Какие же практические следствия из описанных принципов?

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

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

Большим подспорьем при разработке являются шаблоны проектирования ([5],[8]) — проверенные временем решения, которые можно использовать как крупные строительные блоки вашей системы.

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

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

Любопытно (но вполне логично), что на разных уровнях иерархии работают одни и те же законы: распределение толщины стволов в лесу соответствует распределению толщины веток дерева, растущего в этом же лесу или «правило Леонардо»: «Толщина всех веток дерева на любой его высоте, сложенная вместе, дает толщину ствола» [6], но это уже отдельная история.

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

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

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

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

Заключение

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

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

Литература

  1. Стив Макконнелл, Совершенный код.
  2. https://en.wikipedia.org/wiki/Complex_system
  3. Mark Richards. Software Architecture Patterns.
  4. Об интеллекте. Джефф Хокинс.
  5. Шаблоны проектирования. Э. Гамма Р. Хелм Р. Джонсон Дж. Влиссидес.
  6. Лес дробной размерности, lenta.ru
  7. How Much Information is Stored in the Human Genome? Evgeniy Grigoryev.
  8. Мартин Фаулер. Шаблоны корпоративных приложений.