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?