Чистая Архитектура предлагает позволить случай использования Interactor назвать фактическую реализацию ведущих (который вводится, после DIP) для обработки ответа / дисплея. Тем не менее, я вижу людей, реализующих эту архитектуру, возвращающих выходные данные из интерактора, а затем позволяющих контроллеру (на уровне адаптера) решать, как с ним работать. Является ли второе решение утечкой ответственности приложения из уровня приложения, в дополнение к нечеткому определению входных и выходных портов для интерактора?
Порты ввода и вывода
Принимая во внимание определение чистой архитектуры и особенно небольшую блок-схему, описывающую отношения между контроллером, интерактивом варианта использования и докладчиком, я не уверен, правильно ли я понимаю, каким должен быть «Порт вывода варианта использования».
Чистая архитектура, как и гексагональная архитектура, различает первичные порты (методы) и вторичные порты (интерфейсы, реализуемые адаптерами). Следуя коммуникационному потоку, я ожидаю, что «Use Case Input Port» будет основным портом (таким образом, просто методом), а «Use Case Output Port» интерфейсом, который будет реализован, возможно, аргумент конструктора, принимающий фактический адаптер, так что интерактор может его использовать.
Пример кода
Чтобы сделать пример кода, это может быть код контроллера:
Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();
Интерфейс докладчика:
// Use Case Output Port
interface Presenter
{
public void present(Data data);
}
Наконец, сам интерактор:
class UseCase
{
private Repository repository;
private Presenter presenter;
public UseCase(Repository repository, Presenter presenter)
{
this.repository = repository;
this.presenter = presenter;
}
// Use Case Input Port
public void doSomething()
{
Data data = this.repository.getData();
this.presenter.present(data);
}
}
На интеракторе звонит ведущий
Предыдущая интерпретация, кажется, подтверждается самой вышеупомянутой диаграммой, где отношение между контроллером и входным портом представлено сплошной стрелкой с «острой» головкой (UML для «ассоциации», что означает «имеет», где Контроллер "имеет" вариант использования), в то время как связь между презентатором и выходным портом представлена сплошной стрелкой с "белой" головкой (UML для "наследования", что не является "реализацией", но, вероятно, это смысл в любом случае).
Кроме того, в этом ответе на другой вопрос Роберт Мартин точно описывает случай использования, когда интерактор вызывает докладчика по запросу чтения:
Нажатие на карту приводит к тому, что вызывается либо placePinController. Он собирает местоположение щелчка и любые другие контекстные данные, создает структуру данных placePinRequest и передает ее в PlacePinInteractor, который проверяет местоположение булавки, проверяет его при необходимости, создает объект Place для записи булавки, создает EditPlaceReponse объект и передает его в EditPlacePresenter, который вызывает экран редактора места.
Чтобы это хорошо работало с MVC, я мог бы подумать, что логика приложения, которая традиционно идет в контроллер, здесь перенесена в интерактор, потому что мы не хотим, чтобы какая-либо логика приложения просачивалась за пределы уровня приложения. Контроллер на уровне адаптеров просто вызовет интерактор и, возможно, в процессе выполнит небольшое преобразование формата данных:
Программное обеспечение в этом слое представляет собой набор адаптеров, которые преобразуют данные из формата, наиболее удобного для вариантов использования и сущностей, в формат, наиболее удобный для некоторого внешнего агентства, такого как База данных или Интернет.
из оригинальной статьи, говоря об интерфейсных адаптерах.
На интеракторе, возвращающем данные
Однако моя проблема с этим подходом состоит в том, что сценарий использования должен заботиться о самой презентации. Теперь я вижу, что цель Presenter
интерфейса состоит в том, чтобы быть достаточно абстрактным, чтобы представлять несколько различных типов презентаторов (GUI, Web, CLI и т. Д.), И что он на самом деле просто означает «вывод», что может быть примером использования очень хорошо, но все же я не совсем уверен в этом.
Сейчас, просматривая в Интернете приложения чистой архитектуры, я, похоже, обнаружил только людей, которые интерпретируют выходной порт как метод, возвращающий некоторое DTO. Это было бы что-то вроде:
Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);
// I'm omitting the changes to the classes, which are fairly obvious
Это привлекательно, потому что мы снимаем с себя ответственность «вызова» презентации из варианта использования, поэтому сценарий использования не заботится о том, чтобы больше знать, что делать с данными, а просто предоставляет данные. Кроме того, в этом случае мы все еще не нарушаем правило зависимости, потому что вариант использования все еще ничего не знает о внешнем слое.
Тем не менее, вариант использования не контролирует момент, когда фактическая презентация выполняется больше (что может быть полезно, например, для выполнения дополнительных действий в этот момент, таких как ведение журнала, или для полного отмены при необходимости). Кроме того, обратите внимание, что мы потеряли порт ввода варианта использования, потому что теперь контроллер использует только getData()
метод (который является нашим новым портом вывода). Кроме того, мне кажется, что мы здесь нарушаем принцип «говори, не спрашивай», потому что мы просим у интерактора какие-то данные, чтобы что-то с ним сделать, вместо того, чтобы сказать, чтобы они действовали в первое место.
К точке
Итак, является ли какая-либо из этих двух альтернатив «правильной» интерпретацией порта вывода варианта использования в соответствии с чистой архитектурой? Они оба жизнеспособны?
Ответы:
Это, конечно, не Чистая , Лук или Шестиугольная Архитектура. Вот это :
Не то, чтобы MVC был сделан таким образом
Вы можете использовать много разных способов общения между модулями и называть его MVC . Сказать, что что-то использует MVC, на самом деле не говорит мне, как взаимодействуют компоненты Это не стандартизировано. Все, что он говорит мне, состоит в том, что есть по крайней мере три компонента, сосредоточенные на их трех обязанностях.
Некоторые из этих способов получили разные названия :
И каждый из них можно по праву назвать MVC.
Во всяком случае, ни один из этих способов не отражает того, что архитектуры от умных слов (Clean, Onion и Hex) просят вас сделать.
Добавьте структуры данных, которые перемещаются (и переверните их по некоторым причинам), и вы получите :
Здесь должно быть ясно, что модель ответа не проходит через контроллер.
Если вы неравнодушны, вы могли заметить, что только архитектуры модных слов полностью избегают циклических зависимостей . Важно отметить, что это означает, что влияние изменения кода не будет распространяться при циклическом переключении между компонентами. Изменение остановится, когда оно попадет в код, который не заботится об этом.
Интересно, перевернули ли они его вверх дном, чтобы поток управления проходил по часовой стрелке. Подробнее об этом и этих "белых" наконечниках стрел позже.
Поскольку связь между контроллером и презентатором должна проходить через «слой» приложения, то да, заставить контроллер выполнять часть работы докладчиков, скорее всего, утечка. Это моя главная критика архитектуры VIPER .
Почему их разделение так важно, возможно, лучше понять, изучив разделение ответственности командных запросов .
Это API, через который вы отправляете вывод, для этого конкретного случая использования. Это не более того. Интерактору для этого варианта использования не нужно ни знать, ни хотеть знать, идет ли вывод в GUI, CLI, журнал или аудиоколонку. Все, что нужно знать интерактору, - это самый простой API, который позволит ему сообщать о результатах своей работы.
Причина, по которой выходной порт отличается от входного, состоит в том, что он не должен быть СОЗДАН уровнем, который он абстрагирует. То есть уровень, который он абстрагирует, не должен позволять диктовать изменения в нем. Только уровень приложения и его автор должны решить, что порт вывода может измениться.
Это в отличие от входного порта, который принадлежит уровню, который он абстрагирует. Только автор прикладного уровня должен решить, должен ли измениться его входной порт.
Следование этим правилам сохраняет идею о том, что прикладной уровень или любой внутренний уровень вообще ничего не знает о внешних уровнях.
Важной особенностью этой «белой» стрелки является то, что она позволяет вам делать это:
Вы можете позволить потоку управления идти в противоположном направлении зависимости! Это означает, что внутренний слой не должен знать о внешнем слое, и все же вы можете погрузиться во внутренний слой и вернуться обратно!
Это не имеет ничего общего с использованием ключевого слова "interface". Вы можете сделать это с помощью абстрактного класса. Черт возьми, вы можете сделать это с (ick) конкретным классом, если он может быть расширен. Просто приятно делать это с чем-то, что фокусируется только на определении API, который Presenter должен реализовать. Открытая стрелка требует только полиморфизма. Какой вид зависит от вас.
Почему изменение направления этой зависимости так важно, можно узнать, изучив Принцип инверсии зависимости . Я отобразил этот принцип на этих диаграммах здесь .
Нет, это действительно так. Чтобы удостовериться, что внутренние слои не знают о внешних слоях, мы можем удалять, заменять или подвергать рефакторингу внешние слои, уверенные, что это ничего не сломает во внутренних слоях. То, о чем они не знают, не повредит им. Если мы можем сделать это, мы можем изменить внешние на то, что мы хотим.
Проблема здесь в том, что теперь любой, кто знает, как запрашивать данные, также должен принимать данные. Прежде, чем Контроллер смог вызвать Usecase Interactor, блаженно не зная, как будет выглядеть Модель ответа, куда она должна идти и, хех, как ее представить.
Опять же, пожалуйста, изучите разделение Ответственность за запросы команд, чтобы понять, почему это важно.
Да! Рассказ, не спрашивая, поможет сохранить этот объект ориентированным, а не процедурным.
Все, что работает, жизнеспособно. Но я бы не сказал, что второй вариант, который вы верно представили, следует за «Чистой архитектурой». Это может быть что-то, что работает. Но это не то, что просит Чистая Архитектура.
источник
В обсуждении, связанном с вашим вопросом , дядя Боб объясняет цель докладчика в его «Чистой архитектуре»:
Учитывая этот пример кода:
Дядя Боб сказал это:
(ОБНОВЛЕНИЕ: 31 мая 2019 г.)
Учитывая этот ответ дяди Боба, я думаю, что не имеет большого значения, делаем ли мы вариант № 1 (пусть интерактор использует презентатор) ...
... или мы делаем вариант № 2 (пусть интерактор возвращает ответ, создает презентатора внутри контроллера, затем передает ответ презентатору) ...
Лично я предпочитаю вариант # 1 , потому что я хочу , чтобы иметь возможность управления внутри ,
interactor
когда для отображения данных и сообщений об ошибках, как в этом примере ниже:... Я хочу иметь возможность делать
if/else
то, что связано с презентацией внутри,interactor
а не снаружи интерактора.Если с другой стороны , мы делаем вариант # 2, мы должны сохранить сообщение (ы) ошибок в
response
объекте, вернуть этотresponse
объект отinteractor
кcontroller
, и сделатьcontroller
синтаксический анализ наresponse
предмет ...Мне не нравится анализ
response
данных на наличие ошибок внутри,controller
потому что если мы делаем это, мы выполняем избыточную работу - если мы что-то меняем вinteractor
, мы также должны что-то менять вcontroller
.Кроме того, если мы позже решим повторно использовать наши
interactor
данные для представления, например, с помощью консоли, мы должны не забыть скопировать и вставить все эти данныеif/else
вcontroller
нашем консольном приложении.Если мы используем вариант # 1 мы будем иметь это
if/else
только в одном месте :interactor
.Если вы используете ASP.NET MVC (или другие подобные MVC-фреймворки), вариант №2 - более простой способ.
Но мы все еще можем сделать вариант № 1 в такой среде. Вот пример выполнения опции # 1 в ASP.NET MVC:
(Обратите внимание, что нам нужно иметь
public IActionResult Result
в представителе нашего приложения ASP.NET MVC)(Обратите внимание, что нам нужно иметь
public IActionResult Result
в представителе нашего приложения ASP.NET MVC)Если мы решим создать другое приложение для консоли, мы можем использовать
UseCase
вышеупомянутое и создать только дляController
иPresenter
для консоли:(Обратите внимание, что мы не имеем в ведущем
public IActionResult Result
нашего консольного приложения)источник
Вариант использования может содержать как докладчик, так и возвращаемые данные, в зависимости от того, что требуется потоку приложения.
Давайте разберемся в нескольких терминах, прежде чем понимать различные потоки приложений:
Вариант использования, содержащий возвращаемые данные
В обычном случае вариант использования просто возвращает объектный уровень на прикладной уровень, который может быть дополнительно обработан на прикладном уровне, чтобы его было удобно отображать в пользовательском интерфейсе.
Поскольку контроллер отвечает за запуск сценария использования, в этом случае он также содержит ссылку на соответствующий презентатор, чтобы провести домен для просмотра отображения модели перед отправкой его для просмотра для визуализации.
Вот упрощенный пример кода:
Вариант использования, содержащий Presenter
Хотя это и не распространено, но возможно, что в случае использования может потребоваться вызвать докладчика. В этом случае вместо того, чтобы содержать конкретную ссылку докладчика, желательно рассматривать интерфейс (или абстрактный класс) в качестве контрольной точки (которая должна быть инициализирована во время выполнения посредством внедрения зависимости).
Наличие домена для просмотра логики отображения модели в отдельном классе (а не внутри контроллера) также нарушает циклическую зависимость между контроллером и вариантом использования (когда класс сценария использования требует обращения к логике отображения).
Ниже приведена упрощенная реализация потока управления, как показано в исходной статье, которая демонстрирует, как это можно сделать. Обратите внимание, что в отличие от диаграммы, для простоты UseCaseInteractor является конкретным классом.
источник
Несмотря на то, что я в целом согласен с ответом @CandiedOrange, я также вижу выгоду в подходе, в котором интерактор просто перезапускает данные, которые затем передаются контроллером докладчику.
Это, например, простой способ использовать идеи чистой архитектуры (правила зависимости) в контексте Asp.Net MVC.
Я написал сообщение в блоге, чтобы углубиться в это обсуждение: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/
источник
Короче
Да, они оба жизнеспособны, если оба подхода учитывают инверсию контроля между бизнес-уровнем и механизмом доставки. Со вторым подходом мы все еще можем представить МОК, используя наблюдателя, посредника и несколько других шаблонов проектирования ...
Своей « чистой архитектурой » попытка дяди Боба состоит в том, чтобы объединить несколько известных архитектур, чтобы раскрыть важные концепции и компоненты, которые позволят нам широко соблюдать принципы ООП.
Было бы контрпродуктивно рассматривать его диаграмму классов UML (диаграмма ниже) как уникальный проект чистой архитектуры . Эту диаграмму можно было бы нарисовать ради конкретных примеров ... Однако, поскольку она гораздо менее абстрактна, чем обычные архитектурные представления, ему пришлось сделать конкретный выбор, среди которых дизайн выходного порта интерактора, который является лишь деталью реализации ...
Мои два цента
Основная причина , почему я предпочитаю возвращая в
UseCaseResponse
том , что этот подход сохраняет мои случаи использования гибкой , что позволяет как композиции между ними и типичность ( обобщение и конкретные поколения ). Основной пример:Обратите внимание, что он аналогично ближе к сценариям использования UML, включая / расширяя друг друга, и определяется как многократно используемый для разных предметов (сущностей).
Не уверены, что понимаете, что вы имеете в виду под этим, зачем вам нужно «контролировать» представление презентации? Разве вы не контролируете это, пока не возвращаете ответ варианта использования?
Вариант использования может вернуть в своем ответе код состояния, чтобы сообщить клиентскому уровню, что именно произошло во время его работы. Коды состояния ответа HTTP особенно хорошо подходят для описания рабочего состояния варианта использования…
источник