Перечитывая «Чистую архитектуру» (5): архитектура ПО

Tommy Cheese | · Время чтения: 5 мин

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

Прежде всего

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

Что такое архитектура ПО?

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

У проектирования архитектуры обычно две цели:

  • Упростить разработку, развёртывание, эксплуатацию и сопровождение компонентов.
  • Чтобы все эти виды работы продвигались легче, при проектировании нужно как можно дольше сохранять как можно больше вариантов выбора;

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

Цели архитектурного проектирования:

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

Главное в архитектурном проектировании — отделить друг от друга правила, а затем перегруппировать их по характеру изменений, проведя границы. Здесь можно опираться на SRP, CCP, CSP, SDP, SAP и другие принципы.

Обязанности архитектуры ПО

Архитектура отвечает за разработку, развёртывание, эксплуатацию и сопровождение.

В разработке архитектура должна упрощать создание системы. Поэтому разным командам нужны разные архитектурные решения. В определённой мере это отражает закон Конвея.

Почему закон Конвея связывает архитектуру со структурой команды? Подробнее — в видео:Закон Конвея: почему архитектура отражает структуру команды? — bilibili

В развёртывании архитектура должна обеспечивать простое развёртывание одним действием.

В эксплуатации:

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

Сопровождение обычно обходится дороже всего. Его затраты делятся на исследование и риск:

  • Исследование (spelunking): затраты на изучение существующей системы, чтобы определить лучшее место и способ добавления функции или исправления проблемы.
  • Риск (risk): при таких изменениях всегда возможно появление новых проблем. Эта вероятность и составляет затраты риска.

Сохранять варианты выбора

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

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

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

Автор объясняет важность отсрочки деталей на примере независимости от устройств. Современные ОС поддерживают множество устройств ввода-вывода. На заре вычислительной техники основным, почти единственным способом была перфолента, поэтому программисты естественным образом вплетали код её чтения и записи в системный код. С появлением магнитной ленты пришлось писать новый код, затем появились оптические диски… Чтобы избежать повторной разработки и слабой адаптируемости, устройства стали абстрагировать функциями. OS взаимодействует с ними через функции, а разработчики предоставляют конкретные реализации. Ввод-вывод становится плагином OS. Это также прообраз принципа открытости/закрытости: приветствовать 👏 добавление, сопротивляться 🥊 изменению!

Сохранять независимость

Хорошая архитектура должна обеспечивать достаточную независимость.

Разделение зависимостей помогает её сохранять.

Разделение бывает горизонтальным и вертикальным.

  • Горизонтальное разделение, или разделение по слоям: систему можно разделить на горизонтальные слои — UI, базу данных и так далее.
  • Разделение вариантов использования, или вертикальное разделение: одновременно со слоями выделяют вертикальные срезы по вариантам использования. Например, UI добавления заказа отделяют от UI удаления заказа.

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

Уровни разделения также различаются: исходный код, развёртывание и сервисы.

  • Уровень исходного кода: контролируются зависимости между модулями, а компоненты взаимодействуют вызовами функций. Такой режим называется монолитной структурой.
  • Уровень развёртывания: контролируются зависимости между единицами развёртывания, например файлами Jar. Компоненты могут взаимодействовать между потоками — обратите внимание, не по сети, — через Socket или разделяемую память.
  • Уровень сервисов: зависимости между компонентами сводятся к структурам данных, а взаимодействие происходит через сетевые пакеты. Например, микросервисы обмениваются данными через rpc или rest.

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

Теперь рассмотрим, как разделение влияет на независимость системы.

Значение разделения для независимости выполнения

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

Значение разделения для независимости разработки

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

Значение разделения для независимости развёртывания

При хорошем разделении реализации слоёв и конкретных вариантов использования можно даже заменять во время работы системы (hot-swap). Тогда для нового сценария достаточно добавить файлы jar или запустить сервисы, совершенно не затрагивая остальные части.

Что такое дублирование кода

Дублирование бывает двух видов:

  • Настоящее дублирование: повторения, которые нужно устранить.
  • Мнимое дублирование: похожий код, который в действительности развивается по разным путям, с разной скоростью и по разным причинам. Согласно CRP, код с разной частотой использования следует разделять по компонентам. Если не учитывать этого при чтении, мнимое дублирование легко принять за настоящее. Иногда такие повторы необходимы. Помните пример расчёта зарплаты?

(Конец пятой статьи)

comments powered by Disqus