<

7. Wzorce projektowe (3)


Na zakończenie cyklu o wzorcach projektowych poznamy dwa niezwykle ważne wzorce: Command i Model-View-Controller - MVC).  Oba wzorce (a szczególnie wzorzec MVC) są kluczowy dla tworzenia iniwerslanych, elastycznych, łatwo modyfikowalnych programów i systemów


1. Wzorzec Command

Wzorzec Comamnd pozwala na enkapsulację zlecenia do wykonania jako obiektu klasy implementującej określony interfejs. Dzięki temu można uniezależniać logikę działania aplikacji od konkretnych zleceń do wykonania, czyniąc w ten sposób kody łatwe w modyfikacji i prowadzeniu


Diagram GoF (klasy):

 

Diagram GOF (sekwencja działań)

 


Rozważmy przykład.
Stworzymy (dość uniwersalny) interfejs, opisujący różnorakie działania:
import java.util.*;

public interface Command {
   void init();
   void setParameter(String name, Object value);
   Object getParameter(String name);
   void execute();
   List getResults();
   void setStatusCode(int code);
   int getStatusCode();
}
Zatem każda klasa działania powinna implementować metody inicjacji, ustalania i pobierania ew. parametrów, wykonania działania, ustalenia i pobrania kodu wyniku, pobrania wyników działania. Umówimy się, że wyniki będą dostępne jako lista.

Dla ułatwienia implementacji tych metod w konkretnych klasach dostarczymy ich standardowej implementacji, która m.in. zawiera  mapę parametrów i listę wyników i dostarcza gotowych i wystarczających definicji metod ustalania, pobierania parametrów i wyników, a także dodatkowych metod pozwalających tworzyć elementy listy  wyników. Przyjmiemy, że w tej standardowej implementacji każdy element wyników stanowi tablicę dowolnych obiektów, uwzględniając przy tym, że częstym przypadkiem elementu wyników będzie zwykły napis (stąd przeciążana metoda addResult). Klasę moglibyśmy uczynić abstrakcyjną, bowiem nieznane są jeszcze metody inicjacji (init()) oraz  wykonania działań (execute()), ale wygodnie będzie potraktować ją raczej jako adapter, dostarczając pustych definicji tych metod.

import java.util.*;
import java.io.*;

public class CommandImpl implements Serializable, Command {

  private Map parameterMap = new HashMap();
  private List resultList = new ArrayList();

  private int statusCode;

  public CommandImpl() {}

  public void init() {}

  public void setParameter(String name, Object value) {
    parameterMap.put(name, value);
  }

  public Object getParameter(String name) {
    return parameterMap.get(name);
  }

  public void execute() {}

  public List getResults() {
    return resultList;
  }

  public void addResult(Object o) {
    resultList.add(o);
  }

  public void addResult(String s) {
    addResult(new Object[] { s } );
  }


  public void clearResult() {
    resultList.clear();
  }

  public void setStatusCode(int code) {
    statusCode = code;
  }

  public int getStatusCode() {
    return statusCode;
  }

}

Taką standardową implementację interfejsu Command mogą teraz odziedziczyć dowolne klasy, które wykonują dowolne działania.

Np.  konkretna klasa wyszukiwania wyrażeń regularnych.

Działanie które wykonuje obiekt tej klasy ma (niewątpliwie) dwa parametry: regularne wyrażenie i przeszukiwany tekst. Wyniki wyszukiwania będą przedstawiane na liście w postaci: znaleziony podłańcuch, pozycję na której się on zaczyna i pozycję na której się kończy. Ustalimy również kody wyniku: 0 - znaleziono jedno lub więcej dopasowań, 1 -  brak parametrów, 2 - błąd w wyrażeniu, 3 - brak dopasowania.
import java.util.*;
import java.io.*;
import java.util.regex.*;

public class FindCommand extends CommandImpl implements Serializable {

  public FindCommand() {}

  public void execute() {
    clearResult();
    String regex = (String) getParameter("regex");
    String input = (String) getParameter("input");
    if (regex == null || input == null) {
      setStatusCode(1);
      return;
    }
    Pattern pattern;
    try {
      pattern = Pattern.compile(regex);
    } catch (PatternSyntaxException exc) {
       setStatusCode(2);
       return;
    }
    Matcher matcher = pattern.matcher(input);
    boolean found = matcher.find();
    if (!found) setStatusCode(3);
    else {
      setStatusCode(0);
      do {
         addResult("\"" + matcher.group() + "\"" + " " +
                                  matcher.start() +  " " +
                                  matcher.end());

      } while(matcher.find());
    }
  }

}

Możemy też stworzyć całkiem inne, wykonujące zupełnie inne działania klasy.

Dzięki zastosowaniu wzorca Command interfejs do:

może być całkowicie niezależny od konkretnego zadania do wykonania.

Oto przykład:
import java.util.*;
import javax.swing.*;

public class CommandTest {

  public CommandTest() {
    String req = JOptionPane.showInputDialog("Podaj zlecenie do wykonania");
    List resList = serviceRequest(req);
    for (Object o : resList) {
      if (o.getClass().isArray()) {
        Object[] arr = (Object[]) o;
        for (int i=0; i<arr.length; i++) System.out.println(arr[i]);
      } else System.out.println(o);
    }

  }

  // Metoda obsługi zleceń
  // niezależna od konkretnych zleceń

  public List serviceRequest(String req) {
    Command cmd = null;
    try {
      Class klasa = Class.forName(req);
      cmd = (Command) klasa.newInstance();
      String data = JOptionPane.showInputDialog(
                    "-------------------  Podaj parametry -----------------");
      StringTokenizer st = new StringTokenizer(data, "()");
      while (st.hasMoreTokens()) {
        String para = st.nextToken();
        String[] parm = para.split("#");
        cmd.setParameter(parm[0], parm[1]);
      }
      cmd.execute();
    } catch (Exception exc) { exc.printStackTrace(); }
    return cmd.getResults();
  }

Tutaj stosujemy następującą konwencję:
Zobacz prezentację działania tego programu dla przypadku uzycia jako zlecenia omówionej wcześniej klasy FindCommand.

2. Model-View-Controller: idea

Wzorze MVC polega na separacji fragmentów kodu pomiędzy części:


Taka separacja umożliwia bardziej elastyczne i logiczne tworzenie kodu, m.in. dzięki skupieniu uwagi na modelu danych, który może następnie być prezentowany przez różne widoki.

W dużym stopniu pozwala także na uniezalęznienie fragmentów kodu odpowiedzoalnych za model, widok i kontroler. Dzięki temu modyfikacje kodu stają się znacznie łatwiejsze, zwiększają się możliwości ponownego użycia gotowych klas (modeli, kontrolerów i widoków).

Podstawowe idee MVC przezentuje poniższy rysunek.

1

Źróło: J2EE Blueprint

3. MVC - prosty przykład


Przypomnijmy znany nam już z wykładu "Metod programowania" prosty przykład.

Będziemy budować klasę-licznik jako JavaBean. Klasę nazwiemy Counter.
Licznik będzie miał jedną właściwość o nazwie count (stan licznika) oraz metody zmian tego stanu.

Sama klasa Counter odzwierciedla logikę działania licznika ("model "). Obiekty tej klasy "są niewidzialne", a zatem  żeby zobaczyć licznik musimy stworzyć dodatkową klasę, która zdefiniuje widok licznika  (view).
Nazwiemy ją CounterView. Warto zwrócić uwagę: separacja kodu jest korzystna – widok uniezależniamy od modelu, a model od widoku, w ten sposób możemy mieć np. wiele widoków licznika, lub zmieniać model nie zmieniając widoku.

Komunikacja między modelem i widokiem będzie się odbywać na zasadzie nasłuchu zmian właściwości (zmian właściwości count) czyli obiekt klasy CounterView będzie też słuchaczem zmian właściwości (PropertyChangeListener).

Musimy też mieć jakieś środki zmiany stanu licznika. Interakcję użytkownika z modelem/widokiem zapewnia  kontroler.
Klasę CounterControlGui, zapewnia interfejs interakcji z licznikiem.. Widok zostanie dodany do tego GUI (ale kody obu klas będą odseparowane).

Dostarczymy również obiektu-nadzorcy, który będzie sprawdzał czy zmiana właściwości jest dopuszczalna i jeśli stwierdzi, że nie – będzie wetował tę zmianę. Odpowiednią klasę nazwiemy CounterLimitator.

r

Oto kody klas:
Model
import java.awt.event.*;
import java.beans.*;
import java.io.*;

public class Counter implements Serializable {

  private int count = 0;
  private PropertyChangeSupport propertyChange = new PropertyChangeSupport(this);
  private VetoableChangeSupport vetos = new VetoableChangeSupport(this);

public Counter() throws PropertyVetoException {
  this(0);
}

public Counter(int aCount) throws PropertyVetoException {
  setCount( aCount );
}


public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
  propertyChange.addPropertyChangeListener(listener);
}

public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
  propertyChange.removePropertyChangeListener(listener);
}

public synchronized void addVetoableChangeListener(VetoableChangeListener l) {
  vetos.addVetoableChangeListener(l);
}

public synchronized void removeVetoableChangeListener(VetoableChangeListener l) {
  vetos.removeVetoableChangeListener(l);
}


public void increment() throws PropertyVetoException {
  setCount(getCount()+1);
}

public void decrement() throws PropertyVetoException {
  setCount(getCount()-1);
}

public int getCount() {
  return count;
}


public synchronized void setCount(int aCount)
                         throws PropertyVetoException {
  int oldValue = count;
  vetos.fireVetoableChange("count", new Integer(oldValue), new Integer(aCount));
  count = aCount;
  propertyChange.firePropertyChange("count", new Integer(oldValue), new Integer(aCount));
}
}
Widok:

import javax.swing.*;
import java.awt.*;
import java.beans.*;
import java.io.*;

public class CounterView extends JLabel implements PropertyChangeListener, Serializable {

  CounterView()  {
     this("0");
  }
  CounterView(String lab) {
     super(lab);
     setOpaque(true);
     setBorder(BorderFactory.createLineBorder(Color.black));
     setPreferredSize(new Dimension(75, 40));
     setHorizontalAlignment(CENTER);
  }

  public void setLabel(String s) {
     setText(s);
  }

  public void propertyChange(PropertyChangeEvent e)  {
    Integer oldVal = (Integer) e.getOldValue(),
           newVal = (Integer) e.getNewValue();
    System.out.println("Value changed from " + oldVal + " to " + newVal);
    setText("" + newVal + "");
   }


}

Kontroler:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;

public class CounterControlGui extends JFrame implements ActionListener {

private Counter counter;
private CounterView counterView;
private CounterLimitator clim;
private JButton binc = new JButton("Increment");
private JButton bdec = new JButton("Decrement");
private JTextField txt = new JTextField(10);
private JButton save = new JButton("Save");
private JButton load = new JButton("Load");

private static final String fname = "counter.xml";


CounterControlGui(Counter c) {
  installCounter(c);
  Container cp = getContentPane();
  cp.setLayout(new FlowLayout());
  binc.addActionListener(this);
  cp.add(binc);
  cp.add(counterView);
  bdec.addActionListener(this);
  cp.add(bdec);
  txt.addActionListener(this);
  cp.add(txt);
  save.addActionListener(this);
  cp.add(save);
  load.addActionListener(this);
  cp.add(load);
  setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  pack();
  show();
}


public void actionPerformed(ActionEvent e)  {
  try  {
    if (e.getSource() == txt)  {
       int n = 0;
       try  {
          n = Integer.parseInt(txt.getText());
       } catch (NumberFormatException exc)  { return; }
       counter.setCount(n);
       return;
    }
    String cmd = e.getActionCommand();
    if (cmd.equals("Increment")) counter.increment();
    else if (cmd.equals("Decrement")) counter.decrement();
    else if (cmd.equals("Save")) {
      try {
        XMLEncoder enc = new XMLEncoder(
                           new BufferedOutputStream(
                              new FileOutputStream(fname)
                              )
                        );
        enc.writeObject(counter);
        enc.close();
      } catch (Exception exc) {
          exc.printStackTrace();
      }
    }
    else if (cmd.equals("Load")) {
      try {
        XMLDecoder dec = new XMLDecoder(
                           new BufferedInputStream(
                              new FileInputStream(fname)
                              )
                        );
        Counter c = (Counter) dec.readObject();
        dec.close();
        installCounter(c);
      } catch (Exception exc) {
          exc.printStackTrace();
      }
    }
    else System.out.println("Unrecognized command");
  } catch (PropertyVetoException exc)  {
       System.out.println(""+ exc);
  }
}

 public void installCounter(Counter c) {
   if (counter != null) {
    counter.removePropertyChangeListener(counterView);
    counter.removeVetoableChangeListener(clim);
   }
   counter = c;
   if (counterView == null)
    counterView = new CounterView(""+counter.getCount());
   else counterView.setLabel(counter.getCount() + "" );
   counter.addPropertyChangeListener(counterView);
   if (clim == null) clim = new CounterLimitator(-5, 10);
   counter.addVetoableChangeListener(clim);
 }


 public static void main(String[] args) throws PropertyVetoException {
   new CounterControlGui(new Counter());
 }
}
Limitator:

import java.beans.*;

public class CounterLimitator implements VetoableChangeListener {

private int min, max;

CounterLimitator(int minLim, int maxLim)  {
  min = minLim;
  max = maxLim;
}

public void vetoableChange(PropertyChangeEvent e)
            throws PropertyVetoException {
   Integer newVal = (Integer) e.getNewValue();
   int val = newVal.intValue();
   if (val < min || val > max)
      throw new PropertyVetoException("Niedopuszczalna zmiana wartości", e);
   }
}

Integracja:

import java.beans.*;
public class Main {

 Counter counter;
 CounterView counterView;
 CounterLimitator clim;
 CounterControlGui gui;

 public Main() throws PropertyVetoException {
   installCounter(new Counter());
 }

 public void installCounter(Counter c) {
   if (counter != null) {
    counter.removePropertyChangeListener(counterView);
    counter.removeVetoableChangeListener(clim);
   }
   counter = c;
   if (counterView == null)
    counterView = new CounterView(""+counter.getCount());
   else counterView.setLabel(counter.getCount() + "" );
   counter.addPropertyChangeListener(counterView);
   if (clim == null) clim = new CounterLimitator(-5, 10);
   counter.addVetoableChangeListener(clim);
   if (gui == null) {
     gui = new CounterControlGui(counter);
   }
   else gui.installCounter(counter);
 }


 public static void main(String[] args) throws PropertyVetoException {
   new Main();
 }

}

Zobacz demonstrację działania programu:





To jest klasyczny przykład MVC (inne widzieliśmy w  ramach komponentów Swingu).
Wada przykładu: zbyt duże powiązania w kodzie - niektóre klasy są zależne od innych (od konkretnych implementacji). 

Rozwiązanie: programowanie w kategoriach interfejsów, zastosowanie fabryk, "Dependency Injection".

4. MVC w aplikacjach WEB

Przypominając sobie materiał o aplikacjach WEB rozważmy przykładowy kod  z serwletu wyrażeń regularnych z oznaczeniami:

    try {
      Pattern pattern = Pattern.compile(regex);                   // Logika
      Matcher matcher = pattern.matcher(input);                   // Logika
      boolean found = matcher.find();                             // Logika
      if (!found)                                                 // Logika
         out.println("<h3>Nie znaleziono żadnego podłańcucha " +  // Prezentacja
                      "pasującego do wzorca</h3>");
      else {                                                      // Logika
          out.println("<h3>Dopasowano:</h3>");                    // Prezentacja
          out.println("<ol>");                                    // Prezentacja
        do {
          out.println("<li>podłańcuch \"" + matcher.group() +     // L + P
                   "\" od pozycji " + matcher.start() +           // L + P
                   " do pozycji " + (matcher.end()-1) + "</li>"); // L + P
        } while(matcher.find());
        out.println("</ol>");                                     // Prezentacja
      }
    } catch (PatternSyntaxException exc) {                        // Logika
        out.println("<h2>Błąd w wyrażeniu</h2>");                 // Prezentacja
    } finally {                                                   // Logika
        printEndTag();                                            // Prezentacja
        out.close();                                              // Prezentacja
    }

Widzimy jak bardzo zmieszane ze sobą są fragmenty odpowiedzialne za logikę i prezentację.
Co się stanie, gdy będziemy chcieli dokonać prezentacji w innej formie?
Co się stanie, gdy np. wyniki wyszukiwania w ogóle nie powinny podlegać prezentacji bezpośredniej, lecz raczej winny być przekazywane jako strumień do jakiegoś klienta HTTP?
Takie zmiany będą wymagały pisania całego kodu od początku.

Rozwiązaniem jest odpowiednie zastowanie wzorca MVC.

Bardzo ogólny diagram tego podejścia przedstawia poniższy rysunek.

r


Zgodnie z tym wzorcem aplikację WEB podzielimy na cztery zasadnicze części:
Chcielibyśmy przy tym zapewnić, aby:
  1. serwlet-kontroler mógł bez rekompilacji obsługiwać dowolne klasy działań (!) oraz przekazywać zadania pobierania parametrów i prezentacji wyników dowolnym serwletom pobierania parametrów i pokazywania wyników,
  2. serwlet pobierania parametrów nie był zależny od nazw i opisu parametrów (w szczególności ich zlokalizowanych opisów),
  3. serwlet prezentacji mógł być wykorzystywany do prezentacji dowolnych wyników.

Warunek 1 realizujemy poprzez zastosowanie wzorca Command.

Działaniem aplikacji będzie zarządzał serwlet-kontroler, przy czym - jak wspomniano - jego kod uczynimy niezależnym od sposobu pobierania parametrów (serwletu pobierania parametrów), wykonywanych działań (klasy działania) oraz sposobu prezentacji wyników (serwletu-prezentacji). Niezależność tę uzyskamy dostarczając inicjalnych parametrów kontekstu w deskryptorze wdrożenia (web.xml):

     .....
    <context-param>
       <param-name>presentationServ</param-name>
       <param-value>/presentation</param-value>
    </context-param>

    <context-param>
       <param-name>getParamsServ</param-name>
       <param-value>/getparams</param-value>
    </context-param>

    <context-param>
       <param-name>commandClassName</param-name>
       <param-value>FindCommand</param-value>
    </context-param>
    .....
 i pobierając je przy inicjacji serwletu

public class ControllerServ extends HttpServlet {

  private ServletContext context;
  private Command command;            // obiekt klasy dzialania
  private String presentationServ;    // nazwa serwlet prezentacji
  private String getParamsServ;       // mazwa serwletu pobierania parametrów
  private Object lock = new Object(); // semafor dla synchronizacji
                                      // odwołań wielu wątków
  public void init() {

    context = getServletContext();

    presentationServ = context.getInitParameter("presentationServ");
    getParamsServ = context.getInitParameter("getParamsServ");
    String commandClassName = context.getInitParameter("commandClassName");

    // Załadowanie klasy Command i utworzenie jej egzemplarza
    // który będzie wykonywał pracę
    try {
      Class commandClass = Class.forName(commandClassName);
      command = (Command) commandClass.newInstance();
    } catch (Exception exc) {
        throw new NoCommandException("Nie mogę stworzyć obiektu klasy " +
                                      commandClassName);
    }
  }

// ...

Zwróćmy uwagę, że piszemy ten kod w kategoriach interfejsu Command. Dynamicznie ładujemy klasę podaną jako parametr kontekstu (tu FindCommand) i tworzymy jej obiekt. Zmiana wykonywanego działania (np. zamiast wyszukiwania jakieś obliczenia matematyczne) wymaga tylko podania innej wartości parametru kontekstu commandClassName. Może się okazać, że podamy niewłaściwą nazwę klasy lub będzie ona źle zbudowana (np. brak konstruktora bezparametrowego). Wtedy - jak już wiemy - wystąpi wyjątek ClassNotFoundException lub InstantiantionException. Obsługujemy oba te wyjątki poprzez zgłoszenie własnego wyjątku NoComamndException.
public class NoCommandException extends RuntimeException {
  public NoCommandException() { super(); }
  public NoCommandException(String msg) { super(msg); }
}
Jak widać, klasę wyjątku uczyniliśmy pochodną od RuntimeException, dzięki czemu nie musimy go ani obsługiwać w tym miejscu ani zgłaszać w klauzuli throws metody init(), co byłoby niedozowolone (bowiem metoda init() w klasie GenericServlet deklaruje możliwość zgłaszania wyjątku klasy ServletException, a jej przedefiniowanie nie może tego zmienić). Ten sposób oprogramowania umożliwia np. przygotowanie strony HTML z komunikatem o przyczynach błędu, która będzie automatycznie ładowana jeśli użyjemy elementu error-pages w pliku deskryptora wdrożenia.

Obsługa przychodzących zleceń polega na wywołaniu serwletu pobierania parametrów, ustaleniu tych parametrów dla obiektu typu Command, wykonaniu działań (metoda execute() z Command), pobraniu wyników i udostępnieniu ich serwletowi prezentacji, któremu na samym końcu przekażemy sterowanie. Parametry będą zapisywane przez serwlet pobierania parametrów jako atrybuty sesji z przedrostkiem param_.  Uniezależnimy kody serwletów od nazw parametrów: wygodnym rozwiązaniem będzie zastosowanie ResourceBundle, bo przy okazji zinternacjonalizujemy całą aplikację. Po to by wygodnie sięgać po zlokalizowaną i sparametryzowaną informację z różnych serwletów naszej aplikacji przygotowano własną klasę BundleInfo, która tę informację porządkuje (więcej o tym za chwilę). Wreszcie serwlet-kontroler musi udostępnić listę wyników serwletowi prezentacji. Tu również zastosujemy atrybut sesji (naturalnie - parametry i wyniki są związane ze zleceniami od jednego i tego samego klienta).
Kod obsługi zleceń w serwlecie-kontrolerze wygląda więc w następujący sposób.
public class ControllerServ extends HttpServlet {

   // ...
   private Object lock = new Object();  // semafor
   // ...
 
  // Obsługa zleceń
  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {

    resp.setContentType("text/html");

    // Wywolanie serwletu pobierania parametrów
    RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
    disp.include(req,resp);

    // Pobranie bieżącej sesji
    // i z jej atrybutów - wartości parametrów
    // ustalonych przez servlet pobierania parametrów
    // Różne informacje o aplikacji (np. nazwy parametrów)
    // są wygodnie dostępne poprzez własną klasę BundleInfo

    HttpSession ses = req.getSession();

    String[] pnames = BundleInfo.getCommandParamNames();
    for (int i=0; i<pnames.length; i++) {

      String pval = (String) ses.getAttribute("param_"+pnames[i]);

      if (pval == null) return;  // jeszcze nie ma parametrów

      // Ustalenie tych parametrów dla Command
      command.setParameter(pnames[i], pval);
    }

    // Wykonanie działań definiowanych przez Command
    // i pobranie wyników
    // Ponieważ do serwletu może naraz odwoływać sie wielu klientów
    // (wiele watków) - potrzebna jest synchronizacja

    synchronized(lock) {
      // wykonanie
      command.execute();

      // pobranie wyników
      List results = (List) command.getResults();

      // Pobranie i zapamiętanie kodu wyniku (dla servletu prezentacji)
      ses.setAttribute("StatusCode", new Integer(command.getStatusCode()));

      // Wyniki - będą dostępne jako atrybut sesji
      ses.setAttribute("Results", results);
    }

    // Wywołanie serwletu prezentacji
    disp = context.getRequestDispatcher(presentationServ);
    disp.forward(req, resp);
  }
Widzimy, że opisuje on wyłącznie logikę działania, bez elementów prezentacji (drobnym, niestety nieuniknionym wyjątkiem jest ustalenie content-type odpowiedzi na "text/html"; w przeciwnym razie metoda include RequestDispatchera włączy źródło generowanej przez serwlet pobierania parametrów strony, a nie zinterpretowany HTML).
Praktycznie ten serwlet-kontroler jest na tyle niezależny od konkretów, że nadaje się do zastosowania w niemal dowolnych sytuacjach pobierania danych wejściowych, wykonania na nich jakichś działań i prezentacji ich wyników.

Serwlety pobierania parametrów i  prezentacji wyników są już  bardziej skonkretyzowane.
Zakładamy, że parametry będą pobierane z formularza, a wyniki prezentowane po tym formularzu jako lista. Będziemy jednak chcieli uniezależnić oba serwlety od liczby, nazw, opisów pobieranych parametrów oraz liczby, rodzaju i opisów wyników.
Taką sparametryzowaną informację dostarczymy poprzez ResourceBundle, który - dla danej lokalizacji (języka) zlecenia  będzie odczytywany (tylko przy zmianie sesji) przez dodatkowy serwlet. Przy okazji odczytaną informację zapiszemy w obiekcie klasy BundleInfo, przez co będziemy mieli wygodny do niej dostęp z innych serwletów.

Oczywiście, trzeba przyjąć jakąś konwencje opisu aplikacji w ResourceBundle.
Wyróżnimy następujące elementy:
Przygotowana zgodnie z tą konwencją dla lokalizacji polskiej klasa zasobowa wygląda następująco:

public class RegexParamsDef_pl extends ListResourceBundle {
     public Object[][] getContents() {
         return contents;
     }

    static final Object[][] contents = {
       { "charset", "ISO-8859-2" },
       { "header", new String[] { "Testowanie wyrażeń regularnych" } },
       { "param_regex", "Wzorzec:" },
       { "param_input", "Tekst:" },
       { "submit", "Pokaż wyniki wyszukiwania" },
       { "footer", new String[] { } },
       { "resCode", new String[]
                    { "Dopasowano", "Brak danych",
                      "Wadliwy wzorzec", "Nie znaleziono dopasowania" }
                    },
       { "resDescr",
            new String[] { "podłańcuch", "od poz.", "do poz.", "" } },
    };
}

a serwlet czytający ResourceBundle i gromadzący informację w klasie pomocniczej BundleInfo przedstawia poniższy fragment kodu:

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;


class BundleInfo {

  static private String[] commandParamNames;
  static private String[] commandParamDescr;
  static private String[] statusMsg;
  static private String[] headers;
  static private String[] footers;
  static private String[] resultDescr;
  static private String charset;
  static private String submitMsg;

  static void generateInfo(ResourceBundle rb) {

    synchronized (BundleInfo.class) {  // konieczne ze względu
                                       // na możliwość odwołań
      List cpn = new ArrayList();      // z wielu egzemplarzy serwletów
      List cpv = new ArrayList();
      Enumeration keys = rb.getKeys();
      while (keys.hasMoreElements()) {
        String key = (String) keys.nextElement();
        if (key.startsWith("param_")) {
          cpn.add(key.substring(6));
          cpv.add(rb.getString(key));
        }
        else if (key.equals("header")) headers = rb.getStringArray(key);
        else if (key.equals("footer")) footers = rb.getStringArray(key);
        else if (key.equals("resCode")) statusMsg = rb.getStringArray(key);
        else if (key.equals("resDescr")) resultDescr = rb.getStringArray(key);
        else if (key.equals("charset")) charset = rb.getString(key);
        else if (key.equals("submit")) submitMsg = rb.getString(key);
      }
      commandParamNames = (String[]) cpn.toArray(new String[0]);
      commandParamDescr = (String[]) cpv.toArray(new String[0]);
    }
  }

  public static String getCharset() {
    return charset;
  }

  public static String getSubmitMsg() {
    return submitMsg;
  }

    public static String[] getCommandParamNames() {
    return commandParamNames;
  }

  public static String[] getCommandParamDescr() {
    return commandParamDescr;
  }

  public static String[] getStatusMsg() {
    return statusMsg;
  }

  public static String[] getHeaders() {
    return headers;
  }

  public static String[] getFooters() {
    return footers;
  }

  public static String[] getResultDescr() {
    return resultDescr;
  }

}


// Serwlet włączany wyłącznie z serwletu pobierania parametrów
// Ładuje  ResourceBundle i przekazuje go klasie BundleInfo,
// która odczytuje info i daje wygodną formę jej pobierania
// w innych serwletach.
// Ładowanie zasobów i ich przygotowanie przez klasę BundleInfo
// następuje tylko raz na sesję.


public class ResourceBundleServ extends HttpServlet {

  private String resBundleName;

  public void init() {
    resBundleName = getServletContext().getInitParameter("resBundleName");
  }

  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    HttpSession ses = req.getSession();
    ResourceBundle paramsRes = (ResourceBundle) ses.getAttribute("resBundle");

    // W tej sesji jeszcze nie odczytaliśmy zasobów
    if (paramsRes == null) {
       Locale loc = req.getLocale();
       paramsRes = ResourceBundle.getBundle(resBundleName, loc);
       ses.setAttribute("resBundle", paramsRes);

       // Przygotowanie zasobów w wygodnej do odczytu formie
       BundleInfo.generateInfo(paramsRes);
    }

    // ... a jeśli sesja się nie zmieniła - to nie mamy nic do roboty
  }
//...
}

Serwlet ten zostanie uruchomiony na wstępie serwletu pobierania parametrów. Przygotowana informacja posłuży do wygenerowania strony z formularzem.

// SERWLET POBIERANIA PARAMETRÓW

public class GetParamsServ extends HttpServlet {

  private ServletContext context;
  private String resBundleServ;    // nazwa serwletu przygotowującego
                                   // sparametryzowaną informacje


  // Inicjacja
  public void init() {
    context = getServletContext();
    resBundleServ = context.getInitParameter("resBundleServ");
  }

  // Obsługa zleceń
  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {


    // Włączenie serwletu przygotowującego informacje z z zasobów
    // (ResourceBundle). Informacja będzie dostępna poprzez
    // statyczne metody klasy BundleInfo

    RequestDispatcher disp = context.getRequestDispatcher(resBundleServ);
    disp.include(req, resp);

    // Pobranie potrzebnej informacji
    // ktora została wczesniej przygotowana
    // przez klasę BundleInfo na podstawie zlokalizowanych zasobów

    // Zlokalizowana strona kodowa
    String charset = BundleInfo.getCharset();

    // Napisy nagłówkowe
    String[] headers = BundleInfo.getHeaders();

    // Nazwy parametrów (pojawią się w formularzu,
    // ale również są to nazwy parametrów dla Command)
    String[] pnames = BundleInfo.getCommandParamNames();

    // Opisy parametrów - aby było wiadomo co w formularzu wpisywać
    String[] pdes   = BundleInfo.getCommandParamDescr();

    // Napis na przycisku
    String submitMsg = BundleInfo.getSubmitMsg();

    // Ew. końcowe napisy na stronie
    String[] footers = BundleInfo.getFooters();

    // Ustalenie właściwego kodowania zlecenia
    // - bez tego nie będzie można własciwie odczytać parametrów
    req.setCharacterEncoding(charset);

    // Pobranie aktualnej sesji
    // w jej atrybutach są/będą przechowywane
    // wartości parametrów

    HttpSession session = req.getSession();

    // Generowanie strony

    resp.setCharacterEncoding(charset);
    PrintWriter out = resp.getWriter();

    out.println("<center><h2>");
    for (int i=0; i<headers.length; i++)
       out.println(headers[i]);
    out.println("</center></h2><hr>");

       // formularz
    out.println("<form method=\"post\">");
    for (int i=0; i<pnames.length; i++) {
     out.println(pdes[i] + "<br>");
     out.print("<input type=\"text\" size=\"30\" name=\"" +
                   pnames[i] +  "\"");

       // Jezeli są już wartości parametrów - pokażemy je w formularzu
      String pval = (String) session.getAttribute("param_"+pnames[i]);
      if (pval != null) out.print(" value=\"" + pval + "\"");
      out.println("><br>");
    }
    out.println("<br><input type=\"submit\" value=\"" + submitMsg + "\">");
    out.println("</form>");

    // Pobieranie parametrów z formularza

    for (int i=0; i<pnames.length; i++) {
      String paramVal = req.getParameter(pnames[i]);
         // Jeżeli brak parametru (ów) - konczymy
      if (paramVal == null) return;

      // Jest parametr - zapiszmy jego wartość jako atrybut sesji.
      // Zostanie on pobrany przez Controller
      // który ustali te wartości dla wykonania Command

      session.setAttribute("param_" + pnames[i], paramVal);

    }
  }

  //..metody doGet i doPost - wywołują serviceRequest
}

Serwlet przentacji najpierw przekazuje zadanie wygenerowania formularza serwletowi pobierania parametrów, po czym prezentuje wyniki zapisane w atrybutach sesji przez serwer-kontroler.
public class ResultPresent extends HttpServlet {


  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    ServletContext context = getServletContext();

    // Włączenie strony generowanej przez serwlet pobierania parametrów
    // (formularz)
    String getParamsServ = context.getInitParameter("getParamsServ");
    RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
    disp.include(req,resp);

    // Uzyskanie wyników i wyprowadzenie ich
    // Controller po wykonaniu Command zapisał w atrybutach sesji
    // - referencje do listy wyników jako atrybut "Results"
    // - wartośc kodu wyniku wykonania jako atrybut "StatusCode"

    HttpSession ses = req.getSession();
    List results = (List) ses.getAttribute("Results");
    Integer code = (Integer) ses.getAttribute("StatusCode");

    PrintWriter out = resp.getWriter();
    out.println("<hr>");

    // Uzyskanie napisu właściwego dla danego "statusCode"
    String msg = BundleInfo.getStatusMsg()[code.intValue()];
    out.println("<h2>" + msg + "</h2>");

    // Elementy danych wyjściowych (wyników) mogą być
    // poprzedzane jakimiś opisami (zdefiniowanymi w ResourceBundle)
    String[] dopiski = BundleInfo.getResultDescr();

    // Generujemy raport z wyników
    out.println("<ul>");
    for (Iterator iter = results.iterator(); iter.hasNext(); ) {
      out.println("<li>");

      int dlen = dopiski.length;  // długość tablicy dopisków
      Object res = iter.next();
      if (res.getClass().isArray()) {  // jezeli element wyniku jest tablicą
        Object[] res1 = (Object[]) res;
        int i;
        for (i=0; i < res1.length; i++) {
          String dopisek = (i < dlen ? dopiski[i] + " " : "");
          out.print(dopisek + res1[i] + " ");
        }
        if (dlen > res1.length) out.println(dopiski[i]);
      }
      else {                                      // może nie być tablicą
        if (dlen > 0) out.print(dopiski[0] + " ");
        out.print(res);
        if (dlen > 1) out.println(" " + dopiski[1]);
      }
      out.println("</li>");
    }
    out.println("</ul>");
  }

//..metody doGet i doPost - wywołują serviceRequest
}
Uruchamiając tę aplikację uzyskamy znany nam już obraz  dzialania (stronę z formularzem , która po wpisaniu regularnego wyrażenia i przeszukiwanego tekstu zostanie uzupełniona o listę komunikatów o odnalezionych dopasowaniach).
r 

Oczywiście, cała aplikacja jest teraz dość rozbudowana, a nawet skomplikowana. Jednak dzięki  dodatkowemu wysiłkowi włożonemu w separacje logiki działania, prezentacji oraz elementów opisowych zmiany jej funkcjonalności są obecnie bardzo łatwe, Wyniki możemy np. prezentować w tabeli (co wymaga tylko niewielkich modyfikacji kodu serwletu prezentacji, inne komponenty nie ulegają zmianom). Możemy inaczej pobierać parametry (co wymaga zmian tylko w serwlecie GetParamServ). Nawet całkowita zmiana wykonywanego przez aplikację zadania jest niezwykle prosta i nie zabierze więcej niż kilka minut. Zobaczmy to na przykładzie zadania wykonywania kilku operacji na wprowadzanych tekstach (powiedzmy połączenie dwóch tekstów i wykonaniu wybranej operacji - zmiany wielkości liter lub wyodrębnienia słów).
Kody wszystkich serwletów pozostają bez zmian. Musimy jedynie dostarczyć nowej implementacji interfejsu Command  (klasę nazwiemy StringComamnd i podamy te nazwę w parametrze kontekstu) oraz pliku zasobowego z opisem aplikacji (dla lokalizacji polskiej - StringCmdDef_pl).

import java.util.*;

public class StringCmdDef_pl extends ListResourceBundle {
     public Object[][] getContents() {
         return contents;
     }

    static final Object[][] contents = {
       { "charset", "ISO-8859-2" },
       { "header", new String[] { "Działania na Stringach" } },
       { "param_input1", "Tekst 1:" },
       { "param_input2", "Tekst 2:" },
       { "param_cmd", "Polecenie:" },
       { "submit", "Wykonaj" },
       { "footer", new String[] { } },
       { "resCode", new String[]
                    { "Wynik:", "Brak danych",
                      "Wadliwe polecenie, dostępne: upper, lower, words" }
                    },
       { "resDescr",
            new String[] { "" } },
    };
}

import java.io.*;
import java.util.*;

public class StringCommand extends CommandImpl implements Serializable {

  public StringCommand() {}

  public void execute() {
    clearResult();
    String input1 = (String) getParameter("input1");
    String input2 = (String) getParameter("input2");
    String cmd =   (String) getParameter("cmd");
    if (input1 == null || input2 == null || cmd == null) {
      setStatusCode(1);
      return;
    }


   String input = input1 + " " + input2;


   setStatusCode(0);
   if (cmd.equals("upper")) addResult(input.toUpperCase());
   else if (cmd.equals("lower")) addResult(input.toLowerCase());
   else if (cmd.equals("words")) {
     StringTokenizer st = new StringTokenizer(input);
     while (st.hasMoreTokens())  addResult(st.nextToken());
   }
   else setStatusCode(2);
 }

}

Nasza aplikacje bedzie teraz działać tak:

r 


5. Podsumowanie


Zapoznaliśmy się z wzorcami:

Dzięki dostarczanym przez nie możliwościom separacji i uniezależniania kodu są one niezwykle istotne dla budowy systemów elastycznych i łatwo modyfikowalnych.


6. Zadania


Zad. 1

Stworzyć modyfikację  wzorca Command, która zakłada, ze metoda execute(...) może mieć dowolną liczbę parametrów i zwraca wynik (użyć środków Javy 5).
Zastosować ten wzorzec w wymyślonej przykładowej aplikacji.

Zad. 2

Zmodyfikowac kody klas w aplikacji z licznikiem, tak by stały się mniej od siebie zależne.


Zad. 3

Dostarczyć wizualizacji zakupow w zadaniu z internetową kwiaciarnią (z wykładu 5), korzystając z wzorca MVC.