Czysta architektura: czy skrzynka zawiera prezentera lub zwraca dane?

42

Clean Architektura sugeruje niech przypadek użycia interaktora nazwać rzeczywistą realizację prezenter (który jest wtryskiwany w następstwie DIP), aby obsłużyć odpowiedzi / wyświetlacz. Widzę jednak osoby wdrażające tę architekturę, zwracające dane wyjściowe z interactor, a następnie pozwól kontrolerowi (w warstwie adaptera) zdecydować, jak sobie z tym poradzić. Czy drugie rozwiązanie wycieka obowiązki aplikacji poza warstwę aplikacji, oprócz nieokreślonego zdefiniowania portów wejściowych i wyjściowych do modułu pośredniczącego?

Porty wejściowe i wyjściowe

Biorąc pod uwagę definicję czystej architektury , a zwłaszcza mały schemat blokowy opisujący relacje między kontrolerem, interaktorem przypadku użycia i prezenterem, nie jestem pewien, czy poprawnie rozumiem, co powinien być „Port wyjściowy przypadku użycia”.

Czysta architektura, podobnie jak architektura heksagonalna, rozróżnia porty pierwotne (metody) i porty pomocnicze (interfejsy do wdrożenia przez adaptery). Po przepływie komunikacji oczekuję, że „Use Input Input Port” będzie portem podstawowym (a więc tylko metoda), a „Use Case Output Port” interfejs, który ma zostać zaimplementowany, być może argument konstruktora biorący rzeczywisty adapter, aby interactor mógł z niego skorzystać.

Przykład kodu

Na przykład kod może być kodem kontrolera:

Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();

Interfejs prezentera:

// Use Case Output Port
interface Presenter
{
    public void present(Data data);
}

Wreszcie sam interaktor:

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

Na interaktorze wzywającym prezentera

Poprzednią interpretację wydaje się potwierdzać sam wspomniany schemat, w którym relacja między kontrolerem a portem wejściowym jest reprezentowana przez ciągłą strzałkę z „ostrą” głową (UML dla „asocjacji”, co oznacza „ma”, gdzie kontroler „ma„ przypadek użycia), podczas gdy relacja między prezenterem a portem wyjściowym jest reprezentowana przez ciągłą strzałkę z „białą” główką (UML dla „dziedziczenia”, który nie jest tym dla „implementacji”, ale prawdopodobnie to i tak ma znaczenie).

Ponadto w tej odpowiedzi na inne pytanie Robert Martin opisuje dokładnie przypadek użycia, w którym interaktor wywołuje prezentera na żądanie odczytu:

Kliknięcie mapy powoduje wywołanie kontrolera placePinController. Gromadzi lokalizację kliknięcia i wszelkie inne dane kontekstowe, konstruuje strukturę danych placePinRequest i przekazuje ją do PlacePinInteractor, który sprawdza lokalizację pinezki, weryfikuje ją w razie potrzeby, tworzy encję Place, aby zarejestrować pin, konstruuje EditPlaceReponse obiekt i przekazuje go do EditPlacePresenter, który wyświetla ekran edytora miejsc.

Aby ta gra dobrze działała w MVC, mogłem pomyśleć, że logika aplikacji, która tradycyjnie trafiałaby do kontrolera, została przeniesiona do interactor, ponieważ nie chcemy, aby logika aplikacji wyciekła poza warstwę aplikacji. Kontroler w warstwie adapterów po prostu wywołałby interactor, a być może wykonałby niewielką konwersję formatu danych w tym procesie:

Oprogramowanie w tej warstwie to zestaw adapterów, które konwertują dane z formatu najwygodniejszego dla przypadków użycia i encji na format najwygodniejszy dla niektórych zewnętrznych agencji, takich jak Baza danych lub Internet.

z oryginalnego artykułu, mówiąc o adapterach interfejsów.

Na interactor zwracających dane

Jednak moim problemem z tym podejściem jest to, że przypadek użycia musi zająć się samą prezentacją. Widzę teraz, że celem Presenterinterfejsu jest wystarczająco abstrakcyjne przedstawienie różnych typów prezentacji (GUI, WWW, CLI itp.) I że tak naprawdę oznacza to po prostu „wynik”, co może być przypadkiem użycia bardzo dobrze, ale wciąż nie jestem do tego całkowicie pewny.

Teraz, gdy szukam w Internecie aplikacji o czystej architekturze, wydaje mi się, że ludzie interpretują port wyjściowy jako metodę zwracającą trochę DTO. To byłoby coś w stylu:

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

Jest to atrakcyjne, ponieważ przenosimy odpowiedzialność za „wywołanie” prezentacji poza przypadek użycia, więc przypadek użycia nie dotyczy już wiedzy o tym, co zrobić z danymi, a jedynie dostarczenie danych. Ponadto w tym przypadku nadal nie łamiemy reguły zależności, ponieważ przypadek użycia nadal nie wie nic o warstwie zewnętrznej.

Jednak przypadek użycia nie kontroluje już momentu, w którym rzeczywista prezentacja jest wykonywana (co może być przydatne, na przykład w celu wykonania dodatkowych czynności w tym momencie, takich jak rejestrowanie, lub całkowite przerwanie, jeśli to konieczne). Zauważ też, że straciliśmy port wejściowy Case Use, ponieważ teraz kontroler używa tylko getData()metody (która jest naszym nowym portem wyjściowym). Co więcej, wydaje mi się, że łamiemy tutaj zasadę „mów, nie pytaj”, ponieważ prosimy interaktora o pewne dane, aby coś z tym zrobić, zamiast nakazać mu wykonanie rzeczywistej czynności w pierwsze miejsce.

Do momentu

Czy więc któraś z tych dwóch alternatyw jest „poprawną” interpretacją portu wyjściowego przypadku użycia zgodnie z czystą architekturą? Czy oba są opłacalne?

swahnee
źródło
3
Zdecydowanie odradza się zamieszczanie postów. Jeśli tutaj chcesz, aby twoje pytanie mieszkało, powinieneś usunąć je z Przepełnienia stosu.
Robert Harvey

Odpowiedzi:

48

Czysta architektura sugeruje, aby pozwolić interaktorowi przypadku użycia wywołać rzeczywistą implementację prezentera (który jest wstrzykiwany po DIP) w celu obsługi odpowiedzi / wyświetlania. Widzę jednak osoby wdrażające tę architekturę, zwracające dane wyjściowe z interactor, a następnie pozwól kontrolerowi (w warstwie adaptera) zdecydować, jak sobie z tym poradzić.

Z pewnością nie jest to architektura czysta , cebulowa ani heksagonalna . To jest to :

wprowadź opis zdjęcia tutaj

Nie to, że MVC trzeba tak zrobić

wprowadź opis zdjęcia tutaj

Możesz użyć wielu różnych sposobów komunikacji między modułami i nazwać to MVC . Mówienie mi, że coś korzysta z MVC, tak naprawdę nie mówi mi, w jaki sposób komunikują się komponenty. To nie jest ustandaryzowane. Wszystko, co mi mówi, to to, że co najmniej trzy elementy koncentrują się na ich trzech obowiązkach.

Niektóre z tych sposobów otrzymały różne nazwy : wprowadź opis zdjęcia tutaj

I każdy z nich można słusznie nazwać MVC.

Tak czy inaczej, żaden z nich tak naprawdę nie uchwycił tego, o co proszą cię modne hasła (Clean, Onion i Hex).

wprowadź opis zdjęcia tutaj

Dodaj rzutowane struktury danych (i z jakiegoś powodu odwróć je do góry nogami), a otrzymasz :

wprowadź opis zdjęcia tutaj

Jedną z rzeczy, które powinny tu być jasne, jest to, że model odpowiedzi nie przechodzi przez kontroler.

Jeśli jesteś orłem, możesz zauważyć, że tylko architektury modne całkowicie unikają okrągłych zależności . Co ważne, oznacza to, że wpływ zmiany kodu nie rozprzestrzeni się poprzez cykliczne przechodzenie między komponentami. Zmiana zatrzyma się, gdy trafi w kod, który go nie obchodzi.

Zastanawiam się, czy odwrócili go do góry nogami, aby przepływ kontroli przebiegał zgodnie z ruchem wskazówek zegara. Więcej na ten temat, a później te „białe” strzały.

Czy drugie rozwiązanie wycieka obowiązki aplikacji poza warstwę aplikacji, oprócz nieokreślonego zdefiniowania portów wejściowych i wyjściowych do modułu pośredniczącego?

Ponieważ komunikacja między kontrolerem a prezenterem ma przechodzić przez „warstwę” aplikacji, tak, włączenie kontrolera do zadania prezentera jest prawdopodobnie wyciekiem. To moja główna krytyka architektury VIPER .

Dlaczego oddzielenie ich jest tak ważne, prawdopodobnie najlepiej można je zrozumieć, analizując Segregację Odpowiedzialności za Polecenia .

Porty wejściowe i wyjściowe

Biorąc pod uwagę definicję czystej architektury, a zwłaszcza mały schemat blokowy opisujący relacje między kontrolerem, interaktorem przypadku użycia i prezenterem, nie jestem pewien, czy poprawnie rozumiem, co powinien być „Port wyjściowy przypadku użycia”.

Jest to interfejs API, przez który wysyłasz dane wyjściowe, w tym konkretnym przypadku użycia. To nic więcej. Interaktor dla tego przypadku użycia nie musi wiedzieć ani nie chce wiedzieć, czy dane wyjściowe trafią do GUI, CLI, dziennika lub głośnika audio. Wszystko, co interactor musi wiedzieć, to najprostszy możliwy interfejs API, który pozwoli mu raportować wyniki swojej pracy.

Czysta architektura, podobnie jak architektura heksagonalna, rozróżnia porty pierwotne (metody) i porty pomocnicze (interfejsy do wdrożenia przez adaptery). Po przepływie komunikacji oczekuję, że „Use Input Input Port” będzie portem podstawowym (a więc tylko metoda), a „Use Case Output Port” interfejs, który ma zostać zaimplementowany, być może argument konstruktora biorący rzeczywisty adapter, aby interactor mógł z niego skorzystać.

Powodem, dla którego port wyjściowy jest inny niż port wejściowy, jest to, że warstwa, którą abstraktuje, nie może być WŁASNA. Oznacza to, że warstwa, którą abstrakuje, nie może narzucać zmian. Tylko warstwa aplikacji i jej autor powinni zdecydować, że port wyjściowy może się zmienić.

Jest to w przeciwieństwie do portu wejściowego, który jest własnością warstwy, którą abstraktuje. Tylko autor warstwy aplikacji powinien zdecydować, czy port wejściowy powinien się zmienić.

Przestrzeganie tych zasad zachowuje pogląd, że warstwa aplikacji lub jakakolwiek warstwa wewnętrzna nie wie nic o zewnętrznych warstwach.


Na interaktorze wzywającym prezentera

Poprzednią interpretację wydaje się potwierdzać sam wspomniany schemat, w którym relacja między kontrolerem a portem wejściowym jest reprezentowana przez ciągłą strzałkę z „ostrą” głową (UML dla „asocjacji”, co oznacza „ma”, gdzie kontroler „ma„ przypadek użycia), podczas gdy relacja między prezenterem a portem wyjściowym jest reprezentowana przez ciągłą strzałkę z „białą” główką (UML dla „dziedziczenia”, który nie jest tym dla „implementacji”, ale prawdopodobnie to i tak ma znaczenie).

Ważną rzeczą w tej „białej” strzałce jest to, że pozwala ci to zrobić:

wprowadź opis zdjęcia tutaj

Możesz pozwolić, aby przepływ kontroli poszedł w przeciwnym kierunku zależności! Oznacza to, że wewnętrzna warstwa nie musi wiedzieć o zewnętrznej warstwie, a jednak możesz zanurzyć się w wewnętrznej warstwie i wrócić!

Nie ma to nic wspólnego z użyciem słowa kluczowego „interfejs”. Możesz to zrobić za pomocą klasy abstrakcyjnej. Heck, możesz to zrobić za pomocą (ick) konkretnej klasy, o ile można ją przedłużyć. Po prostu miło jest to zrobić z czymś, co koncentruje się tylko na zdefiniowaniu interfejsu API, który musi zaimplementować Presenter. Otwarta strzała prosi tylko o polimorfizm. Jakiego rodzaju zależy od ciebie.

Dlaczego odwrócenie kierunku tej zależności jest tak ważne, można się dowiedzieć, badając zasadę odwrócenia zależności . Mapowałem tę zasadę na tych diagramach tutaj .

Na interactor zwracających dane

Jednak moim problemem z tym podejściem jest to, że przypadek użycia musi zająć się samą prezentacją. Teraz widzę, że celem interfejsu prezentera jest na tyle abstrakcja, aby reprezentować kilka różnych typów prezenterów (GUI, sieć, CLI itp.), I że tak naprawdę oznacza to po prostu „wynik”, co jest przypadkiem użycia może bardzo dobrze, ale wciąż nie jestem do tego całkowicie pewny.

Nie, to naprawdę to. Chodzi o to, aby wewnętrzne warstwy nie wiedziały o zewnętrznych warstwach, że możemy usunąć, wymienić lub przefakturować zewnętrzne warstwy, mając pewność, że w ten sposób nic nie zepsujesz w wewnętrznych warstwach. To, czego nie wiedzą, nie skrzywdzi ich. Jeśli możemy to zrobić, możemy zmienić zewnętrzne na cokolwiek chcemy.

Teraz, gdy szukam w Internecie aplikacji o czystej architekturze, wydaje mi się, że ludzie interpretują port wyjściowy jako metodę zwracającą trochę DTO. To byłoby coś w stylu:

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

Jest to atrakcyjne, ponieważ przenosimy odpowiedzialność za „wywołanie” prezentacji poza przypadek użycia, więc przypadek użycia nie dotyczy już wiedzy o tym, co zrobić z danymi, a jedynie dostarczenie danych. Ponadto w tym przypadku nadal nie łamiemy reguły zależności, ponieważ przypadek użycia nadal nie wie nic o warstwie zewnętrznej.

Problem w tym, że teraz wszystko, co wie, jak poprosić o dane, musi być także tym, co je akceptuje. Zanim kontroler mógłby zadzwonić do Usecase Interactor błogo nieświadomy tego, jak wyglądałby model odpowiedzi, gdzie powinien iść i, heh, jak go przedstawić.

Ponownie, proszę przestudiuj Segregację Odpowiedzialności za Polecenia, aby zobaczyć, dlaczego jest to ważne.

Jednak przypadek użycia nie kontroluje już momentu, w którym rzeczywista prezentacja jest wykonywana (co może być przydatne, na przykład w celu wykonania dodatkowych czynności w tym momencie, takich jak rejestrowanie, lub całkowite przerwanie, jeśli to konieczne). Zauważ też, że straciliśmy port wejściowy Case Use, ponieważ teraz kontroler używa tylko metody getData () (która jest naszym nowym portem wyjściowym). Co więcej, wydaje mi się, że łamiemy tutaj zasadę „mów, nie pytaj”, ponieważ prosimy interaktora o pewne dane, aby coś z tym zrobić, zamiast nakazać mu wykonanie rzeczywistej czynności w pierwsze miejsce.

Tak! Mówienie, a nie pytanie, pomoże utrzymać ten obiekt zorientowany, a nie proceduralny.

Do momentu

Czy więc któraś z tych dwóch alternatyw jest „poprawną” interpretacją portu wyjściowego przypadku użycia zgodnie z czystą architekturą? Czy oba są opłacalne?

Wszystko, co działa, jest opłacalne. Ale nie powiedziałbym, że druga opcja, którą przedstawiłeś, jest wierna czystej architekturze. To może być coś, co działa. Ale nie tego wymaga czysta architektura.

candied_orange
źródło
4
Dziękujemy za poświęcenie czasu na napisanie tak dogłębnego wyjaśnienia.
swahnee
1
Próbowałem owinąć głowę wokół Clean Architecture, a ta odpowiedź była fantastycznym źródłem. Bardzo dobrze zrobione!
Nathan
Świetna i szczegółowa odpowiedź .. Dziękuję za to .. Czy możesz mi dać kilka wskazówek (lub wskazać wyjaśnienie) na temat aktualizacji GUI podczas uruchamiania UseCase, tj. Aktualizacji paska postępu podczas przesyłania dużego pliku?
Ewoks,
1
@Ewoks, jako szybka odpowiedź na twoje pytanie, powinieneś przyjrzeć się wzorowi obserwowalnym. Twój przypadek użycia może zwrócić Temat i powiadomić Temat o aktualizacjach postępu. Prezenter zasubskrybuje Temat i odpowie na Powiadomienia.
Nathan
7

W dyskusji dotyczącej twojego pytania wujek Bob wyjaśnia cel prezentera w swojej Czystej architekturze:

Biorąc pod uwagę ten przykładowy kod:

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

Wujek Bob powiedział:

Celem prezentera jest oddzielenie przypadków użycia od formatu interfejsu użytkownika. W twoim przykładzie zmienna $ response jest tworzona przez interactor, ale jest używana przez widok. To łączy interactor z widokiem. Na przykład , powiedzmy, że jednym z pól w obiekcie $ response jest data. To pole byłoby binarnym obiektem daty, który mógłby być renderowany w wielu różnych formatach dat. Potrzebuje bardzo określonego formatu daty, być może DD / MM / RRRR. Czyją odpowiedzialnością jest utworzenie formatu? Jeśli interaktor tworzy ten format, to wie zbyt dużo o Widoku. Ale jeśli widok przyjmuje obiekt daty binarnej, to wie za dużo o interaktorze.

”Zadaniem prezentera jest wzięcie dane z obiektu odpowiedzi i sformatuj je dla Widoku. Ani widok, ani interactor nie wiedzą o swoich formatach.

--- Wujek Bob

(AKTUALIZACJA: 31 maja 2019 r.)

Biorąc pod uwagę tę odpowiedź wuja Boba, myślę, że nie ma większego znaczenia , czy zrobimy opcję nr 1 (pozwól interaktorowi użyć prezentera) ...

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

... lub wykonujemy opcję # 2 (pozwól interaktorowi zwrócić odpowiedź, utwórz prezentera wewnątrz kontrolera, a następnie przekaż odpowiedź prezenterowi) ...

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

Osobiście preferuję opcję nr 1 , ponieważ chcę, aby móc kontrola wewnątrz interactor kiedy pokazać dane i komunikaty o błędach, jak w poniższym przykładzie poniżej:

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

... Chcę mieć możliwość robienia tych, if/elsektóre są związane z prezentacją w interactorinteraktorze, a nie poza nim.

Jeśli z drugiej strony mamy opcję nr 2 zrobić, trzeba by zapisać się komunikat o błędzie (y) w responseobiekcie, zwrot ten responseobiekt od interactordo controller, i sprawiają, że controller zanalizować ten responseobiekt ...

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

Nie lubię analizować responsedanych pod kątem błędów wewnątrz, controllerponieważ jeśli to zrobimy, wykonujemy zbędną pracę --- jeśli zmienimy coś w interactor, musimy również coś zmienić w controller.

Ponadto, jeśli później postanowimy ponownie wykorzystać nasze interactordane do prezentacji za pomocą konsoli, musimy pamiętać, aby skopiować i wkleić wszystkie te if/elsew controllernaszej aplikacji na konsolę.

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

Jeśli używamy opcji nr 1 będziemy mieli to if/else tylko w jednym miejscu : the interactor.


Jeśli używasz platformy ASP.NET MVC (lub innych podobnych platform MVC), łatwiej jest wybrać opcję nr 2 .

Ale nadal możemy zrobić opcję nr 1 w tego rodzaju środowisku. Oto przykład wykonania opcji nr 1 w ASP.NET MVC:

(Zauważ, że musimy mieć public IActionResult Resultw prezencie naszej aplikacji 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);
    }
}

(Zauważ, że musimy mieć public IActionResult Resultw prezencie naszej aplikacji ASP.NET MVC)

Jeśli zdecydujemy się stworzyć kolejną aplikację dla konsoli, możemy ponownie wykorzystać UseCasepowyżej tworzyć tylko Controlleri Presenterdo konsoli:

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

(Zauważ, że NIE MAMY public IActionResult Resultw prezencie naszej aplikacji na konsolę)

Jboy Flaga
źródło
Dzięki za wkład. Jednak czytając rozmowę, nie rozumiem jednej rzeczy: mówi, że prezenter powinien renderować dane pochodzące z odpowiedzi, a jednocześnie interakcja nie powinna tworzyć odpowiedzi. Ale kto tworzy odpowiedź? Powiedziałbym, że interactor powinien dostarczyć dane do prezentera, w formacie specyficznym dla aplikacji, który jest znany prezenterowi, ponieważ warstwa adapterów może zależeć od warstwy aplikacji (ale nie na odwrót).
swahnee
Przykro mi. Może robi się to mylące, ponieważ nie uwzględniłem przykładu kodu z dyskusji. Zaktualizuję go, aby zawierał przykład kodu.
Jboy Flaga
Wujek Bob nie powiedział, że interakcja nie powinna tworzyć odpowiedzi. Odpowiedź zostanie utworzona przez interaktora . Wujek Bob mówi, że prezenter wykorzysta odpowiedź utworzoną przez interaktora. Prezenter następnie „sformatuje”, umieści sformatowaną odpowiedź w viewmodelu, a następnie przekaże ten viewmodel do widoku. <br/> Tak to rozumiem.
Jboy Flaga
1
To ma więcej sensu. Miałem wrażenie, że „widok” był synonimem „prezentera”, ponieważ Clean Architecture nie wspomina ani o „view”, ani „viewmodel”, które moim zdaniem są wyłącznie koncepcjami MVC, które mogą, ale nie muszą być użyte podczas implementacji adapter.
swahnee
2

Przypadek użycia może zawierać prezentera lub zwracane dane, w zależności od wymagań aplikacji.

Rozumiemy kilka terminów, zanim zrozumiemy różne przepływy aplikacji:

  • Obiekt domeny : Obiekt domeny to pojemnik danych w warstwie domeny, na którym wykonywane są operacje logiki biznesowej.
  • Wyświetl model : Obiekty domeny są zwykle odwzorowywane, aby wyświetlać modele w warstwie aplikacji, aby były kompatybilne i przyjazne dla interfejsu użytkownika.
  • Prezenter : Podczas gdy kontroler w warstwie aplikacji zwykle wywołuje przypadek użycia, ale wskazane jest delegowanie domeny, aby wyświetlić logikę mapowania modelu do oddzielnej klasy (zgodnie z zasadą pojedynczej odpowiedzialności), która nazywa się „Prezenter”.

Przypadek użycia zawierający zwracane dane

W zwykłym przypadku przypadek użycia po prostu zwraca obiekt domeny do warstwy aplikacji, którą można dalej przetwarzać w warstwie aplikacji, aby ułatwić wyświetlanie w interfejsie użytkownika.

Ponieważ kontroler jest odpowiedzialny za wywołanie przypadku użycia, w tym przypadku zawiera również odniesienie odpowiedniego prezentera do przeprowadzenia domeny w celu wyświetlenia mapowania modelu przed wysłaniem go do wyświetlenia do renderowania.

Oto uproszczony przykładowy kod:

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

    }
}

Przypadek użycia zawierający prezentera

Chociaż nie jest to powszechne, ale możliwe jest, że przypadek użycia może wymagać połączenia z prezenterem. W takim przypadku zamiast przechowywania konkretnego odniesienia prezentera zaleca się rozważenie interfejsu (lub klasy abstrakcyjnej) jako punktu odniesienia (który powinien zostać zainicjowany w czasie wykonywania poprzez wstrzyknięcie zależności).

Posiadanie domeny do wyświetlania logiki odwzorowania modelu w oddzielnej klasie (zamiast wewnątrz kontrolera) przerywa również cykliczną zależność między kontrolerem a przypadkiem użycia (gdy klasa przypadków użycia wymaga odwołania do logiki odwzorowania).

wprowadź opis zdjęcia tutaj

Poniżej znajduje się uproszczone wdrożenie przepływu kontroli, jak pokazano w oryginalnym artykule, który pokazuje, jak można to zrobić. Należy pamiętać, że w przeciwieństwie do pokazanego na schemacie, dla uproszczenia UseCaseInteractor jest klasą konkretną.

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

    }
}
Ashraf
źródło
1

Mimo że ogólnie zgadzam się z odpowiedzią @CandiedOrange, dostrzegam także korzyść z podejścia, w którym interactor po prostu ponownie sprawdza dane, które następnie są przesyłane przez kontrolera do prezentera.

Jest to na przykład prosty sposób wykorzystania pomysłów czystej architektury (reguły zależności) w kontekście Asp.Net MVC.

Napisałem wpis na blogu, aby zagłębić się w tę dyskusję: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

prostytutka
źródło
1

Czy przypadek zawiera prezentera lub zwraca dane?

Czy więc któraś z tych dwóch alternatyw jest „poprawną” interpretacją portu wyjściowego przypadku użycia zgodnie z czystą architekturą? Czy oba są opłacalne?


W skrócie

Tak, oba są opłacalne, o ile oba podejścia uwzględniają odwrócenie kontroli między warstwą biznesową a mechanizmem dostarczania. Przy drugim podejściu nadal jesteśmy w stanie wprowadzić MKOl, korzystając z obserwatora, mediatora i kilku innych wzorców projektowych ...

Dzięki swojej Czystej architekturze , wujek Bob próbuje zsyntetyzować wiele znanych architektur w celu ujawnienia ważnych koncepcji i komponentów, które pozwolą nam na szeroką zgodność z zasadami OOP.

Nieproduktywne byłoby uznanie jego diagramu klasy UML (diagram poniżej) za unikalny projekt czystej architektury . Schemat ten mógłby zostać narysowany ze względu na konkretne przykłady … Ponieważ jednak jest on znacznie mniej abstrakcyjny niż zwykłe reprezentacje architektury, musiał dokonać konkretnych wyborów, spośród których projekt portu wyjściowego interaktora, który jest jedynie szczegółem implementacji

Schemat klasy UML wuja Boba z Clean Architecture


Moje dwa centy

Głównym powodem, dla którego wolę zwracać, UseCaseResponsejest to, że takie podejście zapewnia elastyczność moich przypadków użycia , umożliwiając zarówno kompozycję między nimi, jak i generyczność ( uogólnienie i generowanie specyficzne ). Podstawowy przykład:

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

Zauważ, że jest analogicznie bliższy przypadkom użycia UML, w tym / rozszerzaniu się, i jest zdefiniowany jako wielokrotnego użytku na różne tematy (podmioty).


Na interactor zwracających dane

Jednak przypadek użycia nie kontroluje już momentu, w którym rzeczywista prezentacja jest wykonywana (co może być przydatne, na przykład w celu wykonania dodatkowych czynności w tym momencie, takich jak rejestrowanie, lub całkowite przerwanie, jeśli to konieczne).

Nie wiesz, co rozumiesz przez to, dlaczego miałbyś „kontrolować” wykonanie prezentacji? Czy nie kontrolujesz tego, dopóki nie zwrócisz odpowiedzi na przypadek użycia?

Przypadek użycia może zwrócić w swojej odpowiedzi kod statusu, aby poinformować warstwę klienta, co wydarzyło się dokładnie podczas jego działania. Kody stanu odpowiedzi HTTP są szczególnie odpowiednie do opisania stanu działania przypadku użycia…

ClemC
źródło