Когнитивная сложность — это понятие, описывающее сложность процесса познания и мышления. Оно используется в разных областях: в психологии оно характеризует индивидуальную способность к восприятию и обработке информации. Более высокая когнитивная сложность означает, что система (будь то человек или программа) требует больше усилий для понимания и может быть трудной в поддержке.
Когнитивная сложность при проектировании приложения часто возникает из‑за смешения архитектуры кода и архитектуры приложения. В большинстве случаев эти термины никак не разделены, а также эти термины не имеют однозначного толкования, как по содержанию так и по контексту использования. В практике и литературе эти понятия часто используются как синонимы или в пересекающихся контекстах, что приводит к неоднозначности. В зависимости от контекста (например, обсуждение микросервисов, монолитов, паттернов проектирования или рефакторинга), один и тот же термин может обозначать как уровень организации кода, так и более высокий уровень организации приложения или системы. В профессиональной литературе и стандартах (например, TOGAF, ArchiMate) архитектура программного обеспечения охватывает оба аспекта и организацию кода, и организацию приложения, что еще больше стирает границы между этими понятиями.
Процессно-ориентированная архитектура приложения
В рамках этой статьи термин «Архитектура приложения» рассматривается исключительно как процессно‑ориентированная архитектура приложения.
Процессно‑ориентированная архитектура — это подход, при котором элементами архитектуры являются физические процессы ОС, а не абстрактные программные модули.
В такой архитектуре компоненты приложения описываются как отдельные процессы, а не как абстрактные функциональные модули внутри одного процесса.
Каждый процесс выполняет свою задачу и взаимодействует с другими процессами через механизмы межпроцессного взаимодействия (IPC): каналы, сокеты, очереди сообщений, разделяемую память и так далее.
Отличие процессно-ор��ентированной архитектуры (где элементы — физические процессы ОС) от сервис-ориентированной архитектуры (SOA)
Основное отличие процессно‑ориентированной архитектуры (где элементы — физические процессы ОС) от сервис‑ориентированной архитектуры (SOA) заключается в уровне абстракции, способе взаимодействия и области применения:
В процессно‑ориентированной архитектуре элементы — это физические процессы ОС, которые выполняют конкретные задачи и взаимодействуют через механизмы IPC (межпроцессное взаимодействие).
В SOA элементы — это сервисы, которые могут быть реализованы как процессы, потоки, или даже функции, но главное — они предоставляют бизнес‑функции через стандартизированные интерфейсы и не обязательно соответствуют отдельным процессам ОС.
В процессно‑ориентированной архитектуре взаимодействие между компонентами обычно происходит через IPC, сокеты, очереди сообщений и так далее, что связано с особенностями ОС.
В SOA взаимодействие между сервисами осуществляется через стандартные протоколы (например, HTTP, SOAP, REST), и часто используется шина сервисов (ESB) для управления взаимодействием.
Процессно‑ориентированная архитектура чаще применяется в системах, где требуется высокая изоляция, отказоустойчивость и управление ресурсами на уровне ОС (например, микроядерные ОС, распределённые системы).
SOA ориентирована на бизнес‑процессы и интеграцию приложений, обеспечивая многократное использование сервисов, гибкость и масштабируемость в корпоративных средах.
В процессно‑ориентированной архитектуре управление ресурсами и масштабированием происходит на уровне ОС, а сервисы могут быть жестко привязаны к конкретным процессам.
В SOA сервисы могут быть независимо развёрнуты, масштабированы и управляться централизованно через ESB или другие средства управления сервисами.
Таким образом, процессно‑ориентированная архитектура фокусируется на физических процессах ОС и их взаимодействии, тогда как SOA — на бизнес‑сервисах, стандартизированных интерфейсах и интеграции приложений на уровне бизнес‑процессов.
Итак, используя процессно‑оринетированную архитектуру в качестве архитектуры приложения, критически рассмотрим типичные высказывания, которые сложны для понимания и, к сожалению, часто воспринимаются как некий малосодержательный фон повествования.
Примеры высказываний, возникающих при смешении архитектуры кода и архитектуры приложения:
«Приложение использует микросервисную архитектуру, потому что у нас много классов.»
Правильно: «Приложение использует микросервисную архитектуру, потому что его компоненты развернуты как независимые сервисы, взаимодействующие по сети. Количество классов в коде не определяет архитектуру приложения.»«Наше приложение построено по принципу Clean Architecture, потому что у нас есть слои Presentation, Business и Data.»
Правильно: «Код приложения организован по принципу Clean Architecture, если слои кода соответствуют принципам инверсии зависимостей и чёткого разделения ответственностей. Архитектура приложения — это способ развертывания и взаимодействия компонентов, а не только структура кода.»«Мы используем архитектуру приложения MVC, потому что у нас есть контроллеры, сервисы и репозитории.»
Правильно: «Код приложения использует паттерн MVC, если разделение на контроллеры, представления и модели соответствует принципам MVC. Архитектура приложения — это способ организации компонентов приложения в среде выполнения, а не только структура классов.»«Наше приложение имеет архитектуру микросервисов, потому что у нас много модулей.»
Правильно: «Приложение имеет архитектуру микросервисов, если компоненты развернуты как отдельные процессы, взаимодействующие по сети. Количество модулей в коде не определяет архитектуру приложения.»«Мы используем архитектуру приложения N‑Tier, потому что у нас есть папки Presentation, Business и Data.»
Правильно: «Код приложения организован по принципу N‑Tier, если слои кода соответствуют принципам разделения ответственностей. Архитектура приложения — это способ развертывания и взаимодействия компонентов, а не только структура папок.»
Кроме проблем с коммуникацией существуют и другие проблемы.
Проблемы при смешении:
Разработчики могут применять архитектурные решения на уровне кода (например, сложные паттерны или декомпозицию классов), не учитывая общую архитектуру приложения, ��то приводит к излишней сложности и неочевидным зависимостям.
С другой стороны, принятие архитектурных решений без учета возможностей и ограничений кода может привести к непрактичным, трудно поддерживаемым решениям.
Смешение затрудняет понимание системы, увеличивает порог входа для новых участников команды и усложняет поддержку и развитие.
Как минимизировать когнитивную сложность:
Четко разделять уровни архитектурных решений: от глобальной структуры до деталей реализации.
Обеспечивать согласованность между архитектурой приложения и кода, чтобы изменения на одном уровне не нарушали целостность другого.
Таким образом, осознанное разделение архитектуры кода и архитектуры приложения — ключ к снижению когнитивной нагрузки и повышению качества архитектуры.
Далее разбираемся подробно.
Архитектура кода и архитектура приложения
Разница между архитектурой кода и архитектурой приложения заключается в уровне абстракции и контексте, в котором рассматриваются границы и структура системы.
Архитектура кода — это организация исходного кода на уровне логических границ, выраженных структурой файлов, модулей, классов, функций и других конструкций языка программирования. Эти границы определяются принципами проектирования, паттернами, стилем кода и стандартами организации проекта. Например, разделение на слои (presentation, business logic, data access), модульность, инкапсуляция, декомпозиция на микросервисы или компоненты — всё это проявляется на уровне исходного кода и не зависит от того, как код будет выполняться.
Архитектура приложения — это организация исполняемого процесса, то есть того, как компоненты системы взаимодействуют между собой в момент выполнения. В исполняемом процессе границы определяются технически: память (код, данные в стеке, данные в куче), процессы, потоки, ресурсы ОС и так далее Эти границы не обязательно соответствуют логическим границам исходного кода — например, разные модули могут быть загружены в один процесс, а один логический компонент может быть разнесён по нескольким процессам или машинам.
Один и тот же код может дублироваться в разных процессах, даже если в исходной кодовой базе он описан один раз. Например, библиотека вспомогательных функций может быть подключена к нескольким процессам (например микросервисам), и каждый из них будет использовать её независимо. Это не означает, что код физически дублируется в исходниках — он может быть собран в виде отдельной библиотеки и подключаться как зависимость. Однако на уровне архитектуры приложения каждый процесс, использующий эту библиотеку, будет иметь свою копию в своём окружении.
Когда код запускается и становится исполняемым процессом, логические границы исходного кода теряют своё значение, если только они не реализованы через технические границы (например, разные процессы, контейнеры, сервисы). Внутри процесса все данные и код существуют в едином адресном пространстве, и разделение происходит только по типу памяти (стек, куча) и по способу доступа к данным. Исходный код сохраняет свою структуру только как логическая модель, но не как физическая граница выполнения.
При проектировании архитектуры приложения важно понимать, что один и тот же код может быть задействован в нескольких процессах, и это влияет на масштабируемость, обновление и отладку системы.
Архитектура кода помогает минимизировать дублирование и обеспечить переиспользование, но архитектура приложения определяет, где и как этот код будет использоваться.
Смешение этих уровней приводит к когнитивной сложности — разработчики могут не видеть, как изменения в одной части архитектуры повлияют на другую.
Сравнительная таблица
Критерий | Архитектура кода | Архитектура приложения |
|---|---|---|
Уровень абстракции | Логический, исходный код | Технический, исполняемый процесс |
Границы | Файлы, модули, классы, функции | Процессы, потоки, память, ресурсы |
Структура | Документы, конструкции языка | Объекты ядра ОС: память, процессы, ресурсы |
Таким образом, архитектура кода это логическая модель, а архитектура приложения — техническая реализация этой модели в исполняемой среде.
Архитектура приложения
Архитектура приложения — это не просто набор всего исходного кода приложения, а организация исполняемых компонентов системы. Если архитектура кода строится из атомарных сущностей языка программирования, таких как операторы, функции и данные, то архитектура приложения строится из атомарных сущностей среды исполнения кода, объектов ядра операционной системы — процессов.
Процесс является минимальной единицей, внутри которой реализуется функциональность приложения, полностью или частично, и обеспечиваются изоляция, управление ресурсами и безопасность.
Архитектура кода фокусируется на логических границах: функции, классы, модули. Эти границы определяют, как организован исходный код, но не влияют напрямую на выполнение приложения — неважно на сколько метод��в в коде разложен алгоритм сортировки он будет выполнять свою работу. Архитектура приложения, напротив, определяет, как логические функции реализуются и взаимодействуют в реальной среде — через процессы, контейнеры, микросервисы.
Примеры:
В монолитной архитектуре вся логика приложения может быть реализована в одном процессе, и хотя логические функции выделены через структуру кода, но технически работают в рамках одного адресного пространства.
В микросервисной архитектуре каждая функция, а чаще набор функций, реализуется в отдельном процессе, что обеспечивает независимость, масштабируемость и отказоустойчивость для приложения в целом.
Процесс - атомарная единица архитектуры приложения
Процесс — это экземпляр исполняемой программы, который получает выделенные ресурсы операционной системы: память, файловые дескрипторы, потоки выполнения. Каждый процесс изолирован от других, что обеспечивает независимость и устойчивость системы. Внутри процесса могут быть реализованы любые логические функции приложения, но сам процесс определяет техническую границу, за которой не может быть прямого доступа к ресурсам другого процесса без специальных механизмов межпроцессного взаимодействия.
На практике часто один процесс выполняет несколько логических функций, которые можно выделить на уровне архитектуры кода. Например, в монолитном приложении один процесс может содержать слои представления, бизнес‑логики и доступа к данным. Такая организация кода позволяет быстро разрабатывать и запускать весь функционал приложения внутри одного процесса, но при масштабировании и необходимости независимого управления функциями возникает потребность в разделении функционала приложения на отдельные процессы.
Проектируя архитектуру приложения, архитектор обязан рассчитывать его работу под пиковую нагрузку, определять допустимый уровень задержек, планировать масштабирование процессов — горизонтальное (увеличение числа процессов) или вертикальное (увеличение ресурсов на процесс).
Проектирование процессов
Проектирование процессов — это ключевой этап архитектуры приложения, на котором определяются не только функции, но и технические характеристики каждого исполняемого компонента приложения.
При проектировании процесса учитываются четыре основных аспекта:
группа выполняемых функций
максимальная мощность процесса
способы взаимодействия с процессом
количество доступных ресурсов
Группа функций процесса
При проектировании процесса сначала определяется набор логических функций, которые он будет выполнять. Это могут быть задачи бизнес‑логики, обработка запросов, работа с данными или взаимодействие с внешними системами. Функции должны быть сгруппированы логически: например, все операции с пользователями могут быть вынесены в отдельный процесс, а обработка платежей — в другой. Такое разделение упрощает масштабирование, тестирование и подд��ржку.
Максимальная мощность процесса
Важно заранее оценить, какую нагрузку будет испытывать процесс: сколько CPU, памяти, дискового пространства и других ресурсов ему потребуется.
Это позволяет:
избежать перегрузки системы и сбоев из‑за нехватки ресурсов.
оптимизировать использование железа, особенно при работе в контейнерах или облаке.
планировать масштабирование: если нагрузка растёт, можно добавить больше процессов или перераспределить нагрузку между ними.
Способы взаимодействия с процессом
Процесс не существует изолированно — он должен взаимодействовать с другими процессами, внешними системами и пользователями.
Способы взаимодействия определяются:
интерфейсами (API, очереди сообщений, сокеты).
протоколами обмена данными.
механизмами синхронизации и обработки ошибок.
Количество доступных ресурсов
Основными ограничениями для любого процесса служат ресурсы, предоставляемые операционной системой:
CPU — количество операций, вычислительная мощность, доступная для процесса.
Память — объём оперативной памяти, необходимый для хранения данных и выполнения вычислений.
Диск — пространство для хранения файлов, журналов, временных данных; скорость доступа к данным.
Сеть — пропускная способность и доступность сетевых ресурсов для обмена с другими процессами и системами.
Ограничения ресурсов определяют, сколько полезной работы может выполнить процесс в единицу времени, то есть его производительность и какую нагрузку система способна выдержать без деградации производительности.
Ограничения по ресурсам и производительности влияют на то, как архитектура приложения делится на процессы, какие механизмы балансировки и мониторинга внедряются, как реализуются механизмы отказоустойчивости и защиты от перегрузки.
Проектирование процессов — это оптимизация использования ограниченных ресурсов для достижения максимальной полезной работы, с учётом требований к скорости и надёжности системы.
Архитектура кода
Архитектура кода определяет функционал решения, то есть набор задач, которые должна выполнять система. Она описывает, какие функции реализуются, как они объединяются в логические группы и как эти группы взаимодействуют между собой: например, через вызовы функций, интерфейсы, события или паттерны проектирования.
Группировка функций и их распределение по процессам
При проектировании архитектуры кода разработчики группируют функции по смыслу, ответственности или по принципу связности (например, все операции с пользователями — в один модуль, работа с базой — в другой). Эти группы функций могут быть реализованы в рамках одного процесса (например, в монолитном приложении) или распределены по нескольким процессам (например, в микросервисной архитектуре).
Архитектура кода сильно зависит от архитектуры приложения — разделение функционала на отдельные выполняемые процессы, напрямую влияет на архитектуру кода.
Особенно сильно архитектура приложения влияет на способы взаимодействия между группами функций:
внутрипроцессное взаимодействие
межпроцессное взаимодействие — требует дополнительного кода и использования специальных библиотек и компонентов
Таким образом, архитектура кода задаёт логическую структуру решения, а её реализация на уровне процессов определяет, как эта структура будет работать в реальной среде, с учётом ограничений и требований к производительности.
Проектирование кодовой базы
Архитектура кода строится с учётом ключевых ограничений, определяющих, как реализуется и поддерживается система. Эти ограничения включают выбранный технологический стек и квалификацию команды разработчиков.
Ограничения технологического стека
Выбранный стек технологий — языки программирования, библиотеки, фреймворки, системы хранения данных, кэширования, поиска и другие специализированные инструменты — задаёт рамки, в которых возможно проектирование и реализация кода.
Архитектура кода должна учитывать:
совместимость и ограничения используемых технологий.
возможности и ограничения библиотек и фреймворков.
ограничения на выбор решений для хранения и обработки данных.
Например, если выбрана микросервисная архитектура приложения, каждый сервис может использовать свой стек технологий, но при этом возрастает сложность координации и интеграции между сервисами. В монолите чаще требуется единый и согласованный стек для всех компонентов.
Ограничения квалификации команды
Архитектура кода также определяется уровнем знаний и навыков членов команды. Утверждение, что «код пишется для человека, а не для компьютера», означает, что важна читаемость, поддерживаемость и понятность кода.
Архитектура кода должна быть:
доступна для понимания и модификации текущей командой
соответствовать опыту и навыкам разработчиков, чтобы избежать технического долга и ошибок
Если команда не владеет определённым языком или инструментом, использование его в архитектуре кода приведёт к проблемам с поддержкой и развитием системы. Поэтому архитектура кода всегда должна учитывать не только технические, но и человеческие ограничения.
Взаимное влияние архитектуры приложения и архитектуры кода
Архитектура приложения и архитектура кода тесно связаны и оказывают взаимное влияние. Архитектура приложения определяет, как компоненты взаимодействуют в среде выполнения, а архитектура кода — как эти компоненты реализованы и организованы внутри.
Примеры взаимного влияния
Масштабируемость приложения и модульность кода
Если архитектура приложения предусматривает масштабирование (например, микросервисы), то код должен быть организован так, чтобы каждый сервис был автономным модулем с минимальной связностью и высокой когезией. Это влияет на выбор структуры кода: разделение на независимые пакеты, использование API, изоляция бизнес‑логики и данных. Если код не модульный, масштабирование приложения становится сложным и дорогостоящим.
Производительность кода и состав приложения
Архитектура приложения может предусматривать отдельные сервисы кэширования, использование очередей сообщений и оптимизацию взаимодействия между процессами для снижения задержек, но если оптимизация кода привела к значительному увеличению производительности приложения, то появляется возможность полностью исключить указанные дополнительные сервисы или снизить количество ресурсов для их работы.
