Wprowadzenie do wzorca MVP z Vaadin w tle – cz. 2 praktyka

W tej części zastosujemy MVP w praktyce. Na początek dla uproszczenia przebudujemy aplet z poprzedniej części, ale nie do MVP, a do czegoś co można nazwać dobrym kodem.

„Małe MVP”

Nie będę tu pokazywał MVP, ale będzie to coś co też oddaje sens tego wzorca. Standardowe komponenty Swing rozszerzę o implementację prostych interfejsów służących do komunikacji. Dzięki temu będę mógł używać metod szablonowych do obsługi zdarzeń.

Listing 1. Interfejsy i implementacja rozszerzonych komponentów

interface EventController {

	public void performAction(ActionEvent actionEvent);

}

@SuppressWarnings("serial")
class MyButton extends JButton implements EventController {

	private TextProjector textProjector;
	private String text;

	public MyButton(String caption, TextProjector textProjector, String text) {
		super(caption);
		this.textProjector = textProjector;
		this.text = text;
	}

	public void performAction(ActionEvent actionEvent) {
		textProjector.project(text);
	}

}

interface TextProjector {

	public void project(String text);

}

@SuppressWarnings("serial")
class MyLabel extends JLabel implements TextProjector{

	public void project(String text) {
		setText(text);
	}	
}

Nie jest to może najlepszy kod pod słońcem, ale wystarcza do naszych potrzeb. Warto zwrócić uwagę, że ten kod już nadaje się do wiązania za pomocą np. Guice. Przy czym klasa MyButton jest zarówno elementem widoku jak i kontrolerem/prezenterem. Na razie nas to nie boli, bo nie dążymy do przerostu formy nad treścią. Dlatego też nazwałem to „małe MVP”. Mamy zgrabny podział na kontrolery i widoki, ale nie jest on zbyt ostry.
Teraz potrzebujemy jeszcze klasy globalnego dyspozytora zdarzeń.

Listing 2. Globalny dyspozytor.

class EventDispatcher implements ActionListener {
	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();
		if (source instanceof EventController) {
			((EventController) source).performAction(e);
		}
	}
}

Ten fragment kodu to tak naprawdę prosta metoda szablonowa, która odpali metodę „kontrolera”. Oczywiście całość jest bardzo uproszczona, bo znowu mieszamy kod widoku i kontrolera… no ale coś za coś. W końcu jest to nasz „pierwszy aplet”.
Na koniec całość składamy do kupy we właściwej klasie apletu:

Listing 3. Prosty aplet

public class MyFirstApplet extends JApplet{

	private MyButton b1, b2;
	private MyLabel tekst;
	private EventDispatcher dispatcher;
	private JPanel jPanel;

	private void buildLayout() {
		jPanel.add(b1);
		jPanel.add(b2);
		jPanel.add(tekst);
		getContentPane().add(jPanel);
	}
	
	@Override
	public void init() {
		super.init();
		initLayout();
		buildLayout();
		initEvents();
	}

	private void initEvents() {
		b1.addActionListener(dispatcher);
		b2.addActionListener(dispatcher);
	}

	private void initLayout() {
		tekst = new MyLabel();
		b1 = new MyButton("naciśnij mnie", tekst, "Nacisnąłeś 1");
		b2 = new MyButton("i mnie", tekst, "Nacisnąłeś 2");
		jPanel = new JPanel();
		dispatcher = new EventDispatcher();
	}
}

Z takim czymś możemy pójść do wykładowcy z prośbą o zaliczenie. Nie jest to naprawdę dobry kod, ale zły też nie jest.
Generalnie istnieją w nim dwa główne problemy:

  • Wymieszanie UI z wywołaniami biznesowymi obecne w klasie MyButton.
  • Stosunkowo mała elastyczność mapowań źródło zdarzenia – obsługa zdarzenia

Można temu zaradzić poprzez delikatną przeróbkę klasy EventDispatcher tak by na koniec dostać coś takiego:

Listing 4. Finalne rozwiązanie

@SuppressWarnings("serial")
class MyButton extends JButton implements EventController {

	private TextProjector textProjector;
	private String text;

	public MyButton(String caption, TextProjector textProjector, String text) {
		super(caption);
		this.textProjector = textProjector;
		this.text = text;
	}

	public void performAction(ActionEvent actionEvent) {
		textProjector.project(text);
	}

	public void dispatch(EventDispatcher eventDispatcher) {
		eventDispatcher.map(this, this);
		super.addActionListener(eventDispatcher);
	}
}

@SuppressWarnings("serial")
public class MyFirstApplet extends JApplet{

	private MyButton b1, b2;
	private MyLabel tekst;
	private EventDispatcherImpl dispatcher;
	private JPanel jPanel;

	private void buildLayout() {
		jPanel.add(b1);
		jPanel.add(b2);
		jPanel.add(tekst);
		getContentPane().add(jPanel);
	}
	
	@Override
	public void init() {
		super.init();
		initLayout();
		buildLayout();
		initEvents();
	}

	private void initEvents() {
		b1.dispatch(dispatcher);
		b2.dispatch(dispatcher);
	}

	private void initLayout() {
		tekst = new MyLabel();
		b1 = new MyButton("naciśnij mnie", tekst, "Nacisnąłeś 1");
		b2 = new MyButton("i mnie", tekst, "Nacisnąłeś 2");
		jPanel = new JPanel();
		dispatcher = new EventDispatcherImpl();
	}
}

interface EventDispatcher extends ActionListener{
	
	public void map(Object eventSource, EventController eventController);
	
}

class EventDispatcherImpl implements EventDispatcher{
	
	private Map eventMappig = new HashMap();
	
	public void map(Object eventSource, EventController eventController){
		eventMappig.put(eventSource, eventController);
	}
	
	public void actionPerformed(ActionEvent event) {
		Object eventSource = event.getSource();
		if(eventMappig.containsKey(eventSource)){
			eventMappig.get(eventSource).performAction(event);
		}
	}
}
interface EventController {

	public void performAction(ActionEvent actionEvent);

}

I tak oto mamy komplet. Co prawda MyButton nadal jest sobie sterem żeglarzem i okrętem, ale przeróbka i wydzielenie części biznesowej do odpowiednich mapowań nie będzie trudne.

Mam nadzieję, że na tym przykładzie załapaliście czym jest MVP. Podstawowa różnica leży w pozbyciu się globalnego obiektu dyspozytora na rzecz „samodysponujących” się widoków. Dobra lecimy z Vaadinową aplikacją…

MVP z Vaadin

Na początek musimy zdefiniować kilka interfejsów, które będą wzajemnie powiązane i będą stanowiły szkielet aplikacji.
Pierwszym z nich jest interfejs prezentera.

Listing 5. Interfejs prezentera

public interface Presenter {

	public void perform(Context context);
	
}

Jest on bardzo abstrakcyjny, zawiera tylko jedną metodę, która będzie wywoływana przez UI gdy zajdzie potrzeba uruchomienia logiki biznesowej. Parametr reprezentuje pewien kontekst wywołania i pozwala na przekazywanie danych.
Kolejnym interfejsem jest DomainView, który będzie głównym interfejsem dla wszystkich elementów UI domeny.

Listing 6. DomainView

public interface DomainView {

	Object getComponent();

}

Metoda getComponent będzie pozwalała na bezpośredni dostęp do komponentu UI. Przy czym nie będzie zwracała konkretnego komponentu, ale tylko pewien obiekt. Pozwala to na wykonywanie operacji bezpośrednio na komponencie przez wyspecjalizowane prezentery. Jednocześnie nie wiąże naszej struktury z konkretnym rozwiązaniem UI (czytaj operator instanceof się przyda). Operacje usuwania/dodawania komponentów można w ten sposób przenieść do oddzielnych prezenterów, które będzie można łatwo wymienić jeżeli będziemy zmieniać dostawcę UI.

Elementy domeny

Popatrzmy przez chwilę na naszą aplikację i określmy poszczególne domeny i ich elementy.
Domena jest jedna związana z zarządzaniem klientem reprezentowanym przez klasę Customer. To jest część nazywana Modelem Domeny. Do bezpośredniego zarządzania służy interfejs CustomerDao. Nie jest on elementem modelu, ale raczej czymś w rodzaju sterownika umożliwiającego dostęp do modelu (coś na wzór JDBC).
W części nazywanej UI Domeny mamy dwa elementy. Pierwszy to tabela wyświetlająca dane klientów. Drugim jest formularz do dodawania i edytowania klientów. Jak widać formularz ma dwa zadania to o jedno za dużo jak na klasę. Jednocześnie nie chcemy powielać kodu formularza. Ot taka ciekawostka projektowa – Jak przestrzegając SRP nie duplikować kodu? Jest to częsty problem.
Trzecią częścią domeny są Operacje Biznesowe Domeny wykonywane na Modelu Domeny za pomocą UI Domeny. Mamy tu dwie operacje. Dodawanie nowego klienta. Edycja istniejącego klienta. Ważna rzecz wyświetlanie listy klientów nie jest operacją ponieważ nie zmienia stanu domeny (nie zmienia obiektów). Wymaga jednocześnie pewnych działań zarówno w warstwie dostępu do danych jak i ich wyświetlania, dlatego warto mieć to zadanie wydzielone do osobnego prezentera.

Interfejsy domeny

Opis modelu pominę. Kompletny kod będzie opublikowany w książce o Vaadin, a na razie wystarczy nam tylko wiedzieć, że takie obiekty są.
Interfejs związany z wyświetlaniem listy klientów jest bardzo prosty:

Listing 7. AllCustomersView

public interface AllCustomersView extends DomainView {

	public void addOrUpdateCustomer(Customer customer);
	
	public void removeCustomer(Customer customer);
	
	public void addOrUpdateAll(Collection customers);
}

Tu widać to co nazwałem w poprzedniej części „SQLem UI”. Prezenter będzie nakazywał UI wykonanie pewnej akcji np. dodanie lub aktualizację danego obiektu, ale nie będzie mówił jak to ma być zrobione. Piękna sprawa 😀
Kolejnym interfejsem UI jest ten do edycji klientów.

Listing 8. CustomersEditorView

public interface CustomerEditorView extends DomainView {

	enum ErrorFieldsFlags{
		UUID, FIRST_NAME, LAST_NAME, AGE, ERROR;
	}
	public void putCustomer(Customer customer);

	public Long getCustomerUUID();

	public String getCustomerFirstName();

	public String getCustomerLastName();

	public Integer getCustomerAge();
	
	public void setError(String fieldName, String message);
}

Ten interfejs jest już znacznie bogatszy pozwala na ustawienie klienta na formularzu oraz na pobranie jego danych. Bonusem jest możliwość dodania informacji o błędzie.
I tu ważna rzecz. Nie potrzebujemy widoku do dodawania klienta ponieważ samo pokazanie formularza może być zrealizowane samodzielnie przez UI. Jednocześnie zapisanie nowego klienta wymaga już prezentera, który zaktualizuje nam widok AllCustomersView.
Teraz dwa interfejsy rozszerzające Presenter, a służące odpowiednio do dodawania nowego klienta:

Listing 9. CustomerAddPresenter

public interface CustomerAddPresenter extends Presenter {

	public void setAllCustomersView(AllCustomersView allCustomersView);
	
	public void setCustomerEditorView(CustomerEditorView customerEditorView);
	
	public void setCustomerDao(CustomerDao customerDao);
}

i jego edycji

Listing 10. CustomerEditPresenter

public interface CustomerEditPresenter extends Presenter {

	public void setAllCustomersView(AllCustomersView allCustomersView);
	
	public void setCustomerEditorView(CustomerEditorView customerEditorView);

	public void setCustomerDao(CustomerDao customerDao);
}

Nie różnią się one zbytnio pomiędzy sobą, ale postanowiłem je rozdzielić by nie doszło do sytuacji, w której edytujemy nieistniejącego klienta. Swoją drogą jest to przykład „kodu bez ifów”.

Dobra to chyba wszystkie ważne elementy na ten moment. Zabierzmy się za implementację…
Stop! Zanim zaimplementujesz interfejs napisz testy 😀 Stara dobra zasada.

Testowanie

Do testów będę używał TestNG i Mockito.
Pytanie co będzie podlegać testowaniu. Na pewno prezentery. Widoki raczej nie, choć dla chętnych zawsze pozostaje zabawa z Selenium.
Przy testach widać też najlepiej różnice pomiędzy MVC, a MVP. Musimy sprawdzić nie tylko to czy część biznesowa została wykonana prawidłowo, ale też czy prezenter prawidłowo wywołał metody widoku UI. W szczególności czy wywołał metody w prawidłowej kolejności. Poniżej test dla CustomerAddPresenter wraz z komentarzami:

Listing 11. Test CustomerAddPresenter

public abstract class CustomerAddPresenterTest {

	private CustomerAddPresenter customerAddPresenter;

	@Mock
	private AllCustomersView allCustomersView;
	@Mock
	private CustomerEditorView customerEditorView;
	@Mock
	private CustomerDao customerDao;

	private Customer customerFromForm;
	private Customer customerAfterSave;

	@BeforeClass
	protected void setUp() throws Exception {
		// inicjalizacja zaślepek
		MockitoAnnotations.initMocks(this);

		// inicjalizacja testowanej implementacji
		customerAddPresenter = getCustomerAddPresenter();
		customerAddPresenter.setAllCustomersView(allCustomersView);
		customerAddPresenter.setCustomerEditorView(customerEditorView);
		customerAddPresenter.setCustomerDao(customerDao);

		// obiekt pobierany z formularza
		customerFromForm = new Customer();
		customerFromForm.setUUID(null);
		customerFromForm.setAge(40);
		customerFromForm.setFirstName("Jan");
		customerFromForm.setLastName("Kowalski");

		// obiekt zwracany przez DAO
		customerAfterSave = new Customer();
		customerAfterSave.setUUID(1L);
		customerAfterSave.setAge(40);
		customerAfterSave.setFirstName("Jan");
		customerAfterSave.setLastName("Kowalski");

		// konfiguracja zaślepek
		when(customerEditorView.getCustomerUUID()).thenReturn(null);
		when(customerEditorView.getCustomerFirstName()).thenReturn("Jan");
		when(customerEditorView.getCustomerLastName()).thenReturn("Kowalski");
		when(customerEditorView.getCustomerAge()).thenReturn(40);
		when(customerDao.saveOrUpdate(customerFromForm)).thenReturn(
				customerAfterSave);
	}

	@AfterClass
	protected void tearDown() throws Exception {
		customerAddPresenter = null;
	}

	@Test
	public void testPerform() {
		// wywołanie zadania
		customerAddPresenter.perform(new Context());

		// pobrał dane z formularza
		verify(customerEditorView, atMost(1)).getCustomerUUID();
		verify(customerEditorView, atLeastOnce()).getCustomerFirstName();
		verify(customerEditorView, atLeastOnce()).getCustomerLastName();
		verify(customerEditorView, atLeastOnce()).getCustomerAge();

		// wywołał w odpowiedniej kolejnosci
		InOrder inOrder = inOrder(customerDao, allCustomersView);
		// zapis danych
		inOrder.verify(customerDao, atLeastOnce()).saveOrUpdate(
				customerFromForm);
		// aktualizację UI
		inOrder.verify(allCustomersView, atLeastOnce()).addOrUpdateCustomer(
				customerAfterSave);
	}

	/**
	 * Metoda zwraca oknkretną implementację prezentera.
	 * @return
	 */
	public abstract CustomerAddPresenter getCustomerAddPresenter();
}

Test jest abstrakcyjny, bo powinna go przechodzić każda implementacja interfejsu. Każda implementacja powinna być dostarczona z odpowiednią klasą rozszerzającą ten test, która będzie dostarczać tej implementacji oraz może zawierać dodatkowe testy.

Implementacja prezenterów

To jest ta prostsza część… Nie będziemy na razie dotykać naszego kodu Vaadin, a tylko stworzymy klasy, które później zastąpią nam kod w obecnej aplikacji… trochę taka refaktoryzacja przez pisanie od nowa, ale tak jest łatwiej.
Na początek implementacja prezentera, który będzie obsługiwał dodawanie użytkownika:

Listing 12. CustomerAddPresenterImpl

public class CustomerAddPresenterImpl implements CustomerAddPresenter {

	private CustomerDao customerDao;

	private CustomerEditorView customerEditorView;

	private AllCustomersView allCustomersView;

	private RemoveComponentPresenter removeComponentPresenter;

	public CustomerAddPresenterImpl(
			RemoveComponentPresenter removeComponentPresenter) {
		this.removeComponentPresenter = removeComponentPresenter;
	}

	public void perform(Context context) {
		// pobieramy dane z formularza
		String customerFirstName = customerEditorView.getCustomerFirstName();
		String customerLastName = customerEditorView.getCustomerLastName();
		Integer customerAge = customerEditorView.getCustomerAge();

		// tworzymy obiekt Customer
		Customer customer = new Customer(customerFirstName, customerLastName,
				customerAge);

		// utrwalamy za pomocą utrwalacza
		customer = customerDao.saveOrUpdate(customer);

		// aktualizujemy spis klientów
		allCustomersView.addOrUpdateCustomer(customer);

		// usuwamy formularz z UI
		context.put(RemoveComponentPresenter.OBJECT_TO_REMOVE,
				customerEditorView);
		removeComponentPresenter.perform(context);
	}

	public void setAllCustomersView(AllCustomersView allCustomersView) {
		this.allCustomersView = allCustomersView;
	}

	public void setCustomerEditorView(CustomerEditorView customerEditorView) {
		this.customerEditorView = customerEditorView;
	}

	public void setCustomerDao(CustomerDao customerDao) {
		this.customerDao = customerDao;
	}
}

Jak widać nie jest to trudna klasa. Widać też od razu różnicę pomiędzy MVC, a MVP polegającą na tym, że Prezenter może też zarządzać elementami UI. Robi to oczywiście deklaratywnieW, bo nie wie czym konkretnie są te elementy. Kolejna zbieżność z SQL czy też Prologiem(tfu…).
Teraz edycja użytkownika.

Listing 13. CustomerEditPresenterImpl

public class CustomerEditPresenterImpl implements CustomerEditPresenter {

	private CustomerDao customerDao;

	private CustomerEditorView customerEditorView;

	private AllCustomersView allCustomersView;

	private RemoveComponentPresenter removeComponentPresenter;

	public CustomerEditPresenterImpl(
			RemoveComponentPresenter removeComponentPresenter) {
		this.removeComponentPresenter = removeComponentPresenter;
	}

	public void perform(Context context) {
		// pobieramy dane z formularza
		Long customerUUID = customerEditorView.getCustomerUUID();
		String customerFirstName = customerEditorView.getCustomerFirstName();
		String customerLastName = customerEditorView.getCustomerLastName();
		Integer customerAge = customerEditorView.getCustomerAge();

		// tworzymy obiekt Customer
		Customer formCustomer = new Customer(customerFirstName, customerLastName,
				customerAge);
		formCustomer.setUUID(customerUUID);
		// czy klienty istnieje w bazie
		if(customerDao.getById(customerUUID)==null){
			customerEditorView.setError(ErrorFieldsFlags.ERROR, String.format("Customer with ID: %d not exist!", customerUUID));
			return;
		}
		
		// utrwalamy za pomocą utrwalacza
		formCustomer = customerDao.saveOrUpdate(formCustomer);

		// aktualizujemy spis klientów
		allCustomersView.addOrUpdateCustomer(formCustomer);

		// usuwamy formularz z UI
		context.put(RemoveComponentPresenter.OBJECT_TO_REMOVE,
				customerEditorView);
		removeComponentPresenter.perform(context);
	}

	public void setAllCustomersView(AllCustomersView allCustomersView) {
		this.allCustomersView = allCustomersView;
	}

	public void setCustomerEditorView(CustomerEditorView customerEditorView) {
		this.customerEditorView = customerEditorView;
	}

	public void setCustomerDao(CustomerDao customerDao) {
		this.customerDao = customerDao;
	}
}

Nie jest specjalnie bardziej skomplikowana. Generalnie wzbogacona jest o sprawdzenie czy klient o danym UUID istnieje w bazie danych. Jeżeli nie to zwracamy błąd i przerywamy radosną twórczość.

Czas na bardziej skomplikowane zabawy…

Implementacja Widoków

Dróg mamy tu klika. Najprostsza to tak jak w przypadku apletu stworzenie klas rozszerzających komponenty UI, w tym przypadku Vaadin, o implementację odpowiednich interfejsów. Inna wersja to stworzenie własnych komponentów obudowujących komponenty UI. Obie drogi są dobre i dość podobne.

Na początek „moja” wersja tabeli z klientami:

Listing 14. CustomerTable

@SuppressWarnings("serial")
public class CustomerTable extends Table implements AllCustomersView {

	private static final String AGE = "Wiek";
	private static final String LAST_NAME = "Nazwisko";
	private static final String FIRST_NAME = "Imię";
	private static final String ID = "Id";
	private AllCustomersPresenter allCustomersPresenter;

	public CustomerTable() {
		super("Klienci");
		addContainerProperty(ID, Long.class, null);
		addContainerProperty(FIRST_NAME, String.class, null);
		addContainerProperty(LAST_NAME, String.class, null);
		addContainerProperty(AGE, Integer.class, null);
		allCustomersPresenter = new AllCustomersPresenter(this);
		allCustomersPresenter.setCustomerDao(MyVaadinApplication
				.getCustomerDao());
		loadData();
	}

	public Customer getCustomerById(Object id){
		Item item = getItem(id);
		Customer customer = new Customer();
		customer.setUUID((Long) item.getItemProperty(ID).getValue());
		customer.setFirstName(item.getItemProperty(FIRST_NAME).getValue().toString());
		customer.setLastName(item.getItemProperty(LAST_NAME).getValue().toString());
		customer.setAge((Integer) item.getItemProperty(AGE).getValue());		
		return customer;
	}
	
	public void loadData() {
		allCustomersPresenter.perform(null);
	}

	private void addCustomer(Customer customer) {
		addItem(new Object[] { customer.getUUID(), customer.getFirstName(),
				customer.getLastName(), customer.getAge() }, customer.getUUID());
	}

	public void addOrUpdateAll(Collection customers) {
		for (Customer customer : customers) {
			addCustomer(customer);
		}
	}

	public void addOrUpdateCustomer(Customer customer) {
		Item updatedItem = getItem(customer.getUUID());
		if (updatedItem == null) {
			addCustomer(customer);
		} else {
			updatedItem.getItemProperty(FIRST_NAME).setValue(
					customer.getFirstName());
			updatedItem.getItemProperty(LAST_NAME).setValue(
					customer.getLastName());
			updatedItem.getItemProperty(AGE).setValue(customer.getAge());

		}
	}

	public Object getComponent() {
		return this;
	}

	public void removeCustomer(Customer customer) {
		removeItem(customer.getUUID());
	}
}

Jak widać kod, który w poprzeniej wiersji był używany do aktualizacji tabeli teraz jest używany do implementacji interfejsu. Ładnie. No nie do końca, bo ręcznie musimy wiązać DAO z prezenerem, ale od czego jest kontener DI? Ja go na razie nie używam, ale później można go dodać jakby co.

Kolejnym elementem jaki będziemy implementować jest formularz dodawania/edycji użytkownika. W tym przypadku wykorzystam metodę z tworzeniem własnego komponentu. Dlaczego? Ponieważ metoda ta jest w ogólności lepsza. Pozwala nam na zamknięcie komponentu w taki sposób, że użytkownik nie będzie wstanie nic w nim zmienić.

Listing 15. CustomerForm

@SuppressWarnings("serial")
public class CustomerForm extends CustomComponent implements CustomerEditorView {

	private Form customerFrm;
	private TextField uuidFld;
	private TextField firstNameFld;
	private TextField lastNameFld;
	private TextField ageFld;
	private Label errorLbl;
	private Button okBtn;
	private Button resetBtn;
	private Button cancelBtn;
	private CustomerPresenter customerPresenter;
	private Panel panel;

	public CustomerForm(String caption, CustomerPresenter customerPresenter) {
		super();
		this.customerPresenter = customerPresenter;
		customerPresenter.setCustomerEditorView(this);
		panel = new Panel(caption);

		customerFrm = new Form();

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

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

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

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

		errorLbl = new Label();

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

		configureEventListeners();

		VerticalLayout footerLayout = new VerticalLayout();
		footerLayout.addComponent(errorLbl);
		footerLayout.addComponent(buttonBar);

		customerFrm.setFooter(footerLayout);

		panel.addComponent(customerFrm);
		setCompositionRoot(panel);
	}

	private void configureEventListeners() {
		okBtn.addListener(new ClickListener() {

			public void buttonClick(ClickEvent event) {
				customerPresenter.perform(new Context());
			}
		});
		
		
		resetBtn.addListener(new ClickListener() {

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

			public void buttonClick(ClickEvent event) {
				CustomerForm customerForm = (CustomerForm) getComponent();
				((ComponentContainer) customerForm.getParent())
						.removeComponent(customerForm);
			}
		});
	}

	public Object getComponent() {
		return this;
	}

	public void putCustomer(Customer customer) {
		if (customer.getUUID() != null)
			uuidFld.setValue(customer.getUUID());
		firstNameFld.setValue(customer.getFirstName());
		lastNameFld.setValue(customer.getLastName());
		ageFld.setValue(customer.getAge());
	}

	public Long getCustomerUUID() {
		String uuid = uuidFld.getValue().toString();
		if (uuid != null && uuid.length() > 0)
			return Long.parseLong(uuid);
		return null;
	}

	public String getCustomerFirstName() {
		return firstNameFld.getValue().toString();
	}

	public String getCustomerLastName() {
		return lastNameFld.getValue().toString();
	}

	public Integer getCustomerAge() {
		String age = ageFld.getValue().toString();
		if (age != null && age.length() > 0)
			return Integer.parseInt(age);
		return null;
	}

	public void setError(ErrorFieldsFlags fieldName, String message) {
		errorLbl.setValue(message);
	}
}

Tu też mamy do czynienia z pewnymi niedoskonałościami. Jednak z drugiej strony jest bardzo dobrze. Jak widać w razie potrzeby widok odwołuje się do prezentera, ale jeżeli akcja nie wymaga żadnych działań to jest wykonywana „własnymi siłami”.

Aplikacja

Na koniec aplikacja Vaadin z poprzedniego odcinka w wersji MVP:

Listing 16. Aplikacja Vaadin + MVP

public class MyVaadinApplication extends Application {

	public static CustomerDao getCustomerDao() {
		return customerDao;
	}

	private Window window;

	private static CustomerDao customerDao = new CustomerDaoCollectionImpl();

	private CustomerTable customersTbl;

	private Button addCustomerBtn;

	private CustomerPresenter editPresenter;

	private CustomerPresenter addPresenter;

	@Override
	public void init() {
		window = new Window("My Vaadin Application");
		setMainWindow(window);

		initCustomersTbl();
		initAddCustomerBtn();
		initPresenters();

		window.addComponent(customersTbl);
		window.addComponent(addCustomerBtn);
	}

	private void initAddCustomerBtn() {
		addCustomerBtn = new Button("Dodaj Klienta");
		addCustomerBtn.addListener(new Button.ClickListener() {
			public void buttonClick(ClickEvent event) {
				showAddCustomerForm("Dodaj Klienta", addPresenter, null);
			}
		});
	}

	private void initCustomersTbl() {
		customersTbl = new CustomerTable();
		customersTbl.addListener(new ItemClickListener() {

			public void itemClick(ItemClickEvent event) {
				showAddCustomerForm("Edytuj klienta", editPresenter,
						customersTbl.getCustomerById(event.getItemId()));
			}
		});
		customersTbl.loadData();
	}

	private void initPresenters() {
		addPresenter = new CustomerAddPresenterImpl(
				new RemoveVaadinComponentPresenter());
		addPresenter.setCustomerDao(getCustomerDao());
		addPresenter.setAllCustomersView(customersTbl);

		editPresenter = new CustomerEditPresenterImpl(
				new RemoveVaadinComponentPresenter());
		editPresenter.setCustomerDao(getCustomerDao());
		editPresenter.setAllCustomersView(customersTbl);
	}

	protected void showAddCustomerForm(String caption,
			CustomerPresenter customerPresenter, Customer customer) {
		CustomerForm customerForm = new CustomerForm(caption, customerPresenter);
		if (customer != null)
			customerForm.putCustomer(customer);
		window.addComponent(customerForm);
	}
}

Interfejs CustomerPresenter wymyśliłem „w locie” by ogranicznyć ilość wersji konstruktora. Obecnie to w nim są zdefiniowane metody z interfejsów CustomerAddPresenter i CustomerEditPresenter, a te interfejsy przejęły rolę znacznikową.

Podsumowanie

Jak widać wzorzec MVP nie jest trudny. Główne różnice w stosunku do MVC leżą w sposobie komunikacji pomiędzy Prezenterem/Kontolerem a Widokiem. W przypadku MVP widok traktowany jest jako obiekt o pewnym interfejsie do którego można się odwoływać. W MVC można tak napisać całość, że kontrolery nawet nie będą wiedziały, że istnieje coś takiego jak widok.
Który lepszy? Oba służą do trochę innych zadań ponieważ pracują w różnych środowiskach. Nie można ich zatem porównywać wprost. Niewątpliwie MVC jest znacznie popularniejszy ze względu na dużą grupę użytkowników. MVP dotychczas siedział sobie w swoim .NETowyś środowisku i dopiero stosunkowo niedawno został wyciagnięty przed obliczę braci javowej.
Dla mnie, ze względu na charakterystykę środowiska Vaadin wygodniejszy jest MVP.

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