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 Presenter
interfejsu 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?
Odpowiedzi:
Z pewnością nie jest to architektura czysta , cebulowa ani heksagonalna . To jest to :
Nie to, że MVC trzeba tak zrobić
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 :
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).
Dodaj rzutowane struktury danych (i z jakiegoś powodu odwróć je do góry nogami), a otrzymasz :
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.
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 .
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.
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.
Ważną rzeczą w tej „białej” strzałce jest to, że pozwala ci to zrobić:
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 .
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.
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.
Tak! Mówienie, a nie pytanie, pomoże utrzymać ten obiekt zorientowany, a nie proceduralny.
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.
źródło
W dyskusji dotyczącej twojego pytania wujek Bob wyjaśnia cel prezentera w swojej Czystej architekturze:
Biorąc pod uwagę ten przykładowy kod:
Wujek Bob powiedział:
(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) ...
... lub wykonujemy opcję # 2 (pozwól interaktorowi zwrócić odpowiedź, utwórz prezentera wewnątrz kontrolera, a następnie przekaż odpowiedź prezenterowi) ...
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:... Chcę mieć możliwość robienia tych,
if/else
które są związane z prezentacją winteractor
interaktorze, a nie poza nim.Jeśli z drugiej strony mamy opcję nr 2 zrobić, trzeba by zapisać się komunikat o błędzie (y) w
response
obiekcie, zwrot tenresponse
obiekt odinteractor
docontroller
, i sprawiają, żecontroller
zanalizować tenresponse
obiekt ...Nie lubię analizować
response
danych pod kątem błędów wewnątrz,controller
ponieważ jeśli to zrobimy, wykonujemy zbędną pracę --- jeśli zmienimy coś winteractor
, musimy również coś zmienić wcontroller
.Ponadto, jeśli później postanowimy ponownie wykorzystać nasze
interactor
dane do prezentacji za pomocą konsoli, musimy pamiętać, aby skopiować i wkleić wszystkie teif/else
wcontroller
naszej aplikacji na konsolę.Jeśli używamy opcji nr 1 będziemy mieli to
if/else
tylko w jednym miejscu : theinteractor
.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 Result
w prezencie naszej aplikacji ASP.NET MVC)(Zauważ, że musimy mieć
public IActionResult Result
w prezencie naszej aplikacji ASP.NET MVC)Jeśli zdecydujemy się stworzyć kolejną aplikację dla konsoli, możemy ponownie wykorzystać
UseCase
powyżej tworzyć tylkoController
iPresenter
do konsoli:(Zauważ, że NIE MAMY
public IActionResult Result
w prezencie naszej aplikacji na konsolę)źródło
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:
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:
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).
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ą.
źródło
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/
źródło
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 …
Moje dwa centy
Głównym powodem, dla którego wolę zwracać,
UseCaseResponse
jest 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: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).
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…
źródło