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

Что такое "Clean Architecture"?

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

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

Основные характеристики чистой архитектуры

При разработке большого проекта нам необходимо, чтобы его можно было легко поддерживать, при необходимости изменяя отдельные компоненты. Для этого нам необходимо разделять файлы и классы на компоненты, которые могут изменяться независимо друг от друга. На картинке ниже представлены несколько взаимосвязанных “компонентов”.  

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

Рассмотрим другой вариант. 

Теперь, в случае, если нам нужно заменить ножницы, мы можем всего лишь вытащить шнурок от ножниц из-под стикеров и добавить новый шнурок, привязанный к ножу. Это намного проще. Стикерам все равно, потому что к ним даже не был привязан шнурок.

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

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

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

Граница между доменом и инфраструктурой устанавливается таким образом, что домен ничего не знает об инфраструктуре. Это означает, что пользовательский интерфейс и база данных зависят от бизнес-правил, но эти правила не зависят от пользовательского интерфейса или базы данных. Это делает его архитектурой подключаемого модуля. Не имеет значения, является ли пользовательский интерфейс веб-интерфейсом, настольным приложением или мобильным приложением. Также не имеет значения, хранятся ли данные с использованием SQL, NoSQL или в облаке. Домену это все равно. Это упрощает изменение инфраструктуры.

Поговорим о терминологии

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

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

Сущности и варианты использования

Сущность (entity) - это набор связанных бизнес-правил, которые имеют решающее значение для функционирования приложения. В объектно-ориентированном языке программирования правила для сущности были бы сгруппированы как методы в классе. Даже если бы не было приложения, эти правила все равно существовали бы. Например, взимание процентов по кредиту в размере 10% - это правило, которое может быть введено банком. Это было бы справедливо независимо от того, рассчитывались ли проценты на бумаге или с помощью компьютера.

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

Варианты использования (use cases) - это бизнес-правила для конкретного приложения. Они рассказывают, как автоматизировать систему, и с их помощью определяется поведение приложения. Они взаимодействуют с объектами и зависят от них, но они ничего не знают о других уровнях. Им все равно, веб-страница это или приложение для iPhone, и также, им все равно, хранятся ли данные в облаке или в локальной базе данных SQLite. Этот уровень определяет интерфейсы или содержит абстрактные классы, которые могут использовать внешние уровни.

Адаптеры и инфраструктура

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

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

Принципы внедрения чистой архитектуры

В качестве основы внедрения чистой архитектуры мы рассмотрим набор принципов SOLID.  Они являются принципами на уровне класса, но имеют схожие аналоги, применимые к компонентам (группам связанных классов).

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

Буква O - принцип "Открыто-закрыто" (OCP). "Открыто" означает "открыто для расширения". "Закрыто" означает "закрыто для модификации". Таким образом, вы должны иметь возможность добавлять функциональность к классу или компоненту, но вам не нужно изменять существующую функциональность. Нужно убедиться, что у каждого класса или компонента есть только одна задача, а затем скрыть более стабильные классы за интерфейсами, чтобы они не пострадали, когда придется менять менее стабильные классы.

Принцип замещения Лискова (LSP). Это буква L в слове SOLID. Этот принцип означает, что классы или компоненты более низкого уровня могут быть заменены, не влияя на поведение классов и компонентов более высокого уровня. Это можно сделать, реализовав абстрактные классы или интерфейсы. Например, в Java ArrayList и связанный список реализуют интерфейс List, поэтому их можно заменять друг на друга.

Принцип разделения интерфейсов (ISP) – буква I. ISP использует интерфейс для отделения класса от других классов, которые его используют. Интерфейс предоставляет только то подмножество методов, которое необходимо зависимому классу. Таким образом, изменения в других методах не влияют на зависимый класс.

И наконец, буква D - принцип инверсии зависимостей (DIP). Этот принцип означает, что менее стабильные классы и компоненты должны зависеть от более стабильных, а не наоборот. Если стабильный класс зависит от нестабильного класса, то каждый раз, когда меняется нестабильный класс, это также влияет на стабильный класс. Таким образом, направление зависимости должно быть изменено. Мы можем использовать абстрактный класс или скрыть стабильный класс за интерфейсом.

Таким образом, вместо стабильного класса используйте класс Volatile, подобное этому:

    class StableClass {

        void myMethod(VolatileClass param) {

            param.doSomething();

        }

    }

Вы могли бы создать интерфейс, который реализует класс Volatile:

  class StableClass {

        interface StableClassInterface {

            void doSomething();

        }

        void myMethod(StableClassInterface param) {

            param.doSomething();

        }

    }

    

    class VolatileClass implements StableClass.StableClassInterface {

        @Override

        public void doSomething() {

        }

    }

Это меняет направление зависимости на противоположное. Класс Volatile знает название стабильного класса, но стабильный класс ничего не знает о классе Volatile.

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

Заключение

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


В завершение напоминаю об открытых уроках, которые пройдут в ближайшее время в рамках курса "Enterprise Architect":

  • 15 августа: Роль корпоративного архитектора в продуктовой трансформации бизнеса. Записаться

  • 20 августа: Бизнес-архитектура: ключевые объекты. Записаться

Комментарии (25)


  1. OlegZH
    15.08.2024 16:41

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

    Ой. А такая существует? Было бы просто здорово всегда иметь возможность «легко изменять по мере роста проекта».


    1. Luzinov
      15.08.2024 16:41

      Если вовремя рефакторить - возможно. Большинство компаний просто игнорируют техдолг пока гром не грянет.


  1. OlegZH
    15.08.2024 16:41

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

    Стоп. А такое бывает? Смотрите: мы изменяем логику работы одного компонента, но не меняем логику работы другого компонента. И всё продолжает работать?

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

    Пока проще только на бумаге.

    Эта же концепция является архитектурой, которая упростит обслуживание и изменение вашего программного обеспечения.

    Концепция чего?

    Вообще, здесь центральный вопрос: а зачем нам надо что-то менять? Допустим, мы строим настоящее здание. Что нам может потребоваться в нём изменить уже после постройки? Мы же не сможем снести несущие стены, на то эти стены и несущие. Тут одно из двух: либо мы промахнулись с архитектурой или ... не знаем, чего хотим.


    1. gun_dose
      15.08.2024 16:41

      Стоп. А такое бывает? Смотрите: мы изменяем логику работы одного компонента, но не меняем логику работы другого компонента. И всё продолжает работать?

      Да, такое бывает. Это и есть чистая архитектура, а всё остальное в статье лишнее. Как такое возможно? Да легко. Вот есть трактор с плугом, можно пахать. А можно отцепить от трактора плуг и прицепить сеялку, и всё опять же прекрасно будет работать "без изменения логики работы трактора". Программы должны работать точно так же.


      1. OlegZH
        15.08.2024 16:41

        отцепить ... прицепить

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


        1. gun_dose
          15.08.2024 16:41

          Это вы очень сильно упростили. На деле "плуг" может быть не классом, а сложнейшим приложением из десятков микросервисов, написанных на разных языках. Тут вся суть в отсутствии циклических зависимостей и грамотных интерфейсах. Не только, интерфейсов из ООП, API - это ведь тоже интерфейс. В примере с трактором интерфейс - это посадочное место для навесного оборудования. И производитель трактора как раз не ограничен "заданным набором классов", т.к. он знать не знает, что захочет повесить конечный потребитель на его трактор. В то же время производитель плугов тоже не знает, какой именно будет трактор, он просто имплементирует интерфейс, то есть посадочное место в его случае. Сам по себе трактор тоже подчиняется этим принципам: можно поменять колёса, кабина может отличаться. И дальше можно продолжать дробить: в кабине можно менять приборную панель, в приборной панели тахометр, на тахометре циферблат. Всё чётко разделяется на узля, подузлы и т.д. И замена одного узла не нарушает работу остальных.

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


          1. tzlom
            15.08.2024 16:41

            ну а теперь замените двигатель в тракторе чтобы добавить мощности, и смотрите как вся эта красивая картинка рассыпается


            1. gun_dose
              15.08.2024 16:41

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

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


              1. tzlom
                15.08.2024 16:41

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

                конечно можно сказать что мы заменим на аналогичный, но в мире софта такое не возможно


                1. gun_dose
                  15.08.2024 16:41

                  Что значит нет таких примеров? В любой деревне можно встретить подобных Франкенштейнов.

                  в мире софта такое не возможно

                  Чушь полная. Как тогда, по-вашему получается одинаковые библиотеки использовать в разных проектах? Или у вас не получается? :D


                  1. tzlom
                    15.08.2024 16:41

                    В деревнях таких франкенштейнов достаточно на ремонте стоит, но это не то как мы хотим разрабатывать ПО.

                    Как тогда, по-вашему получается одинаковые библиотеки использовать в разных проектах

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


                    1. gun_dose
                      15.08.2024 16:41

                      В деревнях таких франкенштейнов достаточно на ремонте стоит

                      А трактора в штатной комплектации на ремонте не стоят?

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


  1. OlegZH
    15.08.2024 16:41

    Внутренний круг - это уровень предметной области вашего приложения. Здесь вы размещаете бизнес-правила.

    Где это здесь? Во внутреннем круге? А зачем?

    Под "бизнесом" мы необязательно подразумеваем компанию. Это просто означает суть того, что делает ваше приложение, основную функциональность кода.

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

    Эти бизнес-правила, довольно стабильны, поскольку вы вряд ли будете часто менять суть того, что делает ваше приложение.

    Именно они и меняются чаще всего. Иначе было бы довольно просто сделать монолитное приложение и уже больше его никак не поддерживать. Зачем поддерживать то, что и так работает, как часы?


  1. OlegZH
    15.08.2024 16:41

    Например, у вас больше шансов изменить внешний вид кнопки пользовательского интерфейса, чем способ расчета кредита.

    Спорное утверждение.


  1. OlegZH
    15.08.2024 16:41

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

    Кому-то удалось реализовать такой подход?


  1. OlegZH
    15.08.2024 16:41

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

    Вот и возникает вопрос: не имеет место, в действительности, некоторое дублирование? Мы же должны в графическом интерфейсе сделать какую-то контекстную обработку. Вот и возникает зависимость.


  1. OlegZH
    15.08.2024 16:41

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

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


  1. OlegZH
    15.08.2024 16:41

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

    А можно сделать внутри класса (некоторого достаточно общего типа) приёмник сообщений и предусмотреть различную реакцию на различные сообщения.


  1. OlegZH
    15.08.2024 16:41

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

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

    Давайте рассмотрим обобщённый пример. В большинстве ситуаций, мы имеем дело со списками однородных объектов. Соответственно, у нас будет класс Список. Но, если у нас есть объекты определённого типа, то у нас появляется необходимость создавать производный класс СписокОбъектовОпределённогоТипа. В этом смысле, класс Список выступает как общий шаблон для произвольного количества производных классов.


  1. OlegZH
    15.08.2024 16:41

    Например, в Java ArrayList и связанный список реализуют интерфейс List, поэтому их можно заменять друг на друга.

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


  1. OlegZH
    15.08.2024 16:41

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

    Вопрос в том, почему в интерфейс входят именно эти методы.


  1. OlegZH
    15.08.2024 16:41

    Этот принцип означает, что менее стабильные классы и компоненты должны зависеть от более стабильных, а не наоборот.

    А почему должны быть какие-то нестабильные классы?


  1. OlegZH
    15.08.2024 16:41

    ... но в рамках данной статьи другие методы мы не рассматриваем.

    Продолжения разговора не будет?


  1. OlegZH
    15.08.2024 16:41

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

    Осталось много непонятного. Пример также нуждается в комментариях.


  1. OlegZH
    15.08.2024 16:41
    +3

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

    Архитектура архитектуры архитектУрна и архитЕкторна.