Обзор “Clean Architecture” (Часть II: принципы дизайна модулей и разделения по компонентам)

В прошлой части обзора книги мы рассматривали что такое дизайн и архитектура, а также какие существуют парадигмы программирования. В этой части мы поговорим про Design и Component Principles. Они выделены на рисунке ниже

Рис.1 “Содержание книги и второй части обзора”

Начинается все с принципов дизайна.

Design Principles

Цели дизайн принципов и сфера их применимости понятна и приведена на рисунке ниже

Рис.2 “Цели дизайн принципов”

Сами принципы формируют акроним SOLID, который расшифровывается на изображении ниже

Рис.3 “Принципы дизайна (SOLID)”

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

SRP: The Single Responsibility Principle

Этот принцип часто понимают не так, как следует. Стандартное понимание верно для функции

A function should do one, and only one, thing

Но исторически SRP описывался так “A module should have one, and only one, reason to change”. Дальше этот принцип можно было перефразировать так “A module should be responsible to one, and only one, user or stakeholder”, так как софт делают для удовлетворения потребностей стейкхолдеров. Но правильная версия звучит так

A module should be responsible to one, and only one, actor.

Здесь пользователь или стейкхолдер заменен на понятие actor из UML. И определение для этого термина следующее (из стандарта UML 2)

Actor specifies a role played by a user or any other system that interacts with the subject

Остается вопрос с тем, что такое модуль. Это может быть файл с исходным кодом, но в некоторых языках модулем может быть и нечто другое, поэтому стоит определить модуль как “just a cohesive set of functions and data structures”. Где

Cohesion is the force that binds together the code responsible to a single actor

Симптомами нарушения этого принципа являются

  • accidental duplication — когда код разных акторов зависит от общего модуля, в котором происходят изменения нужные для одного актора, но не другого. Изменения вносятся для одного актора, но влияют сразу на всех
  • merges — когда код разных акторов зависит от общего модуля, в который вносят параллельно изменения нужные как для одного актора, так и для другого. Итоговое поведение может не удовлетворять ни одного из акторов

Обе эти проблемы можно решить, если разделять код, который требуется разным акторам.

Этот принцип применим на разных уровнях абстракции как показано на изображении ниже.

Рис.4 “Применение принципа SRP на разных уровнях абстракции”

OCP: The Open-Closed Principle

Этот принцип был предложен Бертраном Мейером в 1988 и звучал так

A software artifact should be open for extension but closed for modification.

По-факту, мы хотим иметь возможность расширять работу артефакта без потребности в масштабном рефакторинге. Дальше автор приводит пример почти на всю главу про отчет, который помимо представления в вебе потребовалось распечатать. Он применяет SRP принцип и получает схему классов и компонентов, в которой основная логика отчета общая, а presenters для веба и печати разные. Глава заканчивается таким утверждением о связи OCP и архитектуры систем

The OCP is one of the driving forces behind the architecture of systems. The goal is to make the system easy to extend without incurring a high impact of change. This goal is accomplished by partitioning the system into components, and arranging those components into a dependency hierarchy that protects higher-level components from changes in lower-level components.

LSP: The Liskov Substitution Principle

В 1988 году Барбара Лисков предложила принцип для определения подтипов

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

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

ISP: The Interface Segregation Principle

Этот принцип говорит про то, что no client should be forced to depend on methods it does not use. На уровне кода реализация этого принципа зависит от конструкций языка, доступных для определения зависимостей.

На уровне архитектуры это обобщается до утверждения близкого к common sense о том, что вредно зависеть от компонентов, содержащих больше функциональности, которая была изначально. Продолжение этого принципа на уровне компонентов автор приводит в главе, посвященной Common Reuse Principle.

DIP: The Dependency Inversion Principle

Этот принцип говорит о том, что the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.

На уровне кода это значит, что

In a statically typed language, like Java, this means that the use, import, and include statements should refer only to source modules containing interfaces, abstract classes, or some other kind of abstract declaration. Nothing concrete should be depended on.

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

It is the volatile concrete elements of our system that we want to avoid depending on. Those are the modules that we are actively developing, and that are undergoing frequent change

Автор дает такие советы относительно обеспечения стабильности абстракций.

Рис.5 “Советы для обеспечения стабильности абстракций”

Также он показывает пример как использовать шаблон проектирования Factory для инверсии зависимостей.

Рис.6 “Диаграмма классов с применением Dependency Inversion Principle”

Application использует конкретную реализацию ConcreteImpl через Service интерфейс. Но Application как-то должен создать инстанс ConcreteImpl. Чтобы не создавать зависимости от конкретной имплементации Application делает вызов методаmakeSvc интерфейса ServiceFactory. Причем конкретная имплементация ServiceFactoryImpl инстанцирует конкретную имплементацию ConcreteImpl и возвращает это как Service.

Красным на снимке выше выделена архитектурная граница, которая разделяет абстрактное от конкретного. Верхняя часть содержит высокоуровневые правила, а нижняя — конкретные детали реализации. Причем поток управления (flow of control) пересекает изогнутую линию в направлениях противоположных зависимостям в исходном коде — в этом смысл этого принципа

The source code dependencies are inverted against the flow of control — which is why we refer to this principle as Dependency Inversion.

При таком разделении нам все равно требуется место, где мы локализуем зависимости на конкретные компоненты, например, часто это main компонент. Например, в нашем примере с рисунка в main будет инстанцироваться ServiceFactoryImpl и дальше она будет доступна Application как имплементация интерфейса ServiceFactory

Этот принцип будет появляться и на следующих уровнях абстракции архитектурных принципов и в итоге превратиться в Dependency Rule.

Дальше автор переходит к

Component Principles

И начинается эта часть с обсуждения того, что такое

Components

В первом предложении дается определение

Components are the units of deployment

А дальше приводятся примеры из разных языков (Java, Ruby, compiled languages). И после этого происходит погружение в историю, когда разработчикам самим требовалось указывать подгружать ли программу в память компьютера. В этом рассказе упоминались loader и linker, которые подгружали в память компоненты и обрабатывали external references и external definitions. Программы росли все дальше и был сформулирован Murphy’s law of program size:

Programs will grow to fill all available compile and link time

Это закон успешно сосуществовал с Moore’s Law

The number of transistors on a microchip doubles every two years

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

These dynamically linked files, which can be plugged together at runtime, are the software components of our architectures. It has taken 50 years, but we have arrived at a place where component plugin architecture can be the casual default as opposed to the herculean effort it once was.

Component Cohesion

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

Рис.7 “Component Cohesion Principles”

REP: The Reuse/Release Equivalent Principle

Этот принцип звучит очень просто и понятно

People who want to reuse software components cannot, and will not, do so unless those components are tracked through a release process and are given release numbers

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

Classes and modules that are grouped together into a component should be releasable together

CCP: The Common Closure Principle

Этот принцип звучит так

Gather into components those classes that change for the same reasons and at the same times. Separate into different components those classes that change at different times and for different reasons.

По-факту это знакомый нам SRP принцип, который применили к компонентам. В нем заменили слово Class на Component.

Во многих приложения maintainability важнее reusability, поэтому локализация причин изменений внутри компонентов, следующих CCP помогает проще менять приложения. Это позволяет перепроверить и перевыложить только тот компонент, в котором были локализованы изменения, а не половину системы:)

Также этот принцип связан с OCP (Open-Closed Principle), в котором утверждалось, что классы должны быть закрыты для модификации и открыты для расширения. Это не всегда достижимо и в CCP мы собираем в компоненты классы, имеющие одинаковые типы и причины изменений.

CRP: The Common Reuse Principle

Этот принцип построен на отрицании и говорит о том, какие классы не стоит помещать в один компонент.

Don’t force users of a component to depend on things they don’t need.

Если мы зависим от какого-то класса внутри другого компонента, то любые изменения во всем компоненте могут потребовать от нас повторной компиляции, тестирования и деплоя. Поэтому когда мы зависим от компонента, следует убедится, что мы зависим от каждого класса в нем. Это близко по смыслу тому, что классы внутри компонента неразделимы. Этот принцип близок к ISP (Interface Segregation Principle), в котором приблизительно то же самое говорилось относительно интерфейсов.

Все эти три cohesion принципа объединяются в такую диаграмму противоречий

Рис.8 “Cohesion principles tension diagram”

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

Дальше автор делится своим опытом и рассказывает, что

Generally, projects tend to start on the right hand side of the triangle, where the only sacrifice is reuse. As the project matures, and other projects begin to draw from it, the project will slide over to the left. This means that the component structure of a project can vary with time and maturity.

Обсудив тему component cohesion автор переходит к

Component Coupling

В этой главе автор рассматривает взаимоотношения между компонентами. И здесь будут трения между вопросами develop-ability и logical design.

Рис.9 “Component Coupling Principles”

ADP: The Acyclic Dependencies Principle

И первый принцип звучит очень просто

Allow no cycles in the component dependency graph

В этой главе автор описывает проблемы со сборками из-за зависимостей, которые постоянно меняются. Для решения проблем он предлагает два средства:

  • Еженедельный билд— билд всей системы раз в неделю. В наше время это кажется уже устаревшим подходом. Надо уметь билдить систему по требованию с той частотой, что нам нужна
  • Устранение циклических зависимостей — по-факту, граф зависимостей должен быть направленным ациклическим графом (directed acyclic graph — DAG). Забавно, что сейчас отсутствие циклов можно контролировать на уровне CI/CD пайплайнов

Для устранения циклов стоит использовать принцип инверсии зависимостей (DIP). Как именно можно посмотреть на рисунке 6.

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

The component structure cannot be designed from the top down. It is not one of the first things about the system that is designed, but rather evolves as the system grows and changes.

А в общем диаграмма зависимостей компонентов друг от друга показывает такие параметры как buildability и maintainability приложения, которые не так актуальны при старте разработки. Но как только компонентов становиться больше, нам становиться актуальнее локализация изменений внутри компонента. Ну и дальше если меняется компонент, от которого зависят другие компоненты, то это тоже вносит нестабильность. Поэтому появляется желание огородить стабильные компоненты от зависимостей на нестабильные.

Дальше приложение становиться больше и нам требуется учитывать CRP (Common Reuse Principle), чтобы мы могли переиспользовать компоненты в разных частях системы.

SDP: The Stable Dependencies Principle

Принцип звучит так

Depend in the direction of stability

Мы делаем системы, которые могут расширяться и меняться, поэтому мы должны это учитывать в дизайне своих приложений. Если мы придерживаемся CCP (Common Closure Principle), то компоненты чувствительны к определенным видам изменений и нечувствительны к другим. Часть компонентов спроектированы для постоянных изменений.

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

Автор много говорит про стабильность компонентов, под которой он понимает то, насколько сложно их изменить

A component with lots of incoming dependencies is very stable because it requires a great deal of work to reconcile any changes with all the dependent components.

Дальше автор приводит метрики стабильности компонента

  • Fan-in: число входящих зависимостей для компонента (сколько компонентов от него зависят)
  • Fan-out: число исходящих зависимостей для компонента (от скольких компонентов он зависит)
  • Fan-in и Fan-out рассчитываются как число классов извне компонентов, которые имеют зависимости на классы внутри компонентов.
  • I (Instability): I = Fan-out/(Fan-out+ Fan-in): эта метрика принимает значения от 0 для полностью стабильного компонента, до 1 — для полностью нестабильного

Полностью нестабильный компонент с I=1 зависит от других компонент и безответственен (от него никто не зависит). И наоборот стабильный компонент ответственен перед зависимыми компонентами, а сам ни от кого не зависит. Ниже приведен рисунок со стабильным и нестабильным компонентом

Рис.10 “Примеры стабильного и нестабильного компонента”

Если размещать нестабильные компоненты сверху, а стабильные снизу, то можно добиться дополнительного удобства, так как любая стрелка направленная вверх нарушает принцип SDP (The Stable Dependencies Principle). Пример такого размещения тех же компонент с предыдущего рисунка, приведен на рисунке 11.

Рис.11 “Правильное размещение стабильных и нестабильных компонент”

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

SAP: The Stable Abstract Principle

Этот принцип звучит довольно просто

A component should be as abstract as it is stable

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

Микс принципов SAP (The Stable Abstract Principle) и SDP (The Stable Dependencies Principle) дает вариант DIP (Dependency Inversion Principle) для компонентов

Dependencies run in the direction of abstraction

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

Nc: Количество классов в компоненте

Na: Количество абстрактных классов и интерфейсов в компоненте

  • A: Abstractness. A = Na/Nc

Дальше используя метрики абстрактности и нестабильности можно построить график главной последовательности (the main sequence)

Рис.12 “График Абстрактности и Стабильности и Главная последовательность”

На графики изображены две зоны исключений:

  • Зона боли — здесь у нас полностью стабильные и неабстрактные компоненты. Проблема в том, что если потребуется поменять реализацию, то это будет очень больно
  • Зона бесполезности — здесь у нас абстрактные классы, от которых никто не зависит. Это просто бессмысленно.

Автор называет главной последовательностью диагональ от точек (1,0) до (0, 1). Именно в окрестностях этой последовательности располагаются компоненты, которые в “самый раз”. Одновременно концы этой последовательности наиболее желанны.

The most desirable position for a component is at one of the two endpoints of the Main Sequence. Good architects strive to position the majority of their components at those endpoints.

Дальше автор пользуется простой геометрией и вводит метрику D (Distance from the Main Sequence). D = |A + I — 1|. Для точек на главной последовательности D=0, а максимума D достигает в точках (0,0) и (1,1). Дальше можно рассчитать эту метрику для компонентов в вашем приложении и оценить насколько все у вас хорошо с зависимостями.

В следующей части обзора книги Clean Architecture мы рассмотрим все главы части, которая непосредственно посвящена архитектуре.

Director of digital ecosystem development department at Tinkoff. Bachelor at applied math, Master at system analysis, Postgraduate studies at economics.

Director of digital ecosystem development department at Tinkoff. Bachelor at applied math, Master at system analysis, Postgraduate studies at economics.