Edit Page

Clean Architecture


Clean Architecture

The Clean Architecture описывает основные общие правила построения архитектуры приложения. Как сделать разработку тестируемой, удобной, понятной, а части системы взаимозаменяемыми.

За последние несколько лет было предложено множество идей построения архитектуры.

Например:

  • Hexagonal Architecture (или Ports and Adapters) от Alistair Cockburn адаптированная Steve Freeman и Nat Pryce в их замечательной книге Growing Object Oriented Software
  • Onion Architecture от Jeffrey Palermo
  • Screaming Architecture из блога Robert Martin
  • DCI от James Coplien и Trygve Reenskaug.
  • BCE от Ivar Jacobson из его книги Object Oriented Software Engineering: A Use-Case Driven Approach

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

Все они разбивают проблему на схожие задачи. Они все добиваются этого разделения разложением кода на слои. Каждая имеет как минимум один слой для бизнес логики и другой - для интерфейсов.

Каждая из этих архитектур создает систему, которая:

  1. Независимость от фреймворка. Архитектура не должна зависеть от какого-либо фреймворка или библиотеки, релиазующей какую-либо фичу. Такой подход позволяет использовать фреймворки как инструмент, нежели, подгонять ваш код под возможности фреймворка.
  2. Тестируемость. Бизнес-логика должна быть тестируема без UI, баз данных, веб-сервисов и прочих сторонник элементов.
  3. Независимость от UI. Должна быть возможность легко изменить UI без затрагивания других частей системы. Веб интерфейс может быть заменен на консольный без изменения бизнес-логики.
  4. Независимость от Базы Данных. Должна быть возможность заменить Oracle или SQL Server на Mongo, BigTable, CouchDB или что-либо еще. Бизнес-логика не зависит от Базы Данных
  5. Независимость от внешних сервисов. Бизнес-логика просто не знает о существовании чего-либо во внешнем мире.

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

Правило зависимости (Dependency rule)

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

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

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

Слои

Entities

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

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

Use Cases

Код этого уровня содержит бизнес-правила конкретного приложения. Он содержит и реализовывает все возможные варианты использования системы. Use cases отвечают за поток данных в/из Entities и используют Entities для выполнения необходимых действий.

Изменения этого слоя не должны влиять на Entities. Также, изменения внених слоев, таких как, Базы Данных, никак не должны влиять на Use Cases.

Однако, код Use Cases будет изменен при изменении логики работы приложения. Если изменится какой-либо случай использования приложения, эти изменения будут реализованы в этом слое.

Interface Adapters

Этот слой представляет из себя набор адаптеров, которые конвертируют данные, из формата, который удобен для использования в Entities и Use Cases в формат, более удобный для внешних сервисов, таких как Базы Данных или Web-сервисы. Например, в этом слое должен содержаться полностью весь GUI MVC. Контроллеры, Презентеры, Представления, все находится здесь. Модели, пожалуй, это всего лишь структуры данных, которые передаются из Контролеров в Use Cases и обратно из Use Cases в Представления и Презентеры.

По аналогии, в этом слое данные должны быть конвертированы из формата, удобного для использования в Entities и Use Cases в форматы, удобные для хранения, например в Базах Данных. Код изнутри этого слоя не должен что-либо знать о Базах Данных. Если используется SQL база данных, то все SQL запросы должны быть обработаны именно на уровне этого слоя.

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

Frameworks and Drivers

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

На этом слое реализованы все детали. Связь с Web-сервисами - это детали. Базы Данных - это детали. Мы оставили все это снаружи, чтобы не причинять вреда внутренним слоям.

И это все?

Нет. Эти слои - схематичны. Вероятно, вам понадобится что-либо, не вписывающие в эти четыре слоя. Нет такого правила, которое говорит, что вы должны использовать только эти четыре слоя. Однако, Правило Зависимостей (Dependency Rule) должно выполняться всегда. Исходный код на любом слое должны ссылаться на внутренний. При движение вовнутрь, уровень абстракции увеличивается. Внешний слой - низкоуровневая реализация деталей. Чем дальше вы продвигаетесь вовнутрь, тем более абстрактным становится код и более высокоуровневую логику реализовывает. Самый внутренний слой отвечает за общую логику.

Пересечение границ слоев

На диаграмме справа-снизу представлено, как можно пересечь границы слоя. Там показано, как Контроллеры и Представления общаются с Use Cases. Обратите внимание на поток управления. Он начинается в Контроллере, проходит через Use Case и заканчивается в Презентере. Также обратите внимание на зависимости исходного кода. Каждый из них указывает на Use Case.

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

Допустим, Use Case должен обратиться к Презентеру. Это обращение не должно быть реализовано напрямую, чтобы не нарушать Правило Зависимостей (Dependency Rule): внутренние слои не должны знать о реализации внешних. В таком случае, Use Case обратится к интерфейсу (изображено на диаграмме как Use Case Output Port) внутреннего слоя, а Презентер из внешнего слоя должен его реализовать.

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

Какие данные должны пересекать границы

Обычно, данные, пересекаемые границы, являются обычными структурами данных. Вы можете пользоваться основными структурами или Data Transfer объектами. Или данные могут быть аргументами вызова функций. Или вообще можно представлять данные в виде hashmap или засунуть в объект. Важно, чтобы данные, пересекаемые границы, были простыми и изолированными. Мы же не хотим читерить и передавать Entities или строки из Базы Данных. Также передаваемые данные не должны иметь какие-либо зависимости и нарушать правило зависимостей (Dependency Rule).

Например, множество фреймворков возвращает из Базы Данных данные в очень удобном формате. Назовем это RowStructure. Однако нельзя передавать RowStructure через границы слоев. Это нарушает правило зависимостей (Dependency Rule) потому что таким образом мы можем дать внутреннему слою информацию о реализации и структуре используемых данных внешнего.

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

В нашем случае

Следовать этим простым правилам не так сложно, однако это сохранит вас от головной боли в будущем. Разделением кода на слои и следованием правилу зависимостей (Dependency Rule) можно создать систему, которая будет действительно тестируемой со всеми вытекающими из этого плюсами. Если какая-то внешняя часть системы устареет (например, База Данных или фреймворк), заменить его будет достаточно легко без каких-либо проблем.

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

Истоки

В 2011 году Robert C. Martin, также известный как Uncle Bob, опубликовал статью Screaming Architecture, в которой говорится, что архитектура должна «кричать» о самом приложении, а не о том, какие фреймворки в нем используются. Позже вышла статья, в которой Uncle Bob даёт отпор высказывающимся против идей чистой архитектуры. А в 2012 году он опубликовал статью «The Clean Architecture», которая и является основным описанием этого подхода. Кроме этих статей также очень рекомендуется посмотреть видео выступления Дяди Боба.

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

Clean Architecture

В Android-сообществе Clean стала быстро набирать популярность после статьи Architecting Android...The clean way?, написанной Fernando Cejas. В этой статье Fernando приводит такую схему слоёв:

android-the-clean-way

То, что на этой схеме другие слои, а в domain слое лежат ещё какие-то Interactors и Boundaries, сбивает с толку. Оригинальная картинка тоже не всем понятна. В статьях многое неоднозначно или слегка абстрактно. А видео не все смотрят (обычно из-за недостаточного знания английского). И вот, из-за недопонимания, люди начинают что-то выдумывать, усложнять...

Давайте разбираться!

Clean Architecture объединила в себе идеи нескольких других архитектурных подходов, которые сходятся в том, что архитектура должна:

  • быть тестируемой;
  • не зависеть от UI;
  • не зависеть от БД, внешних фреймворков и библиотек.

Это достигается разделением на слои и следованием Dependency Rule (правилу зависимостей).

Dependency Rule

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

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

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

Слои

Uncle Bob выделяет 4 слоя:

  • Entities. Бизнес-логика общая для многих приложений.
  • Use Cases (Interactors). Логика приложения.
  • Interface Adapters. Адаптеры между Use Cases и внешним миром. Сюда попадают Presenter'ы из MVP, а также Gateways (более популярное название репозитории).
  • Frameworks. Самый внешний слой, тут лежит все остальное: UI, база данных, http-клиент, и т.п.

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

Переходы

Переходы между слоями осуществляются через Boundaries, то есть через два интерфейса: один для запроса и один для ответа. Их можно увидеть справа на оригинальной схеме (Input/OutputPort). Они нужны, чтобы внутренний слой не зависел от внешнего (следуя Dependency Rule), но при этом мог передать ему данные.

Flow of control

Оба интерфейса относятся к внутреннему слою (обратите внимание на их цвет на картинке).

Смотрите, Controller вызывает метод у InputPort, его реализует UseCase, а затем UseCase отдает ответ интерфейсу OutputPort, который реализует Presenter. То есть данные пересекли границу между слоями, но при этом все зависимости указывают внутрь на слой UseCase'ов.

Чтобы зависимость была направлена в сторону обратную потоку данных, применяется принцип инверсии зависимостей (буква D из аббревиатуры SOLID). То есть, вместо того чтобы UseCase напрямую зависел от Presenter'a (что нарушало бы Dependency Rule), он зависит от интерфейса в своём слое, а Presenter должен этот интерфейс реализовать.

Точно та же схема работает и в других местах, например, при обращении UseCase к Gateway/Repository. Чтобы не зависеть от репозитория, выделяется интерфейс и кладется в слой UseCases.

Что же касается данных, которые пересекают границы, то это должны быть простые структуры. Они могут передаваться как DTO или быть завернуты в HashMap, или просто быть аргументами при вызове метода. Но они обязательно должны быть в форме более удобной для внутреннего слоя (лежать во внутреннем слое).

Особенности мобильных приложений

Надо отметить, что Clean Architecture была придумана с немного иным типом приложений на уме. Большие серверные приложения для крупного бизнеса, а не мобильные клиент-серверные приложения средней сложности, которые не нуждаются в дальнейшем развитии (конечно, бывают разные приложения, но согласитесь, в большей массе они именно такие). Непонимание этого может привести к overengineering'у.

На оригинальной схеме есть слово Controllers. Оно появилось на схеме из-за frontend'a, в частности из Ruby On Rails. Там зачастую разделяют Controller, который обрабатывает запрос и отдает результат, и Presenter, который выводит этот результат на View. Многие не сразу догадываются, но в android-приложениях Controllers не нужны.

Ещё в статье Uncle Bob говорит, что слоёв не обязательно должно быть 4. Может быть любое количество, но Dependency Rule должен всегда применяться.

Глядя на схему из статьи Fernando Cejas, можно подумать, что автор воспользовался как раз этой возможностью и уменьшил количество слоев до трёх. Но это не так. Если разобраться, то в Domain Layer у него находятся как Interactors (это другое название UseCase'ов), так и Entities.

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

Слои и линейность

Сравнивая оригинальную схему от Uncle Bob'a и cхему Fernando Cejas'a многие начинают путаться. Линейная схема воспринимается проще, и люди начинают неверно понимать оригинальную. А не понимая оригинальную, начинают неверно толковать и линейную. Кто-то думает, что расположение надписей в кругах имеет сакральное значение, или что надо использовать Controller, или пытаются соотнести названия слоёв на двух схемах. Смешно и грустно, но основные схемы стали основными источниками непонимания!

Постараемся это исправить. Для начала давайте очистим основную схему, убрав из нее лишнее для нас. И переименуем Gateways в Repositories, т.к. это более распространенное название этой сущности.

cicles schema

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

cicles-liner schema

Как уже сказано выше, цвета обозначают слои. А стрелка внизу обозначает Dependency Rule.

На получившейся схеме уже проще представить себе течение данных от UI к БД или серверу и обратно. Но давайте сделаем еще один шаг к линейности, расположив слои по категориям:

liner schema

Мы намеренно не называем это разделение слоями, в отличие от Fernando Cejas. Потому что мы и так делим слои. Мы называем это категориями или частями. Можно назвать как угодно, но повторно использовать слово «слои» не стоит.

А теперь давайте сравним то, что получилось, со схемой Fernando.

two liner schemas

Надеюсь теперь вcё начало вставать на свои места. Выше мы говорили, что у Fernando всё же 4 слоя. Теперь это тоже стало понятнее. В Domain части у нас находятся и UseCases и Entities.

Такая схема воспринимается проще. Ведь обычно события и данные в наших приложениях ходят от UI к backend'у или базе данных и обратно. Давайте изобразим этот процесс:

both schema

Красными стрелками показано течение данных.

Событие пользователя идет в Presenter, тот передает в Use Case. Use Case делает запрос в Repository. Repository получает данные где-то, создает Entity, передает его в UseCase. Так Use Case получает все нужные ему Entity. Затем, применив их и свою логику, получает результат, который передает обратно в Presenter. А тот, в свою очередь, отображает результат в UI.

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

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

Слои, а не сущности

Как понятно из заголовка, кто-то думает, что на схемах изображены сущности (особенно это затрагивает UseCases и Entities). Но это не так.

На схемах изображены слои, в них может находиться много сущностей. В них будут находиться интерфейсы для переходов между слоями (Boundaries), различные DTO, основные классы слоя (Interactors для слоя UseCases, например).

Не будет лишним взглянуть на схему, собранную из частей, показанных в видео выступления Uncle Bob'a. На ней изображены классы и зависимости:

classes and dependencies

Видите двойные линии? Это границы между слоями. Разделение между слоями Entities и UseCases не показаны, так как в видео основной упор делался на том, что вся логика (приложения и бизнеса) отгорожена от внешнего мира.

C Boundaries мы уже знакомы, интерфейс Gateway – это то же самое. Request/ResponseModel – просто DTO для передачи данных между слоями. По правилу зависимости они должны лежать во внутреннем слое, что мы и видим на картинке.

Про Controller мы тоже уже говорили, он нас не интересует. Его функцию у нас выполняет Presenter.

А ViewModel на картинке – это не ViewModel из MVVM и не ViewModel из Architecture Components. Это просто DTO для передачи данных View, чтобы View была тупой и просто сетила свои поля. Но это уже детали реализации и будет зависеть от выбора презентационного паттерна и личных подходов.

В слое UseCases находятся не только Interactor'ы, но также и Boundaries для работы с презентером, интерфейс для работы с репозиторием, DTO для запроса и ответа. Отсюда можно сделать вывод, что на оригинальной схеме отражены всё же слои.

Entities

Entities по праву занимают первое место по непониманию.

Мало того, что почти никто (включая меня до недавнего времени) не осознает, что же это такое на самом деле, так их ещё и путают с DTO.

Однажды в чате возник спор, в котором оппонент доказывал, что Entity – это объекты,
полученные после парсинга JSON в data-слое, а DTO – объекты, которыми оперируют Interactor'ы...

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

Что же такое Entities?

Чаще всего они воспринимаются как POJO-классы, с которыми работают Interactor'ы. Но это не так. По крайней мере не совсем.

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

Именно фраза: «Entities это бизнес объекты», – запутывает больше всего. Кроме того, на приведенной выше схеме из видео Interactor получает Entity из Gateway. Это также подкрепляет ощущение, что это просто POJO объекты.

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

Это также подтверждается в разъяснении от Uncle Bob'а:

Uncle Bob говорит, что для него Entities содержат бизнес-правила, независимые от приложения. И они не просто объекты с данными. Entities могут содержать ссылки на объекты с данными, но основное их назначение в том, чтобы реализовать методы бизнес-логики, которые могут использоваться в различных приложениях.

А по-поводу того, что Gateways возвращают Entities на картинке, он поясняет следующее:

Реализация Gаteway получает данные из БД, и использует их, чтобы создать структуры данных, которые будут переданы в Entities, которые Gateway вернет. Реализовано это может быть композицией

class MyEntity { private MyDataStructure data;}

или наследованием

class MyEntity extends MyDataStructure {...}

И в конце ответа фраза:

And remember, we are all pirates by nature; and the rules I'm talking about here
are really more like guidelines...
(И запомните: мы все пираты по натуре, и правила, о которых я говорю тут,
на самом деле, скорее рекомендации...)

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

Итак, слой Entities содержит:

  • Entities – функции или объекты с методами, которые реализуют логику бизнеса, общую для многих приложений (а если бизнеса нет, то самую высокоуровневую логику приложения);
  • DTO, необходимые для работы и перехода между слоями.

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

UseCase и/или Interactor

Многие путаются в понятиях UseCase и Interactor. Мы ни раз слышали фразы типа: «Канонического определения Interactor нет». Или вопросы типа: «Мне делать это в Interactor'e или вынести в UseCase?».

Косвенное определение Interactor'a встречается в статье, которая уже упомянута в самом начале. Оно звучит так:

«...interactor object that implements the use case by invoking business objects.»

Таким образом:

Interactor – объект, который реализует use case (сценарий использования), используя бизнес-объекты (Entities).

Что же такое Use Case или сценарий использования? Uncle Bob в видео выступлении говорит о книге «Object-Oriented Software Engineering: A Use Case Driven Approach», которую написал Ivar Jacobson в 1992 году, и о том, как тот описывает Use Case.

Use case – это детализация, описание действия, которое может совершить пользователь системы.

Вот пример, который приводится в видео:

usecase in video

Это Use Case для создания заказа, причём выполняемый клерком.

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

Первый пункт – даже не часть Use Case'a, это его старт – клерк запускает команду для создания заказа с нужными данными.

Далее шаги:

  • Система валидирует данные. Не оговаривается как.
  • Система создает заказ и id заказа. Подразумевается использование БД, но это не важно пока, не уточняется. Как-то создает и всё.
  • Система доставляет id заказа клерку. Не уточняется как. Легко представить, что id возвращается не клерку, а, например, выводится на страницу сайта. То есть Use Case никак не зависит от деталей реализации.

Ivar Jacobson предложил реализовать этот Use Case в объекте, который назвал ControlObject. Но Uncle Bob решил, что это плохая идея, так как путается с Controller из MVC и стал называть такой объект Interactor. И он говорит, что мог бы назвать его UseCase. Это можно посмотреть примерно в этом моменте видео.

Там же он говорит, что Interactor реализует use case и имеет метод для запуска execute() и получается, что это паттерн Команда. Интересно.

Когда кто-то говорит, что у Interactor'a нет четкого определения – он не прав. Определение есть и оно вполне четкое. Выше мы привели несколько источников.

Многим нравится объединять Interactor'ы в один общий с набором методов, реализующих use case'ы. Если вам сильно не нравятся отдельные классы, можете так делать, это ваше решение. Но отдельные Interactor'ы лучше, так как это даёт больше гибкости.

А вот давать определение: «Интерактор – это набор UseCase'ов», – вот это уже плохо. А такое определение бытует. Оно ошибочно с точки зрения оригинального толкования термина и вводит начинающих в непонимания, когда в коде получается одновременно есть и UseCase классы и Interactor классы, хотя всё это одно и то же.

Мы призываем не вводить друг друга в заблуждения и использовать названия Interactor и UseCase, не меняя их изначальный смысл: Interactor/UseCase – объект, реализующий use case (сценарий использования).

За примером того, чем плохо, когда одно название толкуется по-разному, далеко ходить не надо, такой пример рядом – паттерн Repository.

Доступ к данным

Для доступа к данным удобно использовать какой-либо паттерн, позволяющий скрыть процесс их получения. Uncle Bob в своей схеме использует Gateway, но сейчас куда сильнее распространен Repository.

Repository

А что из себя представляет паттерн Repository? Вот тут и возникает проблема, потому что оригинальное определение и то, как мы понимаем репозиторий сейчас (и как его описывает Fernando Cejas в своей статье), фундаментально различаются.

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

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

Подробнее об этом можно прочесть в статье Hannes Dorfmann'а.

Gateway

Сначала мы тоже начали использовать Repository, но воспринимая слово «репозиторий» в значении хранилища, нам не нравилось наличие там методов для работы с сервером типа login() (да, работа с сервером тоже идет через Repository, ведь в конце концов для приложения сервер – это та же база данных, только расположенная удаленно).

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

А в обсуждениях все равно приходится использовать слово «репозиторий», всем так проще.

Доступ к Repository/Gateway только через Interactor?

Многие настаивают, что это единственный правильный способ. И они правы! В идеале использовать Repository нужно только через Interactor.

Но мы не видем ничего страшного, чтобы в простых случаях, когда не нужно никакой логики обработки данных, вызывать Repository из Presenter'a, минуя Interactor.

Repository и презентер находятся на одном слое, Dependency Rule не запрещает нам использовать Repository напрямую. Единственное но – возможное добавления логики в Interactor в будущем. Но добавить Interactor, когда понадобится, не сложно, а иметь множество proxy-interactor'ов, просто прокидывающих вызов в репозиторий, не всегда хочется.

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

Обязательность маппинга между слоями

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

А можно использовать DTO из слоя Entities везде во внешних слоях. Конечно, если те могут его использовать. Нарушения Dependency Rule тут нет.

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

Маппинг DTO на каждом слое:

  • Изменение данных в одном слое не затрагивает другой слой;
  • Аннотации, нужные для какой-то библиотеки не попадут в другие слои;
  • Может быть много дублирования;
  • При изменении данных все равно приходится менять маппер.

Использование DTO из слоя Enitities:

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

Хорошее рассуждение есть вот по этой ссылке.

С выводами автора ответа мы полностью согласены:

Если у вас сложное приложение с логикой бизнеса и логикой приложения, и/или разные люди работают над разными слоями, то лучше разделять данные между слоями (и маппить их). Также это стоит делать, если серверное API корявое. Но если вы работаете над проектом один, и это простое приложение, то не усложняйте лишним маппингом.

Mаппинг в Interactor'e

Да, такое существует. Приведем фразу из оригинальной cтатьи: So when we pass data across a boundary, it is always in the form that is most convenient for the inner circle. (Когда мы передаем данные между слоями, они всегда в форме более удобной для внутреннего слоя)

Поэтому в Interactor данные должны попадать уже в нужном ему виде. Маппинг происходит в слое Interface Adapters, то есть в Presenter и Repository.

А где раскладывать объекты?

С сервера нам приходят данные в разном виде. И иногда API навязывает нам странные вещи. Например, в ответ на login() может прийти объект Profile и объект OrderState. И, конечно же, мы хотим сохранить эти объекты в разных Repository.

Так где же нам разобрать LoginResponse и разложить Profile и OrderState по нужным репозиториям, в Interactor'e или в Repository?

Многие делают это в Interactor'e. Так проще, т.к. не надо иметь зависимости между репозиториями и разрывать иногда возникающую кроссылочность.

Но мы делаем это в Repository. По двум причинам:

  • Если мы делаем это в Interactor'e, значит мы должны передать ему LoginResponse в каком-то виде. Но тогда, чтобы не нарушать Dependency Rule, LoginResponse должен находиться в слое Interactor'a (UseCases) или Entities. А ему там не место, ведь он им кроме как для раскладывания ни для чего больше не нужен.
  • Раскладывание данных – не дело для use case. Мы же не станем писать пункт в описании действия доступного пользователю: «Получить данные, разложить данные». Скорее мы напишем просто: «Получить нужные данные»,– и всё.

Если вам удобно делать это в Interactor, то делайте, но считайте это компромиссом.

Можно ли объединить Interactor и Repository?

Некоторым нравится объединять Interactor и Repository. В основном это вызвано желанием избежать решения проблемы, описанной в пункте «Доступ к Repository/Gateway только через Interactor?».

Но в оригинале Clean Architecture эти сущности не смешиваются. И на это пара веских причин:

  • Они на разных слоях.
  • Они выполняют различные функции.

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

Что лучше Clean Architecture или MVP?

Смешно, да? А некоторые спрашивают такое в чатах. Быстро поясним:

  • Архитектура затрагивает всё ваше приложение. И Clean – не исключение.
  • А презентационные паттерны, например MVP, затрагивают лишь часть, отвечающую за отображение и взаимодействие с UI. Чтобы лучше понять эти паттерны, мы рекомендуем почитать статью нашего коллеги @dmdev.

Clean Architecture в первых проектах

В последнее время архитектура приложений на слуху. Даже Google решили выпустить свои Architecture Components.

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

Конечно, если вам все понятно и есть на это время – то супер. Но если сложно, то не надо себя мучить, делайте проще, набирайтесь опыта.

Однако до сих пор в этой главе документации описывались только концепции и идеомы из Clean Architecture на уровне понимания самой идеи разделения на слои.

Конечно же, если у программиста достаточно опыта - он может все сделать с нуля самостоятельно своими руками. Однако мы изначально поставили перед собой задачу - создать удобный, эффективный, модульный фреймворк LeanES, чтобы воплотить в нем весь наш накопленный опыт. Чтобы он помогал даже не опытным программистам создавать высококачественные приложения, предоставляя свод правил, каркас приложения. А рамки дозволенного будут сдерживать программиста от заведомо неверных реализаций.

Ранее были описаны идеомы из спецификации PureMVC, которые были имплементированы в виде базовых классов в фреймворке в первую очередь. В последствие стало ясно, что PureMVC является лишь ограниченной реализацией Clean Architecture с тремя выделенными слоями:

  • слой Entities отсутствует, т.е. может быть реализован на усмотрение разработчика
  • слой UseCases представлен Командами
  • слой Interface Adapters представлен Медиаторами и Прокси
  • слой Frameworks представлен в виде viewComponent внутри Медиатора и dataObject внутри Прокси

Но Команды в PureMVC все же не на 100% соответствуют концепции UseCases, т.к. инстанцируются и выполняются непосредственно в Controller классе (это не тот контроллер который описывается на уровне Interface Adapters). Но главное, эти Команды предназначены только для запуска через отправку Оповещения, но не для инъекции в качестве зависимости в другие сущности.

Конечно произвести инъекцию и напрямую вызвать метот execute() нам явно не запрещено, но все же цель Команды в PureMVC быть вызванной для обработки Оповещения с одним единственным Интерфейсом.

Поэтому в Фасад были интегрированы дополнительные сущности и методы для работы с ними в рамках концепции "Clean Architecture" by Robert C. Martin (Uncle Bob). Эти сущности Кейс (Case), Сьюит (Suite) и Адаптер (Adapter) только расширяют уровни абстракций в составе PureMVC с сохранением обратной совместимости с эталонной спецификацией.

Кейс (Case) в явном виде говорит о том, что предназначен быть Базовым классом для определения конкретных UseCases в приложении. И конечно разработчик может объявить любые Интерфейсы, которые будут реализовывать его конкретные UseCases. Эти Интерфейсы он будет использовать при инъекции зависимостей в тех классах, в которые он будет инъектировать реализованные в приложении Кейсы.

К сожалению Сущность (Entity) - слишком абстрактное название для класса и несмотря на вышеизложенные объяснения в Clean Architecture, закладывать Базовый класс с таким именем в фреймворке слишком рискованно (эту документацию разработчики могут и не прочить вовсе, но использовать Entity сущность в своем приложении не по назначению). Поэтому Сущность в фреймворке реализуется через Базовый класс Сьюит (Suite) и полностью соответствует предназначению Entity.

Все необходимое для работы с данными в общем случае всегда может быть реализовано внутри Прокси, однако ввиду распростаненности использования паттерна Repository мы решили реализовать дополнительный Базовый класс Адаптер (Adapter). Разработчик может определить конкретный RepositoryProxy в своем приложении, а так же набор Адаптеров для работы с разными данными и/или API серверами.

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

Переосмысление Entities

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

Изначально было не понятно как "так" устроены Entities, чтобы Dependency Rule были направлены вовнутырь Сущности из Кейса. При том, что в описании Сущности передаются в Кейс якобы как "данные" - что только увеличивает замешательство.

Однако на верный ход мыслей натолкнула все та же схема:

Flow of control

Следовательно, чтобы не нарушить Поток управления, Кейсы должны реализовывать такие интерфейсы, через которые они будут инъектироваться внутрь Сущности.

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

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

Например:

import type { FirstCaseInterface } from '../interfaces/FirstCaseInterface';
import type { SecondCaseInterface } from '../interfaces/SecondCaseInterface';
import type { ThirdCaseInterface } from '../interfaces/ThirdCaseInterface';

export default (Module) => {
  const {
    FIRST_CASE, SECOND_CASE, THIRD_CASE,
    Suite,
    initialize, partOf, meta, method, property, nameBy, inject,
  } = Module.NS;

  @initialize
  @partOf(Module)
  class CheckoutOrderSuite extends Suite {
    @nameBy static  __filename = __filename;
    @meta static object = {};

    @inject(`Factory<${FIRST_CASE}>`)
    @property _firstCaseFactory: () => FirstCaseInterface;
    @property get _firstCase(): FirstCaseInterface {
      return this._firstCaseFactory()
    }

    @inject(`Factory<${SECOND_CASE}>`)
    @property _secondCaseFactory: () => SecondCaseInterface;
    @property get _secondCase(): SecondCaseInterface {
      return this._secondCaseFactory()
    }

    @inject(`Factory<${THIRD_CASE}>`)
    @property _thirdCaseFactory: () => ThirdCaseInterface;
    @property get _thirdCase(): ThirdCaseInterface {
      return this._thirdCaseFactory()
    }

    @method async perform(): void {
      if (await this._firstCase.isFirstUserPurchase()) {
        await this._secondCase.makeGift()
      }
      await this._thirdCase.calculateDiscount()
    }
  }
}