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

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

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

Эти проблемы подчиняются диалектическому принципу борьбы противоположностей - любое принятое решение сводится к вынужденному компромиссу и необходимости пожертвовать чем-то другим:

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

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

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

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

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

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

Если для анализа классов и взаимоотношений можно пытаться использовать формальную логику, то на уровне внутренних кишок приложения её польза сильно теряется. Например, проблемен инфраструктурный уровень, для которого трудно подобрать реальные аналогии: окно в десктопном приложении можно рассматривать как отображение, как поведение или как их сочетание. На ранних этапах такой мучительный анализ будет тратить огромное количество времени, не давая ничего взамен - требования быстро меняются и код может их не пережить. Это редуцирует отбор классов до "работает" и "плохо работает". Часть этих проблем пытается решить идеология Domain-driven design, но её полезность сильно снижается из-за частого отсутствия техзадания, специалистов предметных областей и четко очерченных методологий.

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

Аналогия свойств рассматривает элементы и их признаки, если обозначить признак по которому сравниваются элементы как P, то в случае двух элементов a и b её формула очень проста:
P1, P2, P3 (a) - элемент a обладает свойствами P1, P2, P3
P1, P2 (b) - элемент b обладает свойствами P1 и P2
----------------------
◊(P3 (b)) - вероятно (символ ◊ - оператор возможности), элемент b обладает и свойством P3

Аналогия отношений рассматривает отношения между элементами:
a R b - a находится к b в отношении R
c R d - с находится к d в отношении R
R ≡ R и отношения сходны, то
----------------------
◊(a R b)≡(c R d) - вероятно, и отношения а к b и с к d аналогичны. Сказать что-то об отношении элементов друг к другу можно в случае уже одного из видов аналогии отношений - аналогии простого отношения. Например, в случае аналогии простого отношения:
a R b
b R c
----------------------
◊(a R c)
Или же ◊((a R b) ∧ (b R c)→(a R c)), если заменить R, например, на "больше" или "меньше", то это похоже на транзитивность: если a больше b, а b больше c, то a больше c. Однако отличие в вероятностном выводе, хотя в случае аналогии простого отношения связь также не должна меняться. Если она изменяется, то в игру вступает уже аналогия степеней отношения.

Еще одна аналогия условной зависимости пытается выявить причину:
p→q - если p является причиной q
p≡r - а p аналогичен r
q≡s - и q аналогичен s, значит
----------------------
◊(r→s) - вероятно, что r причина для s.

Беспощадный формализм, но я все же должен был это упомянуть для полноты статьи. На практике с аналогией есть определенные проблемы, потому как она, как и любой метод, имеет и свои требования:

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

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

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

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

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

Если принять, что между классами нужно наладить взаимодействие в виде объектов, то на эту роль напрашивается поведенческий паттерн - медиатор. В случае композиции и прямого создания объекта он должен быть информационным экспертом в терминах GRASP. Но может иметь место случай, когда класс просто содержит объекты без какого-либо взаимодействия между ними. Таким образом, есть проблема между взаимодействием объектов и их структурой. Собственных атрибутов и операций нет, он выступает как агрегат, не делая ничего другого. Но такой термин опять вступает в конфликт с UML, где агрегация еще и предполагает независимое существование объектов, что для моделируемого класса может быть нехарактерным, путаясь с композицией, да и еще путаясь с агрегатами в DDD. Но вместе с агрегацией иногда вспоминается и делегирование и даже при публичном свойстве объект-хозяин всегда может контролировать доступ за счет многочисленных хаков. Делегат тоже можно рассмотреть как частный случай такой "агрегации" - если в объекте отсутствует метод, то он ищется в делегате, но при этом между делегатами не осуществляется особого взаимодействия.

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

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

На этой наивной и противоречивой модели можно сделать несколько допущений по аналогии с анализом популяции. Возьмем универсальные категории количества и качества:

  • Увеличение мелких классов - управление большим количеством классов потребует либо большего количества медиаторов, либо прямого переноса взаимодействия в контроллер. Последнее очень распространено для пилотной архитектуры, когда классы фреймворка или бизнес-логики прямо используются в контролирующих классах, при появлении дублирования заменяются службами\сервисами или чем-то подобным. Создавать промежуточное звено трудно из-за ограничений времени.
  • Увеличение медиаторов может быть из-за увеличения классов, сложной предметной области, большого количества решаемых задач или особенностей рефакторинга. Управление такой оравой потребует усложнения и укрупнения контроллеров, либо увеличения их количества.
  • Наконец, увеличение количества контроллеров потребует организации их взаимодействия: древовидная иерархия, шина сообщений и т.п.

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

Поисследуем вообще применимость свойств биологической системы к условной:

  • Воспроизводимость - в контексте объекта через инстанцирование, прототип, фабрики.
  • Саморегуляция - через управление с обратной связью на основе каких-то данных, паттерны Tester-Doer\Try-Parse.
  • Устойчивость - через управление исключительными ситуациями, защиты от изменений через Protected Variations в терминах GRASP.
  • Изменчивость - возможность реализации задачи различными способами. В контексте класса - наследование для специализации. Адаптер и декоратор.
  • Наследственность - в контексте приложений использование устоявшихся паттернов и приемов, в контексте класса также наследование, определение абстрактных контрактов.
  • Раздражимость - обработка прерываний или событий, паттерн наблюдатель.
  • Обмен веществ - обмен сообщениями с системой и между объектами.
  • и т.п.

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

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

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

Интересной может быть аналогия статических и динамических показателей популяции. Статические - численность, плотность, структурные - возрастной, половой, размерный (разные размеры особей). Динамические показатели определяются уже относительно каких-либо промежутков времени: рождаемость, смертность, скорость роста.

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

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

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

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

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

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

При этом экология вводит дополнительное деление, которое обычно не рассматривается в требованиях:

  • Факторы со строгой периодичностью - требования известны и архитектура высоко адаптирована для них.
  • Без строгой периодичности, повторяющиеся время от времени. Вероятно, высокие нагрузки: несмотря на тяжелую жизнь приложения, зависания и работу на границе возможностей - оно все же работает и выполняет свою задачу.
  • Без строгой периодичности и неопределенно – например, действия пользователя и неочевидная логика его работы. Несмотря на сценарии использования все равно найдется такая комбинация, которая положит приложение.
  • Факторы направленного действия, которые действуют постепенно и относительно долго. Приложение изменяется под изменение аудитории, требований и т.п. и эти изменения не нарушают функционала за счет наличия времени на разработку и тестирования.

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

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

Если вспомнить структуру выше, то адаптация может затрагивать все уровни, начиная от контроллеров и заканчивая самыми простыми классами:

  • Изменение строения классов изменяет медиаторы, а следом и контроллеры.
  • Изменение медиаторов изменяет контроллеры.
  • Изменение контроллеров изменяет поведение приложения.

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

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

Однако в контексте адаптации уже человека интересна аналогия двух типов:

  • Аллопластическая - изменение окружения.
  • Аутопластическая- изменение структуры под окружение.

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

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

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

Также напрашивается аналогия к производительности в высоконагруженных приложениях, в которых проводится агрессивная денормализация и прочие хаки. Без соответствия нагрузкам бессмысленна красота архитектуры, гибкость, защищенность и т.п. При этом нагрузка даже может и не относиться к функциональным требованиям. С другой стороны, такая лимитированность относительна и в игру вступает закон Митчерлиха: даже выдерживающее нагрузки приложение может иметь фундаментальные ошибки в юзабилити и в логике работы. Если допустить, что в разные моменты времени разные факторы становятся лимитирующими за счет своего сильного отклонения требований, то практическое применение закона сильно усложняется и мало чем отличается от смысла существующего термина bottleneck. С другой стороны, может играть роль само ограничение последующей адаптации лимитирующим фактором. Ошибки и кривое юзабилити - это проблемы все же работающей системы, а лимитирующий фактор делает приложение неработоспособным.

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

Аналогии достаточно примитивны, поскольку в самой экологии эти законы относятся к разным факторам, толерантности к физиологическим, лимитирования - к ресурсным и прочие нюансы, поэтому полная аналогия невозможна.

На еще более высоком уровне может быть интересным эволюционные стратегии для разных этапов жизни приложения по грубой аналогии с R / K‑отбором - для первых этапов жизни приложения рождается много крупных классов с широкой специализацией для изучение багов либ и проблем их логики, затем они умирают и начинают замещаться большим количеством мелких с узкой специализацией. Хотя это можно отнести и к процессам, происходящим в биоценозе, а переход озера в болото сравнить с легаси-кодом.

Подведу небольшие итоги:

  • В приложении трудно выделить четкую структуру из-за конфликта между собой разных терминов из UML, GRASP, SOLID и т.п.
  • Структурными и количественными характеристиками классов приложения нужно управлять для поиска компромиссов. Характеристики, вероятно, будут частично подчиняться законам эволюции или адаптации.
  • Вероятно, наиболее полезным был бы закон лимитирующего фактора из-за своей высокой критичности.
  • Адаптация интересна связью с требованиями, которые могут как улучшить систему, так и уничтожить её.
  • В случае фундаментальных принципов усматривается частичная аналогия с живой системой. Но множество интересных вопросов, например, о точке бифуркации и влиянии факторов на изменение системы достаточно сложны и их рассмотрение требует специальных знаний.

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