Чистая архитектура: вариант использования, содержащий докладчика или возвращающие данные?

42

Чистая Архитектура предлагает позволить случай использования 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()метод (который является нашим новым портом вывода). Кроме того, мне кажется, что мы здесь нарушаем принцип «говори, не спрашивай», потому что мы просим у интерактора какие-то данные, чтобы что-то с ним сделать, вместо того, чтобы сказать, чтобы они действовали в первое место.

К точке

Итак, является ли какая-либо из этих двух альтернатив «правильной» интерпретацией порта вывода варианта использования в соответствии с чистой архитектурой? Они оба жизнеспособны?

swahnee
источник
3
Кросс-постинг категорически не рекомендуется. Если вы хотите, чтобы ваш вопрос жил, вы должны удалить его из Переполнения стека.
Роберт Харви,

Ответы:

48

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

Это, конечно, не Чистая , Лук или Шестиугольная Архитектура. Вот это :

введите описание изображения здесь

Не то, чтобы MVC был сделан таким образом

введите описание изображения здесь

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

Некоторые из этих способов получили разные названия : введите описание изображения здесь

И каждый из них можно по праву назвать MVC.

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

введите описание изображения здесь

Добавьте структуры данных, которые перемещаются (и переверните их по некоторым причинам), и вы получите :

введите описание изображения здесь

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

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

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

Является ли второе решение утечкой ответственности приложения из уровня приложения, в дополнение к нечеткому определению входных и выходных портов для интерактора?

Поскольку связь между контроллером и презентатором должна проходить через «слой» приложения, то да, заставить контроллер выполнять часть работы докладчиков, скорее всего, утечка. Это моя главная критика архитектуры VIPER .

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

Порты ввода и вывода

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

Это API, через который вы отправляете вывод, для этого конкретного случая использования. Это не более того. Интерактору для этого варианта использования не нужно ни знать, ни хотеть знать, идет ли вывод в GUI, CLI, журнал или аудиоколонку. Все, что нужно знать интерактору, - это самый простой API, который позволит ему сообщать о результатах своей работы.

Чистая архитектура, как и гексагональная архитектура, различает первичные порты (методы) и вторичные порты (интерфейсы, реализуемые адаптерами). Следуя коммуникационному потоку, я ожидаю, что «Use Case Input Port» будет основным портом (таким образом, просто методом), а «Use Case Output Port» интерфейсом, который будет реализован, возможно, аргумент конструктора, принимающий фактический адаптер, так что интерактор может его использовать.

Причина, по которой выходной порт отличается от входного, состоит в том, что он не должен быть СОЗДАН уровнем, который он абстрагирует. То есть уровень, который он абстрагирует, не должен позволять диктовать изменения в нем. Только уровень приложения и его автор должны решить, что порт вывода может измениться.

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

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


На интеракторе звонит ведущий

Предыдущая интерпретация, кажется, подтверждается самой вышеупомянутой диаграммой, где отношение между контроллером и входным портом представлено сплошной стрелкой с «острой» головкой (UML для «ассоциации», что означает «имеет», где Контроллер "имеет" вариант использования), в то время как связь между презентатором и выходным портом представлена ​​сплошной стрелкой с "белой" головкой (UML для "наследования", что не является "реализацией", но, вероятно, это смысл в любом случае).

Важной особенностью этой «белой» стрелки является то, что она позволяет вам делать это:

введите описание изображения здесь

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

Это не имеет ничего общего с использованием ключевого слова "interface". Вы можете сделать это с помощью абстрактного класса. Черт возьми, вы можете сделать это с (ick) конкретным классом, если он может быть расширен. Просто приятно делать это с чем-то, что фокусируется только на определении API, который Presenter должен реализовать. Открытая стрелка требует только полиморфизма. Какой вид зависит от вас.

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

На интеракторе, возвращающем данные

Однако моя проблема с этим подходом состоит в том, что сценарий использования должен заботиться о самой презентации. Теперь я вижу, что цель интерфейса 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

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

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

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

Тем не менее, вариант использования не контролирует момент, когда фактическое представление больше выполняется (что может быть полезно, например, для выполнения дополнительных действий в этот момент, таких как ведение журнала или полное прерывание при необходимости). Также обратите внимание, что мы потеряли порт ввода прецедента, потому что теперь контроллер использует только метод getData () (который является нашим новым портом вывода). Более того, мне кажется, что мы здесь нарушаем принцип «говори, не спрашивай», потому что мы просим у интерактора какие-то данные, чтобы что-то с ним сделать, вместо того, чтобы сказать, чтобы они действовали в первое место.

Да! Рассказ, не спрашивая, поможет сохранить этот объект ориентированным, а не процедурным.

К точке

Итак, является ли какая-либо из этих двух альтернатив «правильной» интерпретацией порта вывода варианта использования в соответствии с чистой архитектурой? Они оба жизнеспособны?

Все, что работает, жизнеспособно. Но я бы не сказал, что второй вариант, который вы верно представили, следует за «Чистой архитектурой». Это может быть что-то, что работает. Но это не то, что просит Чистая Архитектура.

candied_orange
источник
4
Спасибо, что нашли время, чтобы написать такое подробное объяснение.
Суахни
1
Я пытался обернуть голову вокруг Чистой Архитектуры, и этот ответ был фантастическим ресурсом. Очень хорошо сделано!
Натан
Отличный и подробный ответ .. Спасибо за это. Можете ли вы дать мне несколько советов (или указать объяснение) об обновлении GUI во время запуска UseCase, то есть обновления индикатора выполнения при загрузке большого файла?
Эвокс
1
@Ewoks, как быстрый ответ на ваш вопрос, вы должны посмотреть на шаблон Observable. Ваш вариант использования может вернуть предмет и уведомить субъекта о ходе обновления. Докладчик будет подписываться на тему и отвечать на уведомления.
Натан
7

В обсуждении, связанном с вашим вопросом , дядя Боб объясняет цель докладчика в его «Чистой архитектуре»:

Учитывая этот пример кода:

namespace Some\Controller;

class UserController extends Controller {
    public function registerAction() {
        // Build the Request object
        $request = new RegisterRequest();
        $request->name = $this->getRequest()->get('username');
        $request->pass = $this->getRequest()->get('password');

        // Build the Interactor
        $usecase = new RegisterUser();

        // Execute the Interactors method and retrieve the response
        $response = $usecase->register($request);

        // Pass the result to the view
        $this->render(
            '/user/registration/template.html.twig', 
            array('id' =>  $response->getId()
        );
    }
}

Дядя Боб сказал это:

« Цель докладчика состоит в том, чтобы отделить варианты использования от формата пользовательского интерфейса. В вашем примере переменная $ response создается интерактором, но используется представлением. Это связывает интерактор с представлением. Например, предположим, что одно из полей в объекте $ response является датой. Это поле будет двоичным объектом даты, который может быть представлен во многих различных форматах даты. Требуется очень конкретный формат даты, возможно, DD / MM / YYYY. Кто несет ответственность за создание формата? Если интерактор создает этот формат, то он слишком много знает о представлении. Но если представление принимает двоичный объект даты, то оно слишком много знает об интеракторе.

«Работа докладчика заключается в том, чтобы взять данные из объекта ответа и отформатируйте его для просмотра. Ни представление, ни интерактор не знают о форматах друг друга. "

--- дядя боб

(ОБНОВЛЕНИЕ: 31 мая 2019 г.)

Учитывая этот ответ дяди Боба, я думаю, что не имеет большого значения, делаем ли мы вариант № 1 (пусть интерактор использует презентатор) ...

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... или мы делаем вариант № 2 (пусть интерактор возвращает ответ, создает презентатора внутри контроллера, затем передает ответ презентатору) ...

class Controller
{
    public void ExecuteUseCase(Data data)
    {
        Request request = ...
        UseCase useCase = new UseCase(repository);
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        presenter.Show(response);
    }
}

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

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... Я хочу иметь возможность делать if/elseто, что связано с презентацией внутри, interactorа не снаружи интерактора.

Если с другой стороны , мы делаем вариант # 2, мы должны сохранить сообщение (ы) ошибок в responseобъекте, вернуть этот responseобъект от interactorк controller, и сделать controller синтаксический анализ на responseпредмет ...

class UseCase
{
    public Response Execute(Request request)
    {
        Response response = new Response();
        if (<invalid request>) 
        {
            response.AddError("...");
        }

        if (<there is another error>) 
        {
            response.AddError("another error...");
        }

        if (response.HasNoErrors)
        {
            response.Whatever = ...
        }

        ...
        return response;
    }
}
class Controller
{
    private UseCase useCase;

    public Controller(UseCase useCase)
    {
        this.useCase = useCase;
    }

    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        if (response.ErrorMessages.Count > 0)
        {
            if (response.ErrorMessages.Contains(<invalid request>))
            {
                presenter.ShowError("...");
            }
            else if (response.ErrorMessages.Contains("another error")
            {
                presenter.ShowError("another error...");
            }
        }
        else
        {
            presenter.Show(response);
        }
    }
}

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

Кроме того, если мы позже решим повторно использовать наши interactorданные для представления, например, с помощью консоли, мы должны не забыть скопировать и вставить все эти данные if/elseв controllerнашем консольном приложении.

// in the controller for our console app
if (response.ErrorMessages.Count > 0)
{
    if (response.ErrorMessages.Contains(<invalid request>))
    {
        presenterForConsole.ShowError("...");
    }
    else if (response.ErrorMessages.Contains("another error")
    {
        presenterForConsole.ShowError("another error...");
    }
}
else
{
    presenterForConsole.Present(response);
}

Если мы используем вариант # 1 мы будем иметь это if/else только в одном месте : interactor.


Если вы используете ASP.NET MVC (или другие подобные MVC-фреймворки), вариант №2 - более простой способ.

Но мы все еще можем сделать вариант № 1 в такой среде. Вот пример выполнения опции # 1 в ASP.NET MVC:

(Обратите внимание, что нам нужно иметь public IActionResult Resultв представителе нашего приложения ASP.NET MVC)

class UseCase
{
    private Repository repository;

    public UseCase(Repository repository)
    {
        this.repository = repository;
    }

    public void Execute(Request request, Presenter presenter)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {
            ...
        }
        this.presenter.Show(response);
    }
}
// controller for ASP.NET app

class AspNetController
{
    private UseCase useCase;

    public AspNetController(UseCase useCase)
    {
        this.useCase = useCase;
    }

    [HttpPost("dosomething")]
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new AspNetPresenter();
        useCase.Execute(request, presenter);
        return presenter.Result;
    }
}
// presenter for ASP.NET app

public class AspNetPresenter
{
    public IActionResult Result { get; private set; }

    public AspNetPresenter(...)
    {
    }

    public async void Show(Response response)
    {
        Result = new OkObjectResult(new { });
    }

    public void ShowError(string errorMessage)
    {
        Result = new BadRequestObjectResult(errorMessage);
    }
}

(Обратите внимание, что нам нужно иметь public IActionResult Resultв представителе нашего приложения ASP.NET MVC)

Если мы решим создать другое приложение для консоли, мы можем использовать UseCaseвышеупомянутое и создать только для Controllerи Presenterдля консоли:

// controller for console app

class ConsoleController
{    
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new ConsolePresenter();
        useCase.Execute(request, presenter);
    }
}
// presenter for console app

public class ConsolePresenter
{
    public ConsolePresenter(...)
    {
    }

    public async void Show(Response response)
    {
        // write response to console
    }

    public void ShowError(string errorMessage)
    {
        Console.WriteLine("Error: " + errorMessage);
    }
}

(Обратите внимание, что мы не имеем в ведущем public IActionResult Resultнашего консольного приложения)

Jboy Flaga
источник
Спасибо за вклад. Однако, читая беседу, я не понимаю одну вещь: он говорит, что докладчик должен отображать данные, поступающие из ответа, и в то же время, что ответ не должен создаваться интерактором. Но тогда, кто создает ответ? Я бы сказал, что интерактор должен предоставлять данные докладчику в формате приложения, который известен докладчику, поскольку уровень адаптеров может зависеть от уровня приложения (но не наоборот).
Суахни
Мне жаль. Может быть, это сбивает с толку, потому что я не включил пример кода из обсуждения. Я обновлю его, чтобы включить пример кода.
Jboy Flaga
Дядя Боб не сказал, что ответ не должен быть создан интерактором. Ответ будет создан интерактором . Дядя Боб говорит, что созданный интерактором ответ будет использоваться докладчиком. Затем докладчик "отформатирует его", поместит отформатированный ответ в модель представления, а затем передаст эту модель представления. <br/> Вот как я понимаю.
Jboy Flaga
1
Это имеет больше смысла. У меня сложилось впечатление, что «представление» является синонимом «презентатор», поскольку в «Чистой архитектуре» не упоминается ни «представление», ни «представление», что, на мой взгляд, является исключительно концепциями MVC, которые могут или не могут использоваться при реализации адаптер.
Суахни
2

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

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

  • Доменный объект : объект домена - это контейнер данных на уровне домена, на котором выполняются операции бизнес-логики.
  • Просмотр модели : доменные объекты обычно отображаются для просмотра моделей на уровне приложения, чтобы сделать их совместимыми и дружественными к пользовательскому интерфейсу.
  • Presenter : Хотя контроллер на прикладном уровне обычно вызывает сценарий использования, но рекомендуется делегировать домен для просмотра логики сопоставления модели отдельному классу (следуя принципу единой ответственности), который называется «Presenter».

Вариант использования, содержащий возвращаемые данные

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

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

Вот упрощенный пример кода:

namespace SimpleCleanArchitecture
{
    public class OutputDTO
    {
        //fields
    }

    public class Presenter 
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    public class Domain
    {
        //fields
    }

    public class UseCaseInteractor
    {
        public Domain Process(Domain domain)
        {
            // additional processing takes place here
            return domain;
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            UseCaseInteractor userCase = new UseCaseInteractor();
            var domain = userCase.Process(new Domain());//passing dummy domain(for demonstration purpose) to process
            var presenter = new Presenter();//presenter might be initiated via dependency injection.

            return new View(presenter.Present(domain));
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

Вариант использования, содержащий Presenter

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

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

введите описание изображения здесь

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

namespace CleanArchitectureWithPresenterInUseCase
{
    public class Domain
    {
        //fields
    }

    public class OutputDTO
    {
        //fields
    }

    // Use Case Output Port
    public interface IPresenter
    {
        OutputDTO Present(Domain domain);
    }

    public class Presenter: IPresenter
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    // Use Case Input Port / Interactor   
    public class UseCaseInteractor
    {
        IPresenter _presenter;
        public UseCaseInteractor (IPresenter presenter)
        {
            _presenter = presenter;
        }

        public OutputDTO Process(Domain domain)
        {
            return _presenter.Present(domain);
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            IPresenter presenter = new Presenter();//presenter might be initiated via dependency injection.
            UseCaseInteractor userCase = new UseCaseInteractor(presenter);
            var outputDTO = userCase.Process(new Domain());//passing dummy domain (for demonstration purpose) to process
            return new View(outputDTO);
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}
Ашраф
источник
1

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

Это, например, простой способ использовать идеи чистой архитектуры (правила зависимости) в контексте Asp.Net MVC.

Я написал сообщение в блоге, чтобы углубиться в это обсуждение: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

plainionist
источник
1

Вариант использования, содержащий докладчика или возвращающие данные?

Итак, является ли какая-либо из этих двух альтернатив «правильной» интерпретацией порта вывода варианта использования в соответствии с чистой архитектурой? Они оба жизнеспособны?


Короче

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

Своей « чистой архитектурой » попытка дяди Боба состоит в том, чтобы объединить несколько известных архитектур, чтобы раскрыть важные концепции и компоненты, которые позволят нам широко соблюдать принципы ООП.

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

Диаграмма классов UML дяди Боба «Чистая архитектура»


Мои два цента

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

// A generic "entity type agnostic" use case encapsulating the interaction logic itself.
class UpdateUseCase implements UpdateUseCaseInterface
{
    function __construct(EntityGatewayInterface $entityGateway, GetUseCaseInterface $getUseCase)
    {
        $this->entityGateway = $entityGateway;
        $this->getUseCase = $getUseCase;
    }

    public function execute(UpdateUseCaseRequestInterface $request) : UpdateUseCaseResponseInterface
    {
        $getUseCaseResponse = $this->getUseCase->execute($request);

        // Update the entity and build the response...

        return $response;
    }
}

// "entity type aware" use cases encapsulating the interaction logic WITH the specific entity type.
final class UpdatePostUseCase extends UpdateUseCase;
final class UpdateProductUseCase extends UpdateUseCase;

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


На интеракторе, возвращающем данные

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

Не уверены, что понимаете, что вы имеете в виду под этим, зачем вам нужно «контролировать» представление презентации? Разве вы не контролируете это, пока не возвращаете ответ варианта использования?

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

ClemC
источник