Architektura MVP z Interactorem i Repository

0

Dotychczas w swoich aplikacjach na Androida stosowałem najprostszą wersję architektury MVP, czyli:
-View (Fragment/Activity)
-Presenter odpowiedzialny był za "zarządzanie", czyli reakcje na eventy, odwoływanie się do Modelu, w celu pobranie danych i do View w celu ich wyświetlenia.
-Model - pobieranie danych z rest api i przekazywanie ich do Presentera.

Pakiety były podzielone mniej więcej według takiego schematu:

1. feature
*model
       -FeatureModel
       -SomeItem - jakieś DTO
*view
       -FeatureFragment
*presenter
       -FeaturePresenter
*FeatureMVP - zagnieżdżone interfejsy odpowiedzialne za komunikację

FeatureMVP wygląda tak:

interface FeatureMVP
{
    interface View{}

    interface Presenter{ }

    interface Model {}
}

Poszczególne warstwy komunikowały się ze sobą przez interfejsy.

Znalazłem na GitHubie takie coś. Moim zdaniem jest tutaj lepszy i czytelniejszy podział, jednak wraz z rozbudową aplikacji DataManager staje się "bożą klasą" :). Poza tym samo Manager w nazwie to raczej nienajlepszy pomysł. Projekt ma jednak sporo gwiazdek na GitHubie.

Ten sam użytkownik ma też repo z architekturą, która używa klas Interactor i Repository (artykuł). To rozwiązuje powyższy problem.

Zastanawiam się jednak, czy jest jakiś lepszy rodzaj architektury MVP? Z jakiego Wy korzystacie? Polecacie wersję z Interactorem i Repository, a może naprostsze MVP pokazane na początku jest lepsze, a reszta to przerost formy nad treścią?

0

Nie ma najlepszego rozwiazania MVP , jezeli interactor i repo rozwiazuje twoj problem to w czym masz konkretnie problem ? Wszystko jest zalezne od danego przypadku .

0

Szczerze mówiąc, to każda architektura (również jej brak) rozwiąże mój "problem", ponieważ nie uniemożliwi działania kodu. Dopieszczam swój kod, który całkiem możliwe, że pokażę na rozmowie o pracę i zacząłem się zastanawiać, czy to co robię jest najbardziej optymalnym/aktualnym/czytelnym rozwiązaniem i czy nie można go jakoś poprawić. Znalazłem powyższy sposób i chciałem się dowiedzieć, czy jest on dobry.

0

Nie mowie tutaj o konkretnym problemie ale przy kazdej aplikacji architektura wyglada inaczej i nie mozna wskazac najlepszego rozwiazania jak nie widze twojego kodu.

2

Ta wersja MVP, która zaprezentowałeś na początku to przerost formy nad treścią z kilku powodów. Prezenter nie powinien być interfejsem. W MVP chodzi o to, żeby prezenter nic nie wiedział o implementacji widoku i gdybyś chciał nagle podmienić np. Activity na Fragment albo jakiś własny kontener na widok to mógłbyś to zrobić bez żadnego bólu. W drugą stronę tego problemu nie ma. Co do modelu, to może być interfejsem, ale na pewno nie w takiej postaci jak zaprezentowałeś. Nie powinieneś swoich interakcji biznesowych zamykać w postaci jednego obiektu Model per prezenter. Prezenter powinien dbać o to, żeby zdarzenia z widoku byłyby przekazane do odpowiednich elementów modelu i w momencie uzyskania wyniku podać go dalej do widoku.

Oczywiście jakieś klasy w modelu mogą, a nawet powinny, same mieć odwołania do innych klas modelu, ale nie z powodów koordynacyjno-widokowych jak prezenter. Przykładowo, jeżeli pobierasz coś z Internetu i chcesz to zapisać lokalnie w bazie danych, to możesz stworzyć klasę, która będzie za taką interakcję odpowiedzialana i będzie ona częścią Twojego modelu. Będzie potrzebowała w konstruktorze jakiegoś DAO i serwisu. Potem, jeżeli pobrane dane chcesz jakoś wyświetlić w widoku nic nie stoi na przeszkodzie, żeby prezenter miał dostęp i do klasy synchronizującej i do DAO. Bardziej obrazowo.

interface MovieService {
  Callback<List<Movie> movies();
}

interface MovieDao {
  void updateMovies(List<Movie> titles);
  Callback<Movie> topRatedMovie();
}

class MovieSynchronizer {
  private final MovieService movieService;
  private final MovieDao movieDao;

  MovieSynchronizer(MovieService service, MovieDao dao) {
    this.movieService = service;
    this.movieDao = dao;
  }
}

class Presenter {
  private final MovieDao movieDao;
  private final MovieSynchronizer movieSynchronizer;
  private volatile View view;

  Presenter(MovieDao dao, MovieSynchronizer synchronizer) {
    this.movieDao = dao;
    this.movieSynchronizer = synchronizer;
  }

  void attachView(View view);
  void detachView(View view);
}

Ponieważ wszystko w Androidzie jest asynchroniczne z natury, dodałem jakiś interfejs Callback, który zwraca dane asynchronicznie. Może być CallableFuture, Project Reactor (te dwa, jeśli wersja API pozwala, ale dla domowych projektów czemu nie), RxJava, korutyny z Kotlina czy co tam jeszcze się dzisiaj stosuje. Mogę też być zwyczajne listy nieopakowane w nic, ale wtedy sam musisz zadbać o asynchroniczność. Zwróć uwagę, że prezenter w tego typu implementacji musi mieć dwie metody do zarządzania widokiem, żeby być świadomym jego cyklu życia. Żeby się tego pozbyć, ludzie powymyślali jakieś MVVM, MVI, Flux, VIPER i wiele innych hipsterkich skrótów. W gruncie rzeczy to i tak dalej jest jakaś warstwa koordynacji i abstrakcji nad widokiem do komunikowania się z zewnętrznymi systemami. Ostatecznie sprowadza się to do tego, że prezeneter nasłuchuje na zdarzenia z widoku za pomocą jakiegoś strumienia, przekazuje te zdarzenia do modelu, model wykonuje odpowiednie akcje dla zdarzeń, i zwraca wynik w tym samym strumieniu, z którego wyszło zdarzenie z widoku. Tworzy to swojego rodzaju maszynę stanu zamkniętą w strumieniu.

W ten sposób, do widoku możemy zwrócić strumień, do którego on sam się zasubskrybuje i odskubskrybuje w odpowiednim momencie cyklu życia. Prezenter nie wie wtedy absolutnie niczego o widoku. Wie jedynie, że będą przychodzić jakieś zdarzenia, ale skąd i jak już go nie interesuje. Może być z widoku, może być z Broadcast Receivera, może być z okresowego alarmu.

Ja używam zależnie od potrzeby. Jeżeli jest to jakiś PoC, to wszystko pakuję w Activity i jazda, bo nie chcę tracić czasu. W przypadku już rzeczywistych projektów dodaję przynajmniej warstwę prezentera z prostym zarządzaniem wątkowością za pomocą Executora albo korutyn dla Kotlina. Jak projekt jest bardziej skomplikowany to wydzielam w modelu wiele małych klas przypadków użycia, które się ze sobą komunikują za pomocą RxJavy i komponują w bardziej skomplikowane przypadki użycia, która są już przekazywane do prezentera. Myślę, że w tym przypadku można powiedzieć, że za Interactor robią u mnie interfejsy z RxJavy.

Co do artykułu, to nie widzę za bardzo sensu tego interactora. Cała jego funkcjonalność może być równie dobrze przeniesiona do prezentera, skoro jest dokładnie jeden dla każdego prezentera. W tym przykładzie u mnie takim mini interactorem byłby MovieSynchronizer.

0

Nie idź w tym kierunku, MVVM sprawdza się znacznie lepiej, zwłaszcza w połączeniu z architecture components od Google. Poczytaj: https://medium.com/@ramtrg/https-medium-com-ramtrg-mvvm-architecture-components-4d17d3f09bb7

0

@Michał Sikora:
Nieźle się rozpisałeś, za co bardzo dziękuję. Co do bezsensowności używania interfejsu dla prezentera, to przyznaję rację. Szczerze mówiąc, nie wszystko zrozumiałem, więc mam kilka pytań.

  1. Mam wątpliwości co do attachView i detachView.
    Dobrze rozumiem, że służą one np. do ustawiania flagi określającej czy View jeszcze istnieje? Chodzi o to, żeby np. nie próbować wyświetlić pobranych danych w Fragmencie/Activity, które już nie istnieje? Jeśli tak, to po co przekazywać i przechowywać view, skoro można stworzyć flagę.

  2. Nie bardzo rozumiem przedstawioną przez Ciebie ideę rozbijania modelu.
    Powinienem rozdzielać model na wiele małych klas ze względu na jakąś szczegółową funkcjonalność, typ danych (np. Movie, Book, Game), czy źródło danych tak jak w repo na githubie (np. baza danych, SharedPreferences)? Pierwszego przypadku nie do końca rozumiem, a w drugim i trzecim nie mogę sobie wyobrazić po co mi jakoś szczególnie dużo klas.

  3. Czy podział modelu w repo jest dobry zakładając, że nie byłoby tam Interactora, tylko jego funkcję przejąłby Presenter?

@kulson
Dzięki za sugestię, ale jednak na ten moment zostanę przy MVP - po prostu lepiej znam tą architekturę i jest bardziej popularna w środowisku androidowym. Mimo wszystko jest to ciekawe rozwiązanie i przyjrzę mu się bliżej.

2
Hobbajt napisał(a):
  1. Mam wątpliwości co do attachView i detachView.
    Dobrze rozumiem, że służą one np. do ustawiania flagi określającej czy View jeszcze istnieje? Chodzi o to, żeby np. nie próbować wyświetlić pobranych danych w Fragmencie/Activity, które już nie istnieje? Jeśli tak, to po co przekazywać i przechowywać view, skoro można stworzyć flagę.

Tak. Ideą jest to, żeby nie wysyłać takich danych, jeżeli widok umarł. Nie robi się tego za pomocą flagi, żeby nie mieć wycieków pamięci. Gdyby Twój widok był private final View view i kontrolowałbyś dostęp do niego za pomocą boolean isViewAttached, to w momencie, gdy widok chciałby sobie spokojnie umrzeć, nie mógłby tego zrobić, ponieważ trzymałbyś do niego referencję w prezenterze. Można to rozwiązać za pomocą np. WeakReference, ale po co kombinować pod górę, skoro można równie dobrze widok nullować.

Zwróć też uwagę, że widok jest volatile, żeby inne wątki odpalone w prezenterze były świadome usunięcia widoku. To też można rozwiązać za pomocą jakiegoś mechanizmu, który wewnątrz prezentera wysyłałby wszystko na główny wątek przed wykonaniem czegoś na View. Tylko czemu prezenter ma mieć w ogóle jakąkolwiek świadomość tego, że istnieje główny wątek? Niech widok lepiej sam o siebie zadba pod tym względem i jeżeli kiedykolwiek, cokolwiek ma zrobić, zrobi to na głównym wątku. Pewnie niektórzy lubią tworzyć jakąś abstrakcję na to, która będzie wysyłała wiadomości na główny wątek z prezentera. Osobiście nie lubię, ale kwestia smaku bym powiedział. Z RxJavą np. to już w ogóle żaden problem, bo taka abstrakcja jest tam za darmo i wystarczy przekazać tylko Scheduler.

Hobbajt napisał(a):
  1. Nie bardzo rozumiem przedstawioną przez Ciebie ideę rozbijania modelu.
    Powinienem rozdzielać model na wiele małych klas ze względu na jakąś szczegółową funkcjonalność, typ danych (np. Movie, Book, Game), czy źródło danych tak jak w repo na githubie (np. baza danych, SharedPreferences)? Pierwszego przypadku nie do końca rozumiem, a w drugim i trzecim nie mogę sobie wyobrazić po co mi jakoś szczególnie dużo klas.

Jeżeli chodzi o reprezentację danych za pomocą klas, to oczywiście jakieś Movie, Book, Game, itd. Ale to nie jest de facto Model przez wielkie M. To tylko model danych (chyba że ktoś ma potrzebę robienia DDD), który jest używany w Modelu, żeby reprezentować odpowiednio stan aplikacji. Rzecz w tym, że nie ma czegoś takiego jak uniwersalny Model, bo w nim zamyka się całe mięso konkretnej aplikacji. W Androidzie może nie całe mięso, bo musimy się bawić z cyklem życia, ograniczonymi zasobami, zdarzeniami i pozwoleniami systemowymi, obsługą sensorów w przypadku np. kamery i pewnie innymi przypadkami, które mi teraz do głowy nie przychodzą. Niemniej, całe biznesowe mięso aplikacji powinno być zamknięte w Modelu i być niezależne od platformy. Dobrze jest też, kiedy nawet warstwa prezentacji jest niezależna od platformy — w końcu po to jest interfejs dla View.

Wracając do SharedPreferences i bazy danych, jako potencjalnych kontenerów do przetrzymywania, powinny one implementować wspólny interfejs, jeżeli mają przechowywać te same dane, i w trakcie działania programu decydowałbyś o tym, który ma być użyty. Przykładowo (użyję RxJavy jako callbacków do zapisu i odczytu). Chociaż na dobrą sprawę SharedPreferences nie powinno być wykorzystywane do tego rodzaju zapisu.

interface MovieDao {
  Completable updateMovies(List<Movie> movies);
  Observable<List<Movie>> favoriteMovies();
  Observable<List<Movie>> topRatedMovies(int count);
}

Potem miałbyś różne implementacje tego interfejsu w postaci SqlLiteMovieDao, SharedPreferencesMovieDao, InMemoryMovieDao itd. Zależy, czego potrzebujesz. To jest już cześć Modelu i może być tak rozumiane w postaci prostych aplikacji. Jednak gdy aplikacja jest już bardziej skomplikowana i potrzeba zaawansowanej interakcji między różnymi elementami Modelu, to wchodzą wtedy do gry różne interactory, use casy itp. Takie klasy Mogą mieć w nazwach "Interactor" czy "UseCase", ale nie muszą. Kwestia przyjętych standardów w projekcie. Może Ci się wydawać, że to dużo klas, ale to naprawdę nie jest problem, tylko dobra praktyka, która się zwraca przy kompozycji i modularności. Zdecydowanie lepiej jest mieć dużo klas odpowiedzialnych za jedną rzecz niż jedną klasę odpowiedzialną za wszystko.

Załóżmy, że dla tych trzech implementacji DAO chcesz stworzyć kolejne DAO, które je połączy (np. AggregatedMovieDao) i np. w przypadku gdy dopiero wystartowałeś aplikację, to dane będą przychodzić z SqlLiteMovieDao. Jeżeli ta jest pusta, to nastąpi synchronizacja z zewnętrznym serwisem w międzyczasie. A może synchronizacja będzie następować za każdym razem, kiedy poprosisz o dane. Kwestia tego, czego potrzebujesz. Jeżeli dane istnieją, to możemy używać InMemoryMovieDao jako dostawcy danych przez powiedzmy 30 minut, bo zakładamy, że przez tyle dane są w miarę świeże i niezdezaktualizowane. Takie interakcje powinny być też pozamykane we własnych klasach czy w tym wypadku operatorach/funkcjach RxJavy. Zasadniczo nie ma reguł i tu zamyka się clou aplikacji.

Hobbajt napisał(a):
  1. Czy podział modelu w repo jest dobry zakładając, że nie byłoby tam Interactora, tylko jego funkcję przejąłby Presenter?

Nie chce mi się przeglądać tam wszystkiego, ale kilka rzeczy na szybko mi się nie podoba.

  • Nazwa warstwy dostępu do lokalnych danych to Repository. Rozumiem, że w środowisku Androida taka nomenklatura się z jakichś dziwnych powodów przyjęła, ale drażni mnie za każdym razem, kiedy to widzę. To są zwykłe DAO, a nie żadne repozytoria.
  • Źle zdefiniowane metody w "repozytoriach". Każda metoda zwraca Observable. W tym wypadku należałoby użyć Single i Completable. Observable informuje, że dane mogą przychodzić przez dłuższy czas, a nie są jednorazowymi strzałami.
  • Wstrzykiwane klasy z nazwami "Helper", to jeden, wielki WTF. "Helper" sugeruje, że ma jakieś metody rozszerzające funkcjonalość innych klas, których nie możemy edytować, takich jak String. Najlepiej jakby te metody były statyczne, ale jak nie mogą, to po kiego wała je wstrzykiwać. Znaczy się, w tym wypadku wiadomo po co — bo to nie są żadne helpery, tylko źle nazwane klasy robiące coś konkretnego.
  • Taki podział na pakiet o ile wydaje się w porządku, to na dłuższą metę zabija orientację w projekcie. Powinien być np. pakiet user (albo jeszcze lepiej moduł, ale już nie wchodźmy w ten temat) i w nim opisane wszystkie zachowania z użytkowinikiem (ewentualnie w podpakietach, jeżeli coś się za bardzo rozrasta - user.ui, user.data, user.api itd.).
Hobbajt napisał(a):

@kulson
Dzięki za sugestię, ale jednak na ten moment zostanę przy MVP - po prostu lepiej znam tą architekturę i jest bardziej popularna w środowisku androidowym. Mimo wszystko jest to ciekawe rozwiązanie i przyjrzę mu się bliżej.

Wiesz, na dobrą sprawę, czy to MVP, MVVM, MVHipsterAbbreviationOfTheWeek, to nie ma znaczenia. To nie jest de facto architektura aplikacji. To tylko opis interakcji pomiędzy modelem a widokiem. Równie dobrze wszystko można nazywać prezenterem. O to mi chodziło w poprzednim poście, jak pisałem o strumieniach i subskrybcjach. Architektura to coś znacznie głębszego niż rzucenie skrótowca na klasę. Google niestety podąża za tym owczym pędem i z jakichś idiotycznych powodów zdecydowało się nazwać nowy zestaw bibliotek "Architecture components", kiedy z samą architekturą nie mają one dużo wspólnego. I nie mówię, że są to złe biblioteki, bo są one w większości naprawdę fajne. Zwłaszcza Room (chociaż wolę SqlDelight), LifecycleObserver/Owner i Paging. Trochę nie rozumiem, po kiego grzyba jest komponent ViewModel, ale jak jest, to niech będzie. W zasadzie to w pracy używam ViewModel od Googla, bo tak jest łatwiej w zespole, gdzie jest rotacja ludzi.

No i sam też nie daj się porwać owczemu pędowi. Jeżeli typowe, czyste MVP sprawdza się u Ciebie w aplikacji, to zostaw. O ile sam uważam, że tego typu komunikacja między komponentami jest w Androidzie uciążliwa, to jest perfekcyjnie w porządku używać klasycznego MVP. Jak będziesz chciał się douczyć np. RxJavy albo LiveData i dodać interakcje w postaci subskrybcji, to możesz ich używać dla nowych interakcji, zamiast przebudowywać całość projektu. Będzie to nawet o tyle lepsze, że odda bardziej realny przypadek, gdzie już masz gotowy produkt na produkcji i strona biznesowa (w sensie menadżment) nie da Ci czasu na przebudowę wszystkiego ad hoc. Zobaczysz wtedy jakie są wady i zalety wszystkiego, czy i jak ciężkie może być połączenie dwóch róznych rodzajów komunikacji, jak się pracuje z legacy. Wszystko to są przydatne umiejętności dla programisty. Nawet bardziej niż znajomość nowych, hipsterkich skrótów.

0

Podczas poprawiania kodu nasunęło mi się jeszcze kilka pytań:

  1. Czytałem trochę o MVP i wyczytałem, że dobrą praktyką ułatwiającą testowanie jest nieużywanie klas androidowych w Presenterze. W takim przypadku gdzie powinno odbywać się np. sprawdzanie uprawnień, wczytywanie danych z Resources itp. itd.? Na myśl przychodzi mi robienie tego w View, jednak wtedy znajdzie się tam logika. U góry pokazałeś również, że View jest przechowywane w Presenterze w celu kontroli stanu widoku, co łamie tą zasadę, ponieważ View jest klasą androidową.
    Może jest to zasada bardzo teoretyczna i w praktyce cieżko ją zachować, więc podchodzi się do niej z dystansem?

Niemniej, całe biznesowe mięso aplikacji powinno być zamknięte w Modelu i być niezależne od platformy.

Nie wiem czy dobrze to zrozumiałem. Jeśli chcę np. zapisać Bitmap do pamięci telefonu i robię to w warstwie Modelu, to Model przestaje być niezależny od platformy, ponieważ Bitmap jest klasą Androidową? Jeśli Model powinien być niezależny od platformy, a Presenter nie powinien używać klas androidowych, to gdzie mam to zrobić?

  1. Co jeśli używam RecyclerView? Adapter powinienem przechowywać w Presenterze (i traktować jako "drugi view") czy w View? Adapter powinien mieć referencje do Presentera?

  2. Przyjmijmy, że mam do wykonania jakieś obliczenia/operacje na danych wprowadzonych przez użytkownika (np. zdjęcie zrobione aparatem). Powinienem stworzyć jakąś klasę w Modelu, która by się tym zajęła, czy robić to w Presenterze?

2
Hobbajt napisał(a):
  1. Czytałem trochę o MVP i wyczytałem, że dobrą praktyką ułatwiającą testowanie jest nieużywanie klas androidowych w Presenterze. W takim przypadku gdzie powinno odbywać się np. sprawdzanie uprawnień, wczytywanie danych z Resources itp. itd.? Na myśl przychodzi mi robienie tego w View, jednak wtedy znajdzie się tam logika. U góry pokazałeś również, że View jest przechowywane w Presenterze w celu kontroli stanu widoku, co łamie tą zasadę, ponieważ View jest klasą androidową.
    Może jest to zasada bardzo teoretyczna i w praktyce cieżko ją zachować, więc podchodzi się do niej z dystansem?

Nie, to jest zasada, której zdecydowanie należy się trzymać. Pisząc View we wcześniejszych postach, nie miałem na myśli android.view.View. Miałem na myśli interfejs, który byłby implementowany przez jakąś klasę Androidową.

interface LoginView {
  void showProgressBar();
  void hideProgressBar();
}

Potrzeba pozwolenia jest stanem w View. Zaakceptowanie lub odrzucenie jest zdarzeniem, które może być wysłane do logiki (jeżeli trzeba zrobić jakąś logikę dla takiego zdarzenia), żeby potem odpowiedniu zareagować i zaktualizować stan w View. Wczytywanie z Resources powinno odbywać się tylko w klasach frameworkowych.

Hobbajt napisał(a):
  1. Nie wiem czy dobrze to zrozumiałem. Jeśli chcę np. zapisać Bitmap do pamięci telefonu i robię to w warstwie Modelu, to Model przestaje być niezależny od platformy, ponieważ Bitmap jest klasą Androidową? Jeśli Model powinien być niezależny od platformy, a Presenter nie powinien używać klas androidowych, to gdzie mam to zrobić?

Mógłbyś mieć na przykład metodą w swoim prezenterze processImage(ByteArrayInputStream image). Widok wywołałby tę metodę np. po nacisnięciu jakiegoś przycisku i przekazał obrazek zamieniony w strumień. Potem prezenter przekazałby ten strumień do przykładowego interfejsu.

interface ImageProcessor {
  boolean saveImage(ByteArrayInputStream image);
}

Jakaś klasa z Androidowymi zależnościami by implementowała ten interfejs. W momencie zakończenia zapisywania, zwrócony zostanie boolean do prezentera i ten powie widokowi jak ma się zachować. Czy wyświetlić komunikat błędu czy powiadomić o sukcesie.

Hobbajt napisał(a):
  1. Co jeśli używam RecyclerView? Adapter powinienem przechowywać w Presenterze (i traktować jako "drugi view") czy w View? Adapter powinien mieć referencje do Presentera?

RecyclerView wraz z adapterem powinny żyć w warstwie widoku. Adapter nie powinień mieć raczej referencji do prezentera. Widok ją ma i to wystarczy. Interakcja może wyglądać tak - widok prosi o dane presenter.topRatedActionMovies(20). Prezenter prosi model o te filmy i w momencie zwrócenia robi view.updateMovieList(movies) i widok przekazuje te filmy do adaptera, który współpracuje z RecyclerView.

Hobbajt napisał(a):
  1. Przyjmijmy, że mam do wykonania jakieś obliczenia/operacje na danych wprowadzonych przez użytkownika (np. zdjęcie zrobione aparatem). Powinienem stworzyć jakąś klasę w Modelu, która by się tym zajęła, czy robić to w Presenterze?

Trochę nie wiem, jakie obliczenia chciałbyś robić na zdjęciach. Jeżeli np. skalowanie to tak jak w punkcie pierwszym. Masz jakiś interfejs ImageProcessor, który robi co trzeba i zwraca wynik do widoku poprzez prezenter. Ale powiedziałbym, że też nie należy się z tym zapędzać. Jeżeli jest to jakaś prosta obróbka zdjęc to możesz mieć klasę, która się tym zajmie i niech ona sobie żyję w warstwie widoku. Zachowajmy też zdrowy rozsądek. Pewnie zależy jak bardzo anemiczny chcesz mieć widok. Im bardziej anemiczny tym więcej rzeczy powinno być pchane w interakcję z prezenterem i modelem.

1 użytkowników online, w tym zalogowanych: 0, gości: 1