Курс Essential Architecture #Code

Alexander Polomodov
12 min readOct 1, 2021

--

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

Рис. “Титульный слайд лекции”

И начнем мы рассмотрение с того, что бывает если разрабатывать в лоб. Такая разработка выглядит как итерации цикла “идея — требования — разработка — деплой” и в итоге она часто приводит к появлению спагетти-кода. Это название — отсылка к запутанности и сложности решения, когда расширять функциональность такого кода сложно, а рефакторить больно … Такой код обычно появляется в силу неопытности команды разработки или если на команду сильно давят по срокам и требуют результатов в неадекватные сроки.

Рис. “Итоги разработки в лоб”

Возникает вопрос, а всегда ли такой подход плох? На самом деле нет — если мы пишет так прототипы, то все ок. А что если мы пишем не прототипы? Тогда нам надо контролировать сложность.

Рис. “Контроль сложности — способ не допустить спагетти-код”

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

Рис. “План обсуждения”

И начнем мы с основ, а именно парадигм программирования.

Рис. “Парадигмы программирования”

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

Рис. “Парадигмы программирования — сравнительная таблица”

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

Дальше мы переходим к принципам модульности

Рис. “Принципы модульности”

И начать стоит с определения, которое звучит достаточно просто

Рис. “Определение модульности”

Но остается вопрос о том, как нам измерять уровень модульности. Так как мы помним крылатую фразу Билла Хьюлетта (сооснователя Hewlett-Packard): «Нельзя управлять тем, что невозможно измерить, но всего, что измеримо, можно достичь». В итоге, есть 2 основные метрики cohesion и coupling, определения которых приведены ниже.

Рис. “Метрики Cohesion и Coupling”

Если говорить кратко, то cohesion — это про связи внутри компонента, а coupling — про связи между компонентами. Высокое значение метрики cohesion — это хорошо, а высокое значение coupling — это плохо.

Есть еще одна интересная метрика — connascence, которая является объектно-ориентированной версией coupling.

Рис. “Метрика Connascence”

На диаграмме выше видно, что connascence бывает двух видов: статическая и динамическая. Причем снизу вверх приведены варианты connascence по степени нарастания этой метрики. И чем она больше, тем хуже для системы с точки зрения maintainability. Иногда наступает момент, когда пора рефакторить код и тогда мы действуем в обратном порядке — снизу вверх:)

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

Рис. “Принципы организации модулей”

Эти принципы достаточно известны и ниже представлены определения каждого из принципов, которых всего 5.

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

Рис. “Связь SOLID с архитектурой”

Ну а дальше мы идем к следующей абстракции, а именно к паттернам проектирования.

Рис. “Паттерны проектирования”

Паттерн или шаблон проектирования — это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста. Шаблоны проектирования получили популярность после выхода классической книги “Design Patterns” от Банды четырех в 1994. Коллектив авторов вдохновлялся классическими работами архитектора Кристофера Александра про шаблоны проектирования в градостроительстве и архитектуре (работы 1970х годов). До того момента в разработке основной акцент делался на вычислительных паттернах ака алгоритмах (можно вспомнить монументальный труд Кнута «Искусство программирования»)

В книге “Design Pattern” приводились следующие паттерны, которые были разбиты по трем категориям. Сейчас эти паттерны известны многим инженерам — они фактически стали неким общим набором концепций, которые позволяют говорить про проектирование сервисов на одном языке. Эти паттерны говорят о том, как организовать структуру модулей (классов) и поэтому они находятся на уровень выше принципов организации модулей, но ниже принципов организации компонентов.

Рис. “Классы паттернов из книги Gang of Four (GoF)”

Дальше перейдем к принципам организации компонентов

Рис. “Принципы организации компонентов”

Эта часть лекции основана на книге “Clean Architecture” Дяди Боба, в которой они и были изложены. Всего Uncle Bob выделяет шесть принципов, которые разбиты по двум категориям: принципы cohesion и принципы coupling. Ниже перечислены все эти принципы

Рис. “Принципы организации компонентов ”

Начнем мы с принципов, относящихся к cohesion части, и конкретно с Reuse/Release Princimple (REP).

Рис. “Принцип Reuse/Release Equivalent Principle (REP)”

В современной реальности понятный релизный процесс и версионирование компонентов — это must have:) Можно посмотреть на работу стандартных пакетных менеджеров в популярных языках программирования и увидеть, что это есть из коробки. Ну а как группировать модули в компонент — это уже зона ответственности maintainer’ов компонентов. И этот принцип рекомендует группировать для удобства переиспользования.

Дальше идет принцип Common Closure Principle (CCP), который рекомендует объединять модули в компонент для удобства разработки компонентов — при такой группировке у нас правки зачастую локализованы внутри компонентов.

Рис. “Принцип Common Closure Principle (CCP)”

Следующий принцип — Common Reuse Principle (CRP), который в отличие от двух предыдущих подсказывает не как объединять модули в компонент, а как их правильно разделять на разные компоненты.

Рис. “Принцип Common Reuse Principle (CRP)”

Теперь, когда мы рассмотрели три компонентных принципа из категории cohesion, мы можем перейти к диаграмме противоречий, которые возникают при попытке использовать эти принципы. Эта диаграмма представлена на картинке ниже.

Рис. “Диаграмма противоречий”

Если мы забиваем на Reuse/Release Equivalent Principle, то пользователям наших компонентов становится сложно их переиспользовать — тупо не ясно как это сделать без налаженного и понятного процесса релизов и версионирования компонент.

Рис. “Баланс смещен — слишком сложно переиспользовать”

Если мы забиваем на Common Closure Principle, то мейнтейнерам компонент при их доработке требуется внести изменения в слишком большое количество мест — они умирают от непродуктивной нагрузки:) И появляется желание укрупнить и объединить часть компонентов.

Рис. “Баланс смещен — слишком много компонентов меняются (сложно саппортить и развивать компоненты)”

Если мы забиваем на Common Reuse Principle, то появляется большое количество релизов, которые вызваны небольшими изменениями в разных частях крупных компонентов. Это приводит к боли у пользователей компонентов, когда они видят постоянные обновления версий компонентов и должны постоянно читать changelogs, чтобы понять нужны им эти обновления или нет.

Рис. “Баланс смещен — слишком много ненужных релизов”

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

Рис. “Общая диаграмма противоречий”

А теперь пришло время перейти к следующим трем принципам компонентов из группы, относящейся к coupling. И первый принцип Acyclic Dependencies Principle посвящен отсутствию циклов в графе зависимостей графа.

Рис. “Принцип Acyclic Dependencies Principle (ADP)”

Принцип настолько простой, что его проверку легко автоматизировать и впилить в шаги CI/CD пайплайна сборки вашего приложения.

Дальше мы переходим к более интересному принципу Stable Dependencies Principle, который говорит про направление зависимостей, которые должны вести в сторону более устойчивых компонент. На рисунке ниже дано определение того, как считать Instability (нестабильность) компонентов, а также показан пример того, как должен выглядеть граф зависимостей в идеальном случае.

Рис. “Принцип Stable Dependencies Principle (SDP)”

И последний принцип из этой группы — Stable Abstraction Principle, который говорит про то, что компонент должен быть настолько же абстрактным, насколько он стабилен. На рисунке ниже представлен алгоритм расчета метрики абстрактности компонентов через количество абстрактных классов в компоненте по отношению к общему количеству классов

Рис. “Принцип Stable Abstraction Principle (SAP)”

Ну и теперь мы можем визуализировать Stable Abstraction Principle как показано на рисунке ниже. Суть в том, что хорошие компоненты должны группироваться в районе The Main Sequence. Кроме того есть две особые зоны: зона боли и зона бесполезности. В зоне боли у нас идет завязка на конкретные реализации внутри стабильных компонентов. Это приводит к боли при попытке расширить систему. А в зоне бесполезности у нас есть нестабильные абстрактные компоненты, которые в принципе не имеют смысла — зачем нужны интерфейсы, которые никто не использует?

Рис. “Принцип Stable Abstraction Principle (SAP) — диаграмма”

Общий список принципов организации компонентов приведен ниже.

Рис. “Принципы организации компонентов ”

Теперь мы готовы к обсуждению следующего уровня абстракции — библиотек.

Рис. “Библиотеки”

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

Рис. “Фреймворки”

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

Вот наконец-то мы и добрались до финального уровня абстракции — целого приложения, которое должно соответствовать 12 факторам:)

Рис. “12 factor app”

Давайте рассмотрим подробнее эти факторы, так как они во многом относятся к архитектурным требованиям и требованиям по работе с теми или иными зависимостями. И начнем мы с первого принципа, который касается кодовой базы.

Рис. “Первый фактор — кодовая база”

Этот принцип является базовым в мире с современными VCS системами и подходами к CI/CD. В общем, это теперь скорее гигиенический принцип и если он не соблюдается, то это знак … или запах:)

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

Рис. “Второй фактор — зависимости”

Третий фактор относится к тому, как стоит конфигурировать приложения. Когда-то давно часто параметры конфигурации зашивали прямо в приложение. Теперь эту конфигурацию выносят в среду выполнения, например, через env variables или configMaps или Secrets, если мы говорим про Kubernetes.

Рис. “Третий фактор — конфигурация”

В четвертом факторе идет речь про работу с backing services, которые рекомендуют считать подключаемыми ресурсами, которые мы конфигурируем при старте приложения. Такими сторонними службами могут базы данных, внешние API, объектные хранилища и так далее. В общем и целом такая работа со сторонними службами опять же говорит о том, что с внешними зависимостями нашего приложения надо работать осмысленно.

Рис. “Четвертый фактор — backing services”

Пятый принцип закладывает основы правильной работы современных CI/CD пайплайнов, в которых предполагается строгое разделение стадий сборки и выполнения.

Рис. “Пятый фактор — Сборка, релиз, выполнение”

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

Рис. “Шестой фактор — Процессы”

Седьмой фактор говорит о том, что приложение должно само слушать обращения на определенном порту, а не ожидать, что для этого будет развернут какой-то веб-сервер (камень в огород python/php). Принцип очень простой и понятный.

Рис. “Седьмой фактор — привязка портов”

Восьмой фактор говорит о том, что приложения стоит масштабировать при помощи процессов. Если приложение stateless, то это сделать достаточно просто:)

Рис. “Восьмой фактор — параллелизм”

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

Рис. “Девятый фактор — утилизируемость”

Десятый фактор предлагает делать различные окружения ( dev, test, stage, prod) максимально похожими. А кроме этого он предлагает убрать различия во времени (CI/CD), персонале ( devops/SRE)и инструментах (похожесть сред).

Рис. “Десятый фактор — паритет разработки/работы приложения”

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

Рис. “Одиннадцатый фактор — журналирование”

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

Если у вас миграции накатываются руками, то это признак того, что что-то не ладно в датском королевстве.

Рис. “Двенадцатый фактор — задачи администрирования”

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

Рис. “Рассмотренные вопросы”

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

Источники

— Книга "Software Architecture: The Hard Parts" и краткий обзор
— Книга "Fundamentals of Software Architecture" и краткий обзор
— Книга "Clean Architecture" и краткий обзор в двух частях 1, 2
— Книга "Design Patterns" и краткий обзор
— Книга "Evolutionary Architecture" и краткий обзор
— Книга "Distributed Systems, 4th Edition" и краткий обзор
— Книга "A Philosophy of Software Design" и краткий обзор
— Манифест 12 factor app
Wiki статья про Coupling
Wiki статья про Connascence
Wiki статья про Library
Wiki статья про Framework

--

--

Written by Alexander Polomodov

Technical Director @T-Bank, Department of client interfaces, marketing and engagement.

Responses (2)