Курс Essential Architecture #Code
Я уже рассказывал про вводный курс по архитектуре. В этой статье будет расшифровка лекции про подходы к организации кода в приложении и про характеристики хорошего приложения в целом.
И начнем мы рассмотрение с того, что бывает если разрабатывать в лоб. Такая разработка выглядит как итерации цикла “идея — требования — разработка — деплой” и в итоге она часто приводит к появлению спагетти-кода. Это название — отсылка к запутанности и сложности решения, когда расширять функциональность такого кода сложно, а рефакторить больно … Такой код обычно появляется в силу неопытности команды разработки или если на команду сильно давят по срокам и требуют результатов в неадекватные сроки.
Возникает вопрос, а всегда ли такой подход плох? На самом деле нет — если мы пишет так прототипы, то все ок. А что если мы пишем не прототипы? Тогда нам надо контролировать сложность.
В этой лекции мы рассмотрим целую группу разнообразных абстракций, которые помогают в этом деле. Все они приведены на рисунке ниже, причем мы оставляем вне рамок обсуждения уровень Hardware и распределенных систем, а остальное рассмотрим отдельно по мере повышения уровня абстракции.
И начнем мы с основ, а именно парадигм программирования.
Интересные нам парадигмы включают в себя структурное, объектно-ориентированное и функциональное программирование. Все эти парадигмы мы рассматриваем в разрезе вопросов: что каждая их них добавляет, что отнимает и как связана с архитектурой. В итоге, появляется сводная табличка, приведенная на рисунке ниже.
Нам с точки зрения архитектуры важно, что структурная парадигма дает основу для создания алгоритмических основ наших модулей, объектно-ориентированная — позволяет просто работать с абстракциями, полиморфизмом и инверсией зависимостей, а функциональная отлично ложится на подходы к работе с данными.
Дальше мы переходим к принципам модульности
И начать стоит с определения, которое звучит достаточно просто
Но остается вопрос о том, как нам измерять уровень модульности. Так как мы помним крылатую фразу Билла Хьюлетта (сооснователя Hewlett-Packard): «Нельзя управлять тем, что невозможно измерить, но всего, что измеримо, можно достичь». В итоге, есть 2 основные метрики cohesion и coupling, определения которых приведены ниже.
Если говорить кратко, то cohesion
— это про связи внутри компонента, а coupling
— про связи между компонентами. Высокое значение метрики cohesion
— это хорошо, а высокое значение coupling
— это плохо.
Есть еще одна интересная метрика — connascence
, которая является объектно-ориентированной версией coupling
.
На диаграмме выше видно, что connascence
бывает двух видов: статическая и динамическая. Причем снизу вверх приведены варианты connascence
по степени нарастания этой метрики. И чем она больше, тем хуже для системы с точки зрения maintainability
. Иногда наступает момент, когда пора рефакторить код и тогда мы действуем в обратном порядке — снизу вверх:)
Дальше переходим к принципам организации модулей (классов), которые складываются в солидный акроним.
Эти принципы достаточно известны и ниже представлены определения каждого из принципов, которых всего 5.
Но нам интересны эти принципы не сами по себе, а их связь с архитектурой. Эта связь описана для каждого принципа на рисунке ниже
Ну а дальше мы идем к следующей абстракции, а именно к паттернам проектирования.
Паттерн или шаблон проектирования — это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста. Шаблоны проектирования получили популярность после выхода классической книги “Design Patterns” от Банды четырех в 1994. Коллектив авторов вдохновлялся классическими работами архитектора Кристофера Александра про шаблоны проектирования в градостроительстве и архитектуре (работы 1970х годов). До того момента в разработке основной акцент делался на вычислительных паттернах ака алгоритмах (можно вспомнить монументальный труд Кнута «Искусство программирования»)
В книге “Design Pattern” приводились следующие паттерны, которые были разбиты по трем категориям. Сейчас эти паттерны известны многим инженерам — они фактически стали неким общим набором концепций, которые позволяют говорить про проектирование сервисов на одном языке. Эти паттерны говорят о том, как организовать структуру модулей (классов) и поэтому они находятся на уровень выше принципов организации модулей, но ниже принципов организации компонентов.
Дальше перейдем к принципам организации компонентов
Эта часть лекции основана на книге “Clean Architecture” Дяди Боба, в которой они и были изложены. Всего Uncle Bob выделяет шесть принципов, которые разбиты по двум категориям: принципы cohesion
и принципы coupling
. Ниже перечислены все эти принципы
Начнем мы с принципов, относящихся к cohesion части, и конкретно с Reuse/Release Princimple
(REP
).
В современной реальности понятный релизный процесс и версионирование компонентов — это must have:) Можно посмотреть на работу стандартных пакетных менеджеров в популярных языках программирования и увидеть, что это есть из коробки. Ну а как группировать модули в компонент — это уже зона ответственности maintainer’ов компонентов. И этот принцип рекомендует группировать для удобства переиспользования.
Дальше идет принцип Common Closure Principle
(CCP
), который рекомендует объединять модули в компонент для удобства разработки компонентов — при такой группировке у нас правки зачастую локализованы внутри компонентов.
Следующий принцип — Common Reuse Principle
(CRP
), который в отличие от двух предыдущих подсказывает не как объединять модули в компонент, а как их правильно разделять на разные компоненты.
Теперь, когда мы рассмотрели три компонентных принципа из категории cohesion
, мы можем перейти к диаграмме противоречий, которые возникают при попытке использовать эти принципы. Эта диаграмма представлена на картинке ниже.
Если мы забиваем на Reuse/Release Equivalent Principle
, то пользователям наших компонентов становится сложно их переиспользовать — тупо не ясно как это сделать без налаженного и понятного процесса релизов и версионирования компонент.
Если мы забиваем на Common Closure Principle
, то мейнтейнерам компонент при их доработке требуется внести изменения в слишком большое количество мест — они умирают от непродуктивной нагрузки:) И появляется желание укрупнить и объединить часть компонентов.
Если мы забиваем на Common Reuse Principle
, то появляется большое количество релизов, которые вызваны небольшими изменениями в разных частях крупных компонентов. Это приводит к боли у пользователей компонентов, когда они видят постоянные обновления версий компонентов и должны постоянно читать changelogs
, чтобы понять нужны им эти обновления или нет.
По-факту, серебряной пули не бывает и мы создатели компонентов постоянно балансируют этот треугольник в попытке выдержать баланс:)
А теперь пришло время перейти к следующим трем принципам компонентов из группы, относящейся к coupling
. И первый принцип Acyclic Dependencies Principle
посвящен отсутствию циклов в графе зависимостей графа.
Принцип настолько простой, что его проверку легко автоматизировать и впилить в шаги CI/CD пайплайна сборки вашего приложения.
Дальше мы переходим к более интересному принципу Stable Dependencies Principle
, который говорит про направление зависимостей, которые должны вести в сторону более устойчивых компонент. На рисунке ниже дано определение того, как считать Instability
(нестабильность) компонентов, а также показан пример того, как должен выглядеть граф зависимостей в идеальном случае.
И последний принцип из этой группы — Stable Abstraction Principle
, который говорит про то, что компонент должен быть настолько же абстрактным, насколько он стабилен. На рисунке ниже представлен алгоритм расчета метрики абстрактности компонентов через количество абстрактных классов в компоненте по отношению к общему количеству классов
Ну и теперь мы можем визуализировать Stable Abstraction Principle
как показано на рисунке ниже. Суть в том, что хорошие компоненты должны группироваться в районе The Main Sequence
. Кроме того есть две особые зоны: зона боли и зона бесполезности. В зоне боли у нас идет завязка на конкретные реализации внутри стабильных компонентов. Это приводит к боли при попытке расширить систему. А в зоне бесполезности у нас есть нестабильные абстрактные компоненты, которые в принципе не имеют смысла — зачем нужны интерфейсы, которые никто не использует?
Общий список принципов организации компонентов приведен ниже.
Теперь мы готовы к обсуждению следующего уровня абстракции — библиотек.
Библиотека — это коллекция неизменяемых ресурсов, используемых программой. Она может включать конфигурационные данные, документацию, help-файлы, код с классами и функциями, спецификации типов. По-факту, библиотека — это коллекция имплементаций конкретных поведений, написанное на выбранном языке с хорошо определенным интерфейсом, через который и вызывается это поведение. Для себя я привык считать, что библиотеки — это и есть переиспользуемые компоненты. Для компилируемых языков они бывают статические и динамические. Для интерпретируемых языков такой разницы нет. Теперь мы знаем про библиотеки, дальше стоит обсудить фреймворки.
Фреймворк — программная платформа, определяющая структуру программной системы и облегчающее разработку и объединение разных компонентов большого программного проекта. Фреймворк в отличие от библиотеки диктует правила построения архитектуры приложения, задавая на начальном этапе разработки поведение по умолчанию. Он может содержать в себе большое число разных по тематике библиотек. Фреймворки часто реализуют инверсию управления (IoC
) — важный принцип, упрощающий расширение системы, при котором поток управления программы контролируется фреймворком.
Вот наконец-то мы и добрались до финального уровня абстракции — целого приложения, которое должно соответствовать 12 факторам:)
Давайте рассмотрим подробнее эти факторы, так как они во многом относятся к архитектурным требованиям и требованиям по работе с теми или иными зависимостями. И начнем мы с первого принципа, который касается кодовой базы.
Этот принцип является базовым в мире с современными VCS
системами и подходами к CI/CD. В общем, это теперь скорее гигиенический принцип и если он не соблюдается, то это знак … или запах:)
Второй принцип напрямую относится к зависимостям, а именно к тому, что их надо объявлять явно и изолировать. На уровне приложения это обычно делают с помощью пакетного менеджера, который умеет считывать файл зависимостей и на этапе сборки скачивать их и собирать образ приложения. Иногда часть зависимостей выносили на уровень операционной системы — сейчас их обычно это оставляют на уровне контейнера, внутри которого и рантаймится приложения. В итоге, часть зависимостей описывается в рецепте приготовления контейнера с приложением на основе базового образа.
Третий фактор относится к тому, как стоит конфигурировать приложения. Когда-то давно часто параметры конфигурации зашивали прямо в приложение. Теперь эту конфигурацию выносят в среду выполнения, например, через env variables
или configMaps
или Secrets
, если мы говорим про Kubernetes
.
В четвертом факторе идет речь про работу с backing services
, которые рекомендуют считать подключаемыми ресурсами, которые мы конфигурируем при старте приложения. Такими сторонними службами могут базы данных, внешние API
, объектные хранилища и так далее. В общем и целом такая работа со сторонними службами опять же говорит о том, что с внешними зависимостями нашего приложения надо работать осмысленно.
Пятый принцип закладывает основы правильной работы современных 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