Wprowadzenie do wzorca MVP z Vaadin w tle – cz. 1 teoria

Wzorzec projektowy Model-View-Presenter (MVP) nie jest szeroko znany w społeczności Javowej. Inaczej… nie był szerzej znany do czasu aż Google nie postanowił go promować jako jednego z elementów GWT.
Nie jest to nic nadzwyczajnego ponieważ duża część biznesowych aplikacji pisanych w Javie posiada interfejs webowy. Ten rodzaj GUI znacznie lepiej jest obsługiwany za pomocą wzorca MVC (vide Spring Web MVC). Trochę inaczej ma się sprawa w przypadku technologii .NET. Tam M$ zadbał by tworzenie UI było niezależne od sposobu jego wyświetlania (web lub okienka). Domyślnie wspierane są okienka zatem wzorzec MVP jest lepszy.
Czas na teorię.

MVC i MVP – znajdź różnice

Jeżeli przyjrzymy się tym dwóm wzorcom to na pierwszy rzut oka poza nazwą nie dostrzeżemy różnic. Stanie się tak ponieważ są one znacznie bardziej subtelne i wynikają z natury nośnika UI dla którego dedykowane są poszczególne wzorce. W uproszczeniu MVP to pewne rozszerzenie MVC zarówno koncepcyjne jak i funkcjonalne.
Jeżeli przyjrzymy się typowej implementacji MVC to łatwo dostrzeżemy, że muszą istnieć dwa dodatkowe elementy, bez których ten wzorzec będzie kulawy. Pierwszym z nich jest obiekt-dyspozytor (dispatcher). Jest to zazwyczaj wyspecjalizowany serwlet, którego zadaniem jest analiza żądania i na podstawie przekazanych parametrów uruchomienie odpowiedniego kontrolera. Mapowania pomiędzy nazwami widoków, a kontrolerami są zazwyczaj konfigurowane w osobnym pliku lub przy pomocy adnotacji. Rolą dyspozytora jest też pobranie wyniku prac kontrolera i przekazanie ich do kolejnego elementu jakim jest obiekt-producent widoków (w Springu view resolver). Obiekt ten na podstawie danych z kontrolera określa, który szablon widoku powinien być załadowany i wysłany do klienta.
Te dwa obiekty są zazwyczaj pomijane przy opisie MVC, ale są kluczowe dla prawidłowego działania aplikacji.

Wyobraźmy sobie teraz sytuację, w której chcemy zastosować wzorzec MVC w aplikacji okienkowej. Musimy dostarczyć zarówno obiekt dyspozytora, który na podstawie identyfikatorów (jak nadawanych?) będzie wstanie uruchamiać odpowiednie kontrolery, a następnie będzie przekazywał wyniki ich pracy do jakiegoś producenta widoków, który będzie mieszał w okienkach. Skąd my to znamy… zapewne tak wyglądały wasze pierwsze aplety:

Listing 1. „Pierwszy aplet” wskaż dispatcher i viewresolver

public class MyFirstApplet extends JApplet implements ActionListener {

	private JButton b1, b2;
	private JLabel tekst;

	@Override
	public void init() {
		super.init();
		b1 = new JButton("naciśnij mnie");
		b1.addActionListener(this);
		b2 = new JButton("i mnie");
		b2.addActionListener(this);
		tekst = new JLabel();
		JPanel jPanel = new  JPanel();
		jPanel.add(b1);
		jPanel.add(b2);
		jPanel.add(tekst);
		getContentPane().add(jPanel);
	}

	@Override
	public void start() {

		super.start();
	}

	@Override
	public void stop() {

		super.stop();
	}

	@Override
	public void destroy() {
		super.destroy();
	}

	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();
		if (source == b1) {
			tekst.setText("Nacisnąłeś 1");
		}
		if (source == b2) {
			tekst.setText("Nacisnąłeś 2");
		}
	}

}

Jak widać actionPerformed pełni rolę dyspozytora i jednocześnie generatora (lepsze słowo niż producent) widoków. Kod ten na pewno da się zrefaktoryzować tak by był bardziej „flexy-sexy”, ale nadal pozostanie nam pewien problem. Zarówno obiekt dyspozytora jak i generatora będą miały dostęp do wszystkich komponentów w całej aplikacji. Bez bardziej zaawansowanych narzędzi jak kontenerów DI będzie trudno zrobić zgrabną wersję tej aplikacji.
Wynika to z różnego charakteru UI opartego o web i tego opartego o okna. WWW dostarcza nam bardzo fajnego rozwiązania jakim jest URL. Dzięki niemu dostajemy bardzo fajny, deklaratywny i łatwy w konfiguracji sposób wiązania widoku i kontrolera. W praktyce, uwaga bluźnić będę, można powiedzieć, że adresy URL są SQLem interfejsu użytkownika. Mówimy CO chcemy, a sposób w jaki to uzyskamy to już nie nasz problem.
W przypadku aplikacji okienkowych ciężko jest zrobić coś w tym stylu. Można oczywiście wykorzystać jakieś sztuczne identyfikatory nadawane poszczególnym obiektom i na ich podstawie zarówno pobierać informacje o zdarzeniach jak i generować widoki…

Przebudujmy ostatnie zdanie.

Niech obiekty UI będą pogrupowane w „Domeny” na poszczególnych poziomach abstrakcji. Niech każdy obiekt domenowy definiuje interfejs, który będzie określał pewne zadania – funkcje biznesowe na poziomie UI (pokaż, schowaj, ustaw, pobierz), ale nie będzie ujawniał z jakim konkretnie elementem UI mamy do czynienia. Niech w domenie będzie obecny jeden lub więcej obiektów prezenterów, które będą na podstawie przekazanych im dostawców usług będą wykonywały operacje biznesowe na danych pobranych za pomocą interfejsów domenowych UI. Prezenter będzie swoje wyniki publikował za pomocą interfejsów domenowych UI traktując je jako pewne modele. Ergo prezenter transformuje pobrane za pomocą usług obiekty Modelu na pewne abstrakcyjne obiekty domeny IU.
Mamy zatem Model – Widok – Prezentera.

Różnica leży w tym gdzie odbywa się zarządzanie wywołaniami kontrolera. W MVC kontroler MUSI zostać wywołany w celu zmiany w widoku nawet jeżeli nie wymaga ona zmian w Modelu. W MVP widok odwoła się do pomocy Prezentera tylko wtedy gdy zmiana widoku będzie wymagała zmian w Modelu.

Tyle teorii. Poniżej program w Vaadin, który zostanie zrefaktoryzowany do MVP. Specjalnie poza wyniesione jest tylko DAO.

Listing 2. Aplikacja w Vaadin przed zmianami.

public class MyVaadinApplication extends Application {

	private Window window;

	private CustomerDao customerDao = new CustomerDaoCollectionImpl();

	private Table customersTbl;

	private Button addCustomerBtn;

	private Form customerAddFrm;

	private TextField ageFld;

	private TextField lastNameFld;

	private TextField firstNameFld;

	private TextField uuidFld;

	@Override
	public void init() {
		window = new Window("My Vaadin Application");
		setMainWindow(window);
		initCustomersTbl();
		initAddCustomerBtn();
		window.addComponent(customersTbl);
		window.addComponent(addCustomerBtn);
	}

	private void initCustomersTbl() {
		customersTbl = new Table("Klienci");
		customersTbl.addContainerProperty("Id", Long.class, null);
		customersTbl.addContainerProperty("Imię", String.class, null);
		customersTbl.addContainerProperty("Nazwisko", String.class, null);
		customersTbl.addContainerProperty("Wiek", Integer.class, null);
		Collection all = customerDao.getAll();
		for (Customer c : all) {
			customersTbl.addItem(new Object[] { c.getUUID(), c.getFirstName(),
					c.getLastName(), c.getAge() }, c.getUUID());
		}

		customersTbl.addListener(new ItemClickListener() {

			public void itemClick(ItemClickEvent event) {
				Customer byId = customerDao.getById(event.getItemId());
				showAddCustomerForm();
				firstNameFld.setValue(byId.getFirstName());
				lastNameFld.setValue(byId.getLastName());
				ageFld.setValue(byId.getAge());
				uuidFld.setValue(byId.getUUID());
			}
		});
	}

	private void initAddCustomerBtn() {
		addCustomerBtn = new Button("Click Me");
		addCustomerBtn.addListener(new Button.ClickListener() {
			public void buttonClick(ClickEvent event) {
				showAddCustomerForm();
			}
		});
	}

	protected void showAddCustomerForm() {
		customerAddFrm = new Form();
		customerAddFrm.setCaption("Dodaj klienta");

		uuidFld = new TextField("Id");
		uuidFld.setEnabled(false);
		customerAddFrm.addField("uuid", uuidFld);

		firstNameFld = new TextField("Imię");
		customerAddFrm.addField("firstName", firstNameFld);

		lastNameFld = new TextField("Nazwisko");
		customerAddFrm.addField("lastName", lastNameFld);

		ageFld = new TextField("Wiek");
		customerAddFrm.addField("age", ageFld);

		HorizontalLayout buttonBar = new HorizontalLayout();
		customerAddFrm.setFooter(buttonBar);
		Button okBtn = new Button("Dodaj");
		Button resetBtn = new Button("Wyczyść");
		Button cancelBtn = new Button("Anuluj");

		buttonBar.addComponent(okBtn);
		buttonBar.addComponent(resetBtn);
		buttonBar.addComponent(cancelBtn);

		okBtn.addListener(new ClickListener() {

			public void buttonClick(ClickEvent event) {
				String firstName = customerAddFrm.getField("firstName")
						.getValue().toString();
				String lastName = customerAddFrm.getField("lastName")
						.getValue().toString();
				Integer age = Integer.parseInt(customerAddFrm.getField("age")
						.getValue().toString());
				Object uuidFldValue = customerAddFrm.getField("uuid")
						.getValue();
				String uuid = null;
				if (uuidFldValue != null)
					uuid = uuidFldValue.toString();

				Customer c = new Customer();
				c.setAge(age);
				c.setFirstName(firstName);
				c.setLastName(lastName);
				if (uuid != null && uuid.length() > 0)
					c.setUUID(Long.parseLong(uuid));
				customerDao.saveOrUpdate(c);
				if (uuid != null && uuid.length() > 0) {
					Item updatedItem = customersTbl.getItem(c.getUUID());
					updatedItem.getItemProperty("Imię").setValue(
							c.getFirstName());
					updatedItem.getItemProperty("Nazwisko").setValue(
							c.getLastName());
					updatedItem.getItemProperty("Wiek").setValue(c.getAge());

				} else {
					customersTbl.addItem(
							new Object[] { c.getUUID(), c.getFirstName(),
									c.getLastName(), c.getAge() }, c.getUUID());
				}
				window.removeComponent(customerAddFrm);
			}
		});

		resetBtn.addListener(new ClickListener() {

			public void buttonClick(ClickEvent event) {
				customerAddFrm.getField("firstName").setValue("");
				customerAddFrm.getField("lastName").setValue("");
				customerAddFrm.getField("age").setValue("");
			}
		});
		cancelBtn.addListener(new ClickListener() {

			public void buttonClick(ClickEvent event) {
				window.removeComponent(customerAddFrm);
			}
		});

		window.addComponent(customerAddFrm);
	}

}

Zajmiemy się też Apletem z listingu 1.

4 myśli na temat “Wprowadzenie do wzorca MVP z Vaadin w tle – cz. 1 teoria

  1. Trochę dyskusyjnych skrótów poczyniłeś z tym MVC. Dyspozytor w Spring WebMVC to Front Controller, który w aplikacjach webowych spotyka się bardzo często, ale z samym MVC nic wspólnego nie ma. Ot, tłumaczy URL-e na akcje i deleguje do odpowiedniego kontrolera. To raczej rozsądne obejście kłopotliwego HTTP. View Resolver to też zupełnie inny (i opcjonalny) byt. W końcu można się umówić, że każda akcja zwraca nazwę pliku JSP i to też byłaby rozsądna implementacja.

    Wydaje mi się, że w aplikacjach okienkowych dyspozytora i view resolvera raczej się nie spotyka. Nie ma tłumaczenia URL-i na komponenty – jest raczej sieć zwyczajnych, trwałych i stanowych komponentów. Raczej rzadko widzi się globalnego dyspozytora – w końcu każdy komponent ma własne event listenery. A jak nawet potrzeba czegoś bardziej dynamicznego, to prędzej trafi się zwykła, ograniczona do danego modułu wyspecjalizowana fabryka i nieco bogatsze, dedykowane interfejsy.

  2. @Konrad, co do dyspozytora w aplikacjach okienkowych to czasami się zdarza. Chociażby w Eclipse RPC czy NBPlatform obsługa zdarzeń jest przepychana przez „globalny listener”. Co do zwracania adresu jsp to też jest „śliskie”, ponieważ wymaga od kontrolera wiedzy o docelowym widoku. Czyli przerzucasz do konfiguracji. Sam wzorzec Front Controller stał się nierozerwalnym elementem MVC tak jak wzorzec Factory i Singleton stały się elementem DI. Ciężko mówić o MVC bez takiego ustrojstwa jak FC.

  3. Zgoda, w sieciowych aplikacjach FC to podstawa, ale nie ma nic wspólnego z MVC. Można mieć FC bez MVC albo MVC bez FC. OK, moje doświadczenie z desktopem to tylko czysty Swing, gdzie przez kilka lat z globalnym dyspozytorem się nie spotkałem. Pewnie się da, ale w żaden sposób nie nazwałbym FC integralną częścią MVC.

    Te dwa środowiska bardzo się różnią. Web to bezstanowe żądanie-odpowiedź, a MVC używa się tylko do rozdzielenia danych od prezentacji. Klasyczne MVC jest bardziej stanowe i ociera się o obserwatora.

  4. @Konrad, takie ortodoksyjne MVC to już dawno nie istnieje. Zresztą bezstanowy WEB też powoli odchodzi w zapomnienie ponieważ jeżeli chcesz dostarczać coś poza stronami WWW (np. SaaS) to trzeba kłaść nacisk na sesję i jej obsługę. W weekend pewno będę siedział nad książką i to właśnie w tym temacie.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax