MVP Przekazywanie parametrów do komponentów widoku

1

Na pewno stęskniliście się za ciekawymi rozkminami na temat inżynierii oprogramowania więc wracamy dziś w kolejnej odsłonie. Problem, który chciałbym poruszyć jest związany z przekazywaniem danych do kontrolek w widoku we wzorcu MVP.
W najbardziej klasycznym ujęciu na prezenterze wywołana zostaje jakaś akcja. Prezenter uruchamia odpowiednie serwisy a następnie zebrane dane przesyła do warstwy widoku w celu ich prezentacji. Problem pojawia się właśnie w tym ostatnim punkcie - jak zrobić to przekazywanie z głową?

Załóżmy, że mam widok który składa sie z kilku komponentów, każdy z nich z jeszcze kilku i tak przez pare poziomów. Np. mamy okienko w którym jest kilka paneli, jeden z tych paneli zawiera jeszcze kilka innych paneli i na koniec ma pole tekstowe do którego właśnie ładuje sobie dane. W efekcie z punktu widzenia kodu mam taką śmieszną delegację metod bo prezenter wysyła dane do widoku, widok wysyła do odpowiedniego okienka, okienko do panelu, panel do swojego podpanelu a tamten dopiero do pola tekstowego. Kiedy na tym najniższym poziomie komponentów jest dużo to nagle te klasy wyżej mają po kilkanaście (lub więcej) delegowanych wywołań. Coś w stylu:

class View{
     private final MainWindow window;
//
     public void loadText(String text){
         window.loadText(text);
     }
}

class MainWindow{
     private final MainPanel mainPanel;
//
     public void loadText(String text){
         mainPanel.loadText(text);
     }    
}

class MainPanel{
     private final TextPanel textPanel;
//
     public void loadText(String text){
         textPanel.loadText(text);
     }    
}

class TextPanel{
     private final TextField textField;
//
     public void loadText(String text){
         textField.loadText(text);
     }    
}

Jednym pomysłem byłoby spłaszczenie struktury, tzn władowanie tych komponentów na poziomie jednej klasy, ale efekt raczej odwrotny od zamierzonego bo mamy wtedy klasę mother-of-all-views ;) Niemniej nie bardzo jest to możliwe bo te komponenty są wielokrotnie re-używane więc ich separacja ma generalnie sens.

Od razu mówie że wiem że są technologie które ten problem rozwiązują poprzez wiązanie danych z komponentami za pomocą identyfikatorów (np. technologie webowe zwykle tak robią, robi tak też chyba WPF). Swing takich cudów nie posiada więc jeśli chciałbym coś takiego mieć to musiałbym to sam napisać i wydaje mi się to niekoniecznie eleganckie, chyba że ktoś ma pomysł jak można by coś takiego zaimplementować (w sposób sensowny więc nie poprzez pchanie jakiegoś Map<String, Object> a potem rzutowanie tego objecta...)

Podkreślam tu że chodzi o MVP ponieważ w MVC zwykle istnieje jakieś powiązanie modelu z widokiem i aktualizacja następuje "automatycznie" za pomocą jakiegoś Observera. Niemniej w tym przypadku trudno mówić o jakimś "modelu" w sensie danych przechowujących stan aplikacji, jak w jakiejś grze komputerowej. Niemniej teoretycznie mógłbym tak zrobić, że komponenty rejestrują sie gdzieś jako obserwatorzy, tylko gdzie? W prezenterze?

@somekind @Koziołek @Krolik @niezdecydowany @karolinaa @msm @winerfresh @stryku @Patryk27 @katelx @WhiteLightning @datdata @spartanPAGE @Wizzie @Azarien @Satirev @MarekR22 @Afish @panryz @Wibowit @azalut @mychal @_Mateusz_ @krzysiek050
(wołam tych co ostatnio plus tych co sie wypowiadali więc jak kogoś nie ma to sam sobie winien bo mi ostatnio nic nie napisał :P)

1

Dobra skoro zaś ja to się wypowiem.
Ponieważ w Androidzie (i tutaj zaznaczam ostro, bo wiem że to nie to samo co czysta java) jest tak że często robi się sporo aktywności, fragmentów, które właśnie prezentują dane. Robię to w ten sposób że tworzę sobie jedną klasę (Singleton) gdzie trzymam obiekty, interfejsy. Do tych interfejsów przekazuję instancję każdego Activity

setSomethingListener(this) <- Tak, zapewne swing tego nie ma o czym @Shalom wspomniał. Ale idąc dalej wspominałeś o obserwatorze. Więc wydaje mi się że ta droga może być dobra i tak samo bym zrobił takiego singletona który by mi to trzymał. A potem każdy widok zarejstrował się na wywołanie metody z tego listenera.
W konsekwencji każdy widok który chciałby sobie zaktualizować kontrolkę jest zarejestorwany na to i dzieje się to "automatycznie".

Dispatch design pattern - tak to się nazywa chyba profesjonalnie

6

W teorii
Można by, ale to nie jest zbyt efektywne, zaimplementować coś w rodzaju "szukajki". Załóżmy, że prezenter zwraca do widoku tą nieszczęsną Map<String, Object> (która technicznie rzecz biorąc może być "pod spodem" JSON-em). Teraz widok powiadamia wszystkie zapisane w nim obserwatory, że ma nowe dane i mogą sobie je przeszukać. Te robią przegląd szukając swoich identyfikatorów.

W praktyce

  1. Naiwnie implementację oparłbym na Guava Event Bus. Każdy "końcowy" komponent niech implementuje sobie jakąś metodę oznaczoną jako @Subscribe i przy tworzeniu niech się rejestruje w EvenBus (globalna, ale zakładam, że to nie problem na teraz). Widok po odebraniu mapy rozsyła ją do elementów.

  2. Trochę inna, bardziej popieprzona, implementacja, która będzie trochę wydajniejsza. robimy jak wyżej, ale zamiast mapy w której kluczem jest jakieś tam id komponentu, a wartość musimy rzutować przekazujemy listę czegoś co dziedziczy po Event<T>:

public class Event<>{

    final String componentId;
    final T value;

   //...
}

Widok po odebraniu komunikatu z prezentera rozgłasza listę eventów. Zalety:

  • Guava dba o to by komunikaty trafiły do odpowiednich odbiorców zapisanych na konkretny typ. Zatem jeżeli masz pole tekstowe, które oczekuje np. Integer, to dostanie IntegerEvent.
  • Masz możliwość częściowej aktualizacji, bo istnieje mechanizm związany z componentId i jeżeli dany subskrybent nie dopasuje go do swojego identyfikatora to nie zmieni się.

Wady:

  • EventBus jako globalny element widoku. Trzeba by dopisać tak naprawdę kawałek kodu by odciąć komponenty bezpośrednio od szyny.
  • Może się zdarzyć tak, że będzie dużo eventów jednego typu i zrobisz DDOS własnych komponentów.

To tak na szybko.

0

Skoro już mnie zawołano.. nie jestem zwolennikiem sztywnego przestrzegania wzorców, więc powiem że powyższy kod ma o jedną warstwę za dużo.

View jest niepotrzebne, albo może być ale jako klasa abstrakcyjna czy interfejs, implementowany przez MainWindow.

Ale jak mamy się trzymać książkowych wzorców, to nie narzekajmy że są nieżyciowe..

0

Dane zbieram w jakieś struktury/modele(zazwyczaj wypracowany status to paczka danych). Dzięki temu "klasy wyżej" mają kilka metod i te modele lub ich części delegowane są dalej.
Dodatkowo korzystam z Visitora, czyli wołam na modelach metodę, gdzie argumentem jest gui ukryte za interfejsem z referencją na siebie, dzięki czemu unikam castowania, ifów, instance of...

0

@garai_nz hmm no ale jak mam np. 20 różnych DTO bo tyle funkcji prezentera to chcesz wysłać taki mother-of-all-DTO z tymi 20 polami gdzie tylko jeden jest "wypełniony" i potem każdy sobie wybiera co mu potrzebne? To raczej słabe ;) A jeśli mówisz o samym fakcie pakowania danych do DTO to jasne że tak robię ale to nijak nie zmniejsza liczby delegowanych metod.

1

Może coś w stylu okrojonego Fluxa (pattern do kontroli danych z JS'owego Reacta)? Musisz poczytać o tym poza moim postem, żeby załapać zasadę działania.

public interface <T> Store { // nie wiem czy to dobrze zdefiniowałem, mało ostatnio w Javie pisałem
    void update(T data);  // aktualizuje stan obiektu, woła callbacki zarejestrowane z subscribe
    void subscribe(/* callback */); // callback wołany za każdym razem jak ktoś rzuci update
    // jeszcze ewentualnie getInitialState dla pobrania domyślnego stanu, ale to już jak tam sobie wymyślisz :P
}

Singletony np. TextStore<String> rozszerzające ten Store wyżej (zaczerpnięte fluxowe nazewnictwo) z danymi, masz tą instancję TextStore tylko w klasie View i TextPanel. W View wołasz metodę update na storze, w TextPanel wrzucasz tylko callbacka do subscribe i śmiga.
Dla klasy renderującej np. ustawienia programu zrobisz SettingsStore<Setting> przykładowo itd.

We Fluxie to wygląda inaczej, bo nie ma żadnego update, tylko są akcje i store nasłuchują akcji (cały ten ambaras żeby zachować unidirectional flow, ale u ciebie nie widzę potrzeby jakiejś, poza tym Java to inny świat niż JS)

Teraz dlaczego tak, a nie jeden singleton ze stanem? Bo a) SRP, b) klasy są małe co wynika z SRP, c) nie przychodzi mi do głowy nic lepszego :D

Co o tym myślicie?

2
Shalom napisał(a):

Jednym pomysłem byłoby spłaszczenie struktury, tzn władowanie tych komponentów na poziomie jednej klasy, ale efekt raczej odwrotny od zamierzonego bo mamy wtedy klasę mother-of-all-views ;) Niemniej nie bardzo jest to możliwe bo te komponenty są wielokrotnie re-używane więc ich separacja ma generalnie sens.

Nie byłem wołany ale zapytam.
A prezenter w tym co podałeś nie jest w tym momencie mother-of-all-presenters? Bardzo mocno kombinując można by każdemu re-używalnemu komponentowi dołożyć jego własny prezenter, te mniejsze prezentery byłyby zbierane w kolekcji w głównym prezenterze który odpowiadałby za komunikację z modelem i te pomniejsze implementowałyby interfejs który "pytałby" komponentu czy przyjmuje dane informacje (stała,enum ?).
Ale "pogdybałem", to zalety ma jakieś plusy? Ile minusów? ;)

0

@Alag nie da rady bo co prawda w warstwie widoku te komponenty wyglądają tak samo, ale robią coś zupełnie innego ;) Wyobraź sobie że komponentem jest panel z polem tekstowym, buttonem "browse" do przeglądania plików na dysku i guzikiem "load". Jak widać komponent jest zupełnie generyczny i można go wcisnąć w 100 miejsc, ale akcja "load" jest zupełnie inna za każdym razem ;)
A prezentery są raczej bardzo małe, mają raptem po kilka metod ;)

@Wizzie @Koziołek no ten pomysł (bo mówicie o podobnych rzeczach) z event busem / observerem nie jest w sumie taki zły ;)

1

Tak na szybko, bez dogłębnej analizy problemu:
Zrób coś w stylu command i potraktuj jako tunelujące się zdarzenie. Miałbyś wtedy

interface IUpdate{}
class UpdateLabel : IUpdate{
  public String Text {get; private set;
} 

Następnie widok implementuje:

class View{
  public bool Update(IUpdate details){
    if(!mainWindow.update(details)){
      // Element podrzędny nie obsłużył, więc ja spróbuję
    }
  }
}
 

A panel miałby coś takiego:

class TextPanel {
  public bool Update(IUpdate details){
    if(details instanceof UpdateLabel){
       UpdateMyLabel(details);
       return true;
    }
    return false;
  }
} 

Oczywiście to tylko zarys. Logikę aktualizacji możesz też wrzucić do jakiejś bazowej klasy dziedziczącej po IUpdate i potem robić double dispatch przy pomocy wizytora.

1

Osobiście bardzo podoba mi się system eventów w angularze. I tak tutaj moim zdaniem najlepsza byłaby propagacja w dół od głównego rodzica.

Załóżmy że B to jest główny komponent, który posiada dzieci, które posiadają dzieci. Używając wspomnianego Guavowego event bus, można zrobić coś takiego.

import java.lang.reflect.Field;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

import org.apache.commons.lang3.ClassUtils;

public class Test {

	public static void main(String[] args) {
		final B b = new B();
		b.post(new TextFieldChanged("Zmiana 1"));
		b.post(new TextFieldChanged("Zmiana 2"));
	}

}

interface EventComponent {
	void registerChildrens(EventBus e);
}

abstract class AI implements EventComponent {
	@Override
	public void registerChildrens(EventBus e) {
		for (Field f : getClass().getDeclaredFields()) {
			for (Class<?> i : ClassUtils.getAllInterfaces(f.getType())) {
				if (i == EventComponent.class) {
					try {
						final EventComponent eventComponent = (EventComponent) f.get(this);
						if (eventComponent != null) {
							e.register(eventComponent);
							eventComponent.registerChildrens(e);
						}
					} catch (Exception e1) {
						e1.printStackTrace();
					}
				}
				;
			}
		}
	}
}

class B extends AI {
	C c;
	C c2 = new C();
	String s;
	EventBus bus = new EventBus();

	B() {
		registerChildrens(bus);
	}

	public void post(Object event) {
		bus.post(event);
	}
}

class C extends AI {
	D d1 = new D();
	D d2 = new D();

	@Subscribe
	public void listen(TextFieldChanged event) {
		System.out.println("From C: " + event.getMessage());
	}

}

class D extends AI {
	@Subscribe
	public void listen(TextFieldChanged event) {
		System.out.println("From D: " + event.getMessage());
	}

}

class TextFieldChanged {

	private final String message;

	public TextFieldChanged(String message) {
		this.message = message;
	}

	public String getMessage() {
		return message;
	}
}

Output:

From C: Zmiana 1
From D: Zmiana 1
From D: Zmiana 1
From C: Zmiana 2
From D: Zmiana 2
From D: Zmiana 2
 

Zakładając że nie będzie eventów typu String, tylko złożone to self DDOS w cale nam nie grozi. Kod oczywiście klepany na szybko żeby przedstawić koncepcję, także ten ;P

0

@Shalom jak to rozwiązałeś?

0

Tak jak sugerowała większość ;) Wykorzystałem sugerowane przez @Koziołek guava Event Bus i wygląda że działa bardzo fajnie. Niemniej nie było tu potrzeby pchać żadnej mapy z objectami. Mam sobie Prezenterów które mają odpowiednie metody. Po wywołaniu metody, prezenter odbiera z odpowiedniego serwisu DTO z danymi i posyła je do Widoku. Każdy Widok ma swój Event Bus i za jego pomocą wysyła event odpowiedniego typu.
Komponenty "końcowe" rejestrują się w Widoku na eventy i maja metody @Subscribe.
Event Bus per Widok jest o tyle konieczne, że komponenty w warstwie widoku mogą być używane w wielu widokach (np. taki sam komponent może być użyty w dwóch różnych okienkach i robić zupełnie co innego) i głupio byłoby jakby się eventy mieszały w takiej sytuacji. Teraz każdy komponent reaguje tylko na zdarzenia ze swojego Widoku. Poza tym było to dość proste w implementacji bo i tak każdy komponent miał dostęp do Widoku (konieczne, bo jeden Prezenter może sterować wieloma Widokami tego samego typu, np. mozesz odpalić kilka identycznych okienek, więc Prezenter wykonując akcje musi wiedzieć który Widok ma dostać dane :) )

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