Чиста архітектура: використовувати корпус, що містить презентатора або повертаючі дані?


42

Чистий Архітектура пропонує дозволити випадок використання Interactor назвати фактичну реалізацію провідних (який вводиться, після DIP) для обробки відповіді / дисплея. Однак я бачу людей, що реалізують цю архітектуру, повертають вихідні дані з інтерактора, а потім дозволяють контролеру (у адаптерному шарі) вирішувати, як з ним поводитися. Чи є друге рішення, що витікає з додаткового рівня обов'язки програми, окрім чіткого визначення портів вводу та виведення в інтерактор?

Порти введення та виведення

Враховуючи визначення «Чиста архітектура» , і особливо маленьку схему потоку, що описує взаємозв’язки між контролером, інтерактором випадку використання та презентатором, я не впевнений, чи правильно я розумію, яким повинен бути «Порт використання вихідного випадку».

Чиста архітектура, як і гексагональна архітектура, розрізняє первинні порти (методи) та вторинні порти (інтерфейси, які слід реалізувати адаптерами). Після комунікаційного потоку я очікую, що "Порт введення випадку використання" є основним портом (таким чином, просто методом), а "Використовувати вихідний порт виходу", який буде реалізований інтерфейс, можливо, аргумент конструктора з фактичним адаптером, щоб інтерактор міг ним користуватися.

Приклад коду

Щоб зробити приклад коду, це може бути код контролера:

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, який перевіряє розташування штифта, при необхідності перевіряє його, створює сутність місця для запису PIN-коду, будує 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

Це привабливо, оскільки ми перекладаємо відповідальність за "виклик" презентації із випадку використання, тому випадок використання вже не стосується того, щоб знати, що робити з даними, а не просто наданням даних. Також у цьому випадку ми все ще не порушуємо правило залежності, оскільки випадок використання все ще нічого не знає про зовнішній шар.

Однак випадок використання не контролює момент, коли фактична презентація вже виконується (що може бути корисним, наприклад, робити додаткові речі в цей момент, як-от реєстрація, або при необхідності взагалі відміняти його). Також зауважте, що ми втратили Use Case Input Port, оскільки зараз контролер використовує лише getData()метод (який є нашим новим вихідним портом). Крім того, мені здається, що тут ми порушуємо принцип "скажи, не запитуй", тому що ми просимо інтерактора для того, щоб зробити щось із цим, а не казати йому робити фактичну справу в першість.

До суті

Отже, чи є одна з цих двох альтернатив «правильною» інтерпретацією вихідного порту Use Case відповідно до чистої архітектури? Вони обидва життєздатні?


3
Перехресне опублікування сильно не рекомендується. Якщо ви хочете, щоб ваше запитання жило, тоді вам слід видалити його з переповнення стека.
Роберт Харві

Відповіді:


48

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

Це, звичайно, не чиста , цибульна або шестикутна архітектура. Ось це :

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

Не те, що MVC має бути зроблено саме так

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

Ви можете використовувати багато різних способів спілкування між модулями та називати його MVC . Розповідаючи мені про те, що використовує MVC, насправді не розказує, як спілкуються компоненти. Це не стандартизовано. Все, що мені підказує, - це щонайменше три компоненти, зосереджені на їхніх трьох обов'язках.

Деякі з цих способів отримали різні назви : введіть тут опис зображення

І кожного з них можна по праву назвати MVC.

У будь-якому випадку, ніхто з цих не реально захоплює те, що всі архітектури модних слів (Clean, Onion і Hex) просять вас зробити.

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

Додайте структури даних, що їх обкидають (і чомусь переверніть їх догори дном), і ви отримаєте :

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

Одне, що повинно бути зрозуміло, - це те, що модель реагування не проходить через контролер.

Якщо у вас є орел, то, можливо, ви помітили, що лише архітектура модників повністю уникає кругових залежностей . Що важливо, це означає, що вплив зміни коду не поширюватиметься на велосипеді через компоненти. Зміна припиняється, коли вона потрапляє до коду, який її не хвилює.

Цікаво, якщо вони перевернули її догори дном, щоб потік управління пройшов через годинникову стрілку. Детальніше про це, і ці "білі" стрілки, пізніше.

Чи є друге рішення, що витікає з додаткового рівня обов'язки програми, окрім чіткого визначення портів вводу та виведення в інтерактор?

Оскільки комунікація від Controller до Presenter призначена для проходження програми "шару", то так, так, щоб зробити контролер частиною роботи презентації, швидше за все, витік. Це моя головна критика архітектури VIPER .

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

Порти введення та виведення

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

Це API, через який ви надсилаєте вихід, для цього конкретного випадку використання. Це не більше того. Інтерактор для цього випадку використання не повинен знати і не хоче знати, чи буде вихідний графічний інтерфейс, CLI, журнал або динамік аудіо. Все, що потрібно знати, - це найпростіший можливий API, який дозволить йому звітувати про результати своєї роботи.

Чиста архітектура, як і гексагональна архітектура, розрізняє первинні порти (методи) та вторинні порти (інтерфейси, які слід реалізувати адаптерами). Після комунікаційного потоку я очікую, що "Порт введення випадку використання" є основним портом (таким чином, просто методом), а "Використовувати вихідний порт виходу", який буде реалізований інтерфейс, можливо, аргумент конструктора з фактичним адаптером, щоб інтерактор міг ним користуватися.

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

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

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


Про інтерактор, що дзвонить ведучому

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

Важлива річ у білій стрілці - це те, що вона дозволяє вам це робити:

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

Ви можете відпустити потік управління у зворотному напрямку залежності! Це означає, що внутрішній шар не повинен знати про зовнішній шар, і все ж ви можете зануритися у внутрішній шар і повернутися назад!

Це не має нічого спільного з використанням ключового слова "інтерфейс". Це можна зробити за допомогою абстрактного класу. Чорт ви можете зробити це з (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, не знаючи, як виглядатиме модель відповіді, куди вона має йти, і, хе, як її представити.

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

Однак випадок використання не контролює момент, коли фактична презентація вже виконується (що може бути корисним, наприклад, робити додаткові речі в цей момент, як-от реєстрація, або при необхідності взагалі відміняти його). Також зауважте, що ми втратили порт вводу Use Case, оскільки зараз контролер використовує лише метод getData () (який є нашим новим вихідним портом). Крім того, мені здається, що тут ми порушуємо принцип "скажи, не запитуй", тому що ми просимо інтерактора для того, щоб зробити щось із цим, а не казати йому робити фактичну справу в першість.

Так! Розповідання, не запитуючи, допоможе зберегти цей об’єкт, а не процедурний.

До суті

Отже, чи є одна з цих двох альтернатив «правильною» інтерпретацією вихідного порту Use Case відповідно до чистої архітектури? Вони обидва життєздатні?

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


4
Дякуємо, що знайшли час для написання такого глибокого пояснення.
swahnee

1
Я намагався обернути голову навколо чистої архітектури, і ця відповідь була фантастичним ресурсом. Дуже добре зроблено!
Натан

Чудова і детальна відповідь. Дякую за це ... Чи можете ви дати мені поради (або вкажіть на пояснення) щодо оновлення графічного інтерфейсу під час запуску UseCase, тобто оновлення панелі прогресу під час завантаження великого файлу?
Евокс

1
@Ewoks, як швидка відповідь на ваше запитання, ви повинні розглянути шаблон спостереження. Ваш випадок використання може повернути Тему та сповістити Тему про оновлення. Ведучий підписався на тему та відповів на сповіщення.
Натан

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 є дата. Це поле буде бинарним об'єктом дати, який може бути представлений у багатьох різних форматах дат. Хоче дуже специфічний формат дати, можливо, 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в презентаторі нашої консольної програми)


Дякуємо за внесок. Читаючи розмову, однак, я не розумію одного: він каже, що ведучий повинен надавати дані, отримані від відповіді, і в той же час, що відповідь не повинен створюватися інтерактором. Але тоді хто створює відповідь? Я б сказав, що інтерактор повинен надавати дані презентатору у конкретному форматі додатку, який це знає презентатор, оскільки рівень адаптерів може залежати від рівня програми (але не навпаки).
swahnee

Мені шкода. Можливо, це стає заплутаним, тому що я не включив приклад коду з обговорення. Я оновлю його, щоб включити приклад коду.
Jboy Flaga

Дядько Боб не сказав, що відповідь не повинен створювати інтерактор. Відповідь буде створений інтерактором . Що говорить дядько Боб, це те, що ведучий використає відповідь, створену інтерактором. Потім ведучий "відформатує його", покладе відформатовану відповідь у перегляд, а потім передасть цю модель перегляду. <br/> Ось як я це розумію.
Jboy Flaga

1
Це має більше сенсу. У мене було враження, що "view" є синонімом "презентатор", оскільки Clean Architecture не згадує ні "view", ні "viewmodel", що, на мою думку, є виключно концепціями MVC, які можуть бути, а можуть і не використовуватися при впровадженні перехідник.
swahnee

2

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

Давайте розберемося з кількома термінами, перш ніж зрозуміти різні потоки додатків:

  • Об'єкт домену : Об'єкт домену - це контейнер даних у доменному шарі, на якому здійснюються операції бізнес-логіки.
  • Модель перегляду : Об'єкти домену зазвичай відображаються для перегляду моделей на рівні додатків, щоб зробити їх сумісними та зручними для користувальницького інтерфейсу.
  • Презентатор : Хоча контролер на рівні додатків, як правило, викликає випадок використання, але доцільно делегувати домен для перегляду логіки відображення моделі в окремий клас (за принципом єдиної відповідальності), який називається «Presenter».

Випадок використання, що містить дані, що повертаються

У звичайному випадку випадок використання просто повертає об’єкт домену до рівня додатків, який може бути додатково оброблений у шарі додатка, щоб зробити його зручним для показу в інтерфейсі користувача.

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

Ось спрощений зразок коду:

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;
        }

    }
}

Справа, що містить презентатор

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

Наявність домену для перегляду логіки відображення моделі в окремому класі (замість внутрішнього контролера) також розбиває кругову залежність між контролером та випадком використання (коли посилання на логіку відображення вимагається класом case use).

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

Нижче наводиться спрощена реалізація потоку управління, як показано в оригінальній статті, що демонструє, як це можна зробити. Зверніть увагу, що на відміну від показаних на схемі, для простоти 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, я також побачив би користь у підході, коли інтерактор просто повертає дані, які потім передаються контролером презентатору.

Наприклад, це простий спосіб використовувати ідеї чистої архітектури (правило залежності) в контексті MVC Asp.Net.

Я написав повідомлення в блозі, щоб зануритися в цю дискусію: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/


1

Використовувати регістр, що містить ведучого або повертає дані?

Отже, чи є одна з цих двох альтернатив «правильною» інтерпретацією вихідного порту Use Case відповідно до чистої архітектури? Вони обидва життєздатні?


Коротко

Так, вони обоє життєздатні до тих пір, поки обидва підходи враховують інверсію контролю між бізнес-рівнем та механізмом доставки. З другим підходом ми все ще можемо запровадити МОК, використовуючи спостерігача, посередника, декілька інших моделей дизайну ...

Своєю чистою архітектурою дядько Боб намагається синтезувати купу відомих архітектур, щоб розкрити важливі для нас концепції та компоненти, щоб широко відповідати принципам ООП.

Було б протилежним вважати його діаграму класів 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 особливо добре підходять для опису стану операції у випадку використання ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.