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<customer> 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);
}
}
</customer>
Zajmiemy się też Apletem z listingu 1.