<

6. Wzorce projektowe (2)


Teraz zajmiemy się wzorcami Observer, Visitor i Composite. Są one nie tylko bardzo ważne, ale i niezwykle ciekawe. Wzorzec Observer dobrze znamy z delegacyjnego modelu obsługi zdarzeń, natomaist Visitor, sczególnie w połączeniu z Composite, może stanowić istotne uzupełnienie umiejętności programistycznych.



1. Observer

Observer definiuje zależności pomiędzy obiektami w taki sposób, że gdy obiekty "obserwowane" zmieniają stan, to obiekty "obserwatorzy" są o tym powiadamiani


Diagram GoF dla tego wzorca wygląda następująco.

 


Dzięki temu możemy tworzyć uniwersalne, elastyczne i rozszerzalne aplikacje, w których łatwo realizowana jest komunikacja pomiędzy niepowiązanymi obiektami.

W Javie istnieją interfejs Observer oraz klasa Observable.
Są one raczej mało użyteczne, bo obserwowani muszą dziedziczyć Observable, co nie jest wygodne.
Lepszym rozwiazaniem jest oparcie wzorca na obsłudze zdarzeń.
Możemy tu wykorzystać różne interfejsy nasłucgu zdarzeń, ale szczególnie użyteczne (bo uniwersalne) są ChangeListener i PropertyChangeListener.

Rozważmy przykład narzędziowej aplikacji, służącej do nadawania różnych atrybutów wybranym rodzajom tekstu w komponentach tekstowych.

Działanie narzędzie ilustruje następująca prezentacja.

W programie wykorzystano m.in. prosty model selekcji kolorów.

package colorgoodies;
import java.awt.*;
import javax.swing.colorchooser.*;
import javax.swing.event.*;
import java.io.*;

public class SwatchColorSelectionModel implements ColorSelectionModel,
                                                  Serializable
{
  protected transient ChangeEvent changeEvent = null;
  protected EventListenerList listenerList = new EventListenerList();
  private Color[] colors;
  private Color selectedColor;
  private int selectedIndex = -1;

  public SwatchColorSelectionModel(Color[] c) {
    colors = c;
    selectedColor = null;
  }

  public SwatchColorSelectionModel(Color[] col, Color c) {
    this(col);
    for (int i=0; i<colors.length; i++)
      if (c.equals(colors[i])) {
        selectedIndex = i;
        selectedColor = c;
        break;
      }
  }

  public SwatchColorSelectionModel(Color[] col, int i) {
    this(col);
    try {
      selectedColor = colors[i];
      selectedIndex = i;
    } catch (ArrayIndexOutOfBoundsException exc) { }
  }

  public int getSelectedIndex() {
    return selectedIndex;
  }

  public void setSelectedIndex(int value) {
    int old = selectedIndex;
    if (value != old) {
      if (value == -1) {
        selectedIndex = -1;
        setSelectedColor(null);
      }
      else
        try {
          selectedIndex = value;
          setSelectedColor(colors[value]);
        } catch (ArrayIndexOutOfBoundsException exc) {
            selectedIndex = old;
        }
    }
  }

  public void setSelectedColor(Color color) {
    if (color == null) {
      selectedColor = null;
      selectedIndex = -1;
    }
    else if (selectedColor == null || !selectedColor.equals(color)) {
      selectedColor = color;
    }
    fireStateChanged();
  }

  public Color getSelectedColor() {
    return selectedColor;
  }

  public void addChangeListener(ChangeListener l) {
     listenerList.add(ChangeListener.class, l);
  }

  public void removeChangeListener(ChangeListener l) {
    listenerList.remove(ChangeListener.class, l);
  }

  public ChangeListener[] getChangeListeners() {
    return (ChangeListener[])listenerList.getListeners(
            ChangeListener.class);
  }

  protected void fireStateChanged()  {
ChangeListener[] listeners = getChangeListeners();
for (int i=0; i<listeners.length; i++) {
if (changeEvent == null) {
changeEvent = new ChangeEvent(this);
}
listeners[i].stateChanged(changeEvent);
}
}
}
O zmianie wybranego koloru model powiadamia zainteresowanych słuchaczy za pomocą generacji zdarzenia changeEvent i rozesłania go po przyłączonych słuchaczach (metoda fireStateChanged()).

Czyli zmiany w modelu są obserwowane. Kto je obserwuje?

Po pierwsze, swatch-panel (po to by zaznaczać wizualnie wybrany kolor).

r

Kliknięcie w swatch ustawia kolor w modelu - model powiadamia o zmianie stanu obserwatorów. Tu jednym z obserwatorów jest modelListener  w klasie SwatchPanel (ten ChangeListener reaguje na zmianę poprzez odświeżenie rysunku - zaznaczenie kółkiem wybranego koloru).

package colorgoodies;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.colorchooser.*;

public class SwatchPanel extends JPanel {

    private Color[] colors;
    private int rows;
    private int columns;
    private Dimension swatchSize;
    private Dimension gap;
    private boolean tipsEnabled;
    private SwatchColorSelectionModel model;
    private int mouseX;
    private int mouseY;


    private ChangeListener modelListener = new ChangeListener() {
        public void stateChanged(ChangeEvent e) {
          repaint();
        }
    };

    public ChangeListener getRepaintListener() {
      return modelListener;
    }

    public SwatchPanel(Color[] c, int r, Dimension s, Dimension g) {
      this(c,r,s,g,true);
    }

    public SwatchPanel(Color[] c, int r, Dimension s, Dimension g,
                       boolean t) {
      init(c, r, s, g, t);
      setBackground(Color.white);
      addMouseListener(colorListener);

    }

    public void init(Color[] c, int r, Dimension s, Dimension g,
                       boolean t) {
      colors = c;
      rows = r;
      swatchSize = s;
      gap = g;
      tipsEnabled = t;
      columns = colors.length / rows +
                (colors.length%rows > 0 ? 1 : 0);
      setTipsEnabled(tipsEnabled);
   }

    public void setModel(SwatchColorSelectionModel m) {
int oldSelInd = -1;
if (model != null) {
oldSelInd = model.getSelectedIndex();
model.removeChangeListener(modelListener);
}
model = m;
int newSelInd = m.getSelectedIndex();
model.addChangeListener(modelListener); if (oldSelInd != newSelInd) modelListener.stateChanged(new ChangeEvent(model)); }
public SwatchColorSelectionModel getModel() { return model; } public void paintComponent(Graphics g) { g.setColor(getBackground()); g.fillRect(0,0,getWidth(), getHeight()); for (int row = 0; row < rows; row++) { for (int column = 0; column < columns; column++) { Color c = getColorForCell(column, row); if (c == null) return; g.setColor(c); int x = column * (swatchSize.width + gap.width); int y = row * (swatchSize.height + gap.height); int selectedInd = model.getSelectedIndex(); if (selectedInd != -1 && selectedInd == getIndexForLocation(x,y)) { g.fillOval( x, y, swatchSize.width, swatchSize.height); g.setColor(Color.black); g.drawOval( x, y, swatchSize.width-1, swatchSize.height-1); } else { g.fillRect( x, y, swatchSize.width, swatchSize.height); g.setColor(Color.black); g.drawRect( x, y, swatchSize.width-1, swatchSize.height-1); } } } } public Dimension getPreferredSize() { int x = columns * (swatchSize.width + gap.width) -1; int y = rows * (swatchSize.height + gap.height) -1; return new Dimension( x, y ); } public Dimension getMinimumSize() { return getPreferredSize(); } public Dimension getMaximumSize() { return getPreferredSize(); } public String getToolTipText(MouseEvent e) { if (!tipsEnabled) return null; Color color = getColorForLocation(e.getX(), e.getY()); if (color == null) return null; return color.getRed()+" "+ color.getGreen() + " " + color.getBlue(); } public Color getColorForLocation( int x, int y ) { int column = x / (swatchSize.width + gap.width); int row = y / (swatchSize.height + gap.height); return getColorForCell(column, row); } private Color getColorForCell( int column, int row) { int ind = (row * columns) + column; if (ind < colors.length) return colors[ind]; else return null; } public int getIndexForLocation( int x, int y) { int column = x / (swatchSize.width + gap.width); int row = y / (swatchSize.height + gap.height); int ind = (row * columns) + column; if (ind < colors.length) return ind; else return -1; } public void setSwatchSize(Dimension d) { swatchSize = d; } public Dimension getSwatchSize() { return swatchSize; } public void setRows(int n) { rows = n; columns = colors.length / rows + (colors.length%rows > 0 ? 1 : 0); } public int getRows() { return rows; } public void setGapSize(Dimension d) { gap = d; } public Dimension getGapSize() { return gap; } public void setTipsEnabled(boolean te) { tipsEnabled = te; String tip = (te ? "" : null); setToolTipText(tip); } public boolean isTipsEnabled() { return tipsEnabled; } static int jccind = -1; private ActionListener HSBlistener = new ActionListener() { public void actionPerformed(ActionEvent e) { if (jccind == -1) return; if (e.getActionCommand().equals("OK") ) { colors[jccind] = jcc.getColor(); repaint(); } jccind = -1; } }; private MouseListener colorListener = new MouseAdapter() { public void mousePressed(MouseEvent e) { if (e.isMetaDown()) return; // opcja z menu kontekstowego - obsługa nie tu int ind = getIndexForLocation(e.getX(), e.getY()); if (ind == -1) return; if (e.isControlDown()) { // Zmiana swatcha jccind = ind; JDialog d = JColorChooser.createDialog(SwatchPanel.this, "Choose color", true,jcc, HSBlistener, HSBlistener); d.show(); } else { mouseX = e.getX(); mouseY = e.getY(); model.setSelectedIndex( ind ); // Zmiana w modelu
}
}
};

public int getMouseX() { return mouseX; }
public int getMouseY() { return mouseY; }


private static JColorChooser jcc;
static {
jcc = new JColorChooser();
AbstractColorChooserPanel[] p = jcc.getChooserPanels();
jcc.removeChooserPanel(p[0]);
jcc.removeChooserPanel(p[2]);
}



}
Wydaje się, że pojawił się  tu nadmiar kounikacyjny:

mousePressed (na SwatchPanelu) zamiast zrobić od razu coś wizualnego powiadamia model kolorów, a ten dopiero powiadamia z powrotem SwatchPanel (przez modelListener), że coś ma zmienić w swoim wyglądzie.

Ale tylko model selekcji kolorów może zdecydować, czy rzeczywiście kolor został wybrany (wybór już ustalonego koloru nie jest selekcją i nie generuje sekwencji zdarzeń).
Selekcja koloru musi być też w jednym miejscu (czyli w modelu selekcji) bo są też inni obserwatorzy - reagujący na wybór koloru poprzez zmianę atrybutów komponentu tekstowego  Robimy to w innej klasie odpoweidzialnej za zmiany atrybutu kompoenntu tekstowego.

 private ChangeListener colorListener =  new ChangeListener() {
    public void stateChanged(ChangeEvent e) {
      if (e.getSource() != cbp.getModel()) return;
      Color c = cbp.getModel().getSelectedColor();
      if (c == null) return;
      String[] types = view.getTypes();
      if (!(view.getTextComponent() instanceof JTextPane) ||
         attrsToTypes.get(attrs[view.getCurrentMode()]) == null)
        view.set(attrs[view.getCurrentMode()], c);
      else
        view.set(types[view.getCurrentType()], attrs[view.getCurrentMode()], c);
    }
  };
Tutaj:
A w jaki sposób colorListener (jako observer) jest przyłączany do modelu selekcji kolorow?

Otóż dla każdego widoku komponentu tekstowego mamy dwuwymiarową tablicę modeli kolorów. Jeden model opisuje wybrany kolor dla danego typu tekstu i danego trybu działania (zmiana dotycząca atrybutów foreground, background etc). Te modele tworzymy raz, w trakcie opakowywania komponentu tekstowego "w widok" i następnie będziemy ich ponownie używać (wzorzez Flyweight). Jest to potrzebne, aby pamiętać ustawione kolory dla każdej z sytuacji i wrócić do nich przy zmianach stanów.

Zmiana trybu działania  i/lub typu tekstu jest zmianą właściwości opakowanego komponentu i jest zgłaszana poprzez generację PropertyChangeEvent.
W klasie odpowiedzialnej za zmiany atrybytó tekstu mamy słuchacza zmian właściwości (kolejne zastosowanie wzorca Observer) który ustala odpowiedni model oraz przyłącza do niego ChangeListenera.
  private PropertyChangeListener viewPropsChanged =
    new PropertyChangeListener() {
      public void propertyChange(PropertyChangeEvent e) {
        String propName = e.getPropertyName();
        // ...
        int newVal = ((Integer)e.getNewValue()).intValue();
        int cmode = view.getCurrentMode();
        int ctype = view.getCurrentType();
        boolean ta = attrsToTypes.get(attrs[cmode]) != null;
        SwatchColorSelectionModel newModel = null;
        if (propName.equals("Type"))
          newModel = csm[cmode][ ta ? newVal : 0];
        else if (propName.equals("Mode"))
          newModel = csm[newVal][ta ? ctype : 0];
        else return;
        cbp.getModel().removeChangeListener(colorListener);
        cbp.setModel(newModel);
        newModel.addChangeListener(colorListener);
      }
  };


Właśnie szczególnie użycie nasłuchu zmian właściwości pozwala na unikanie wiązania obiektów, klas w dużych projektach (unikamy niepotrzebnych zależności)  oraz umożliwia asynchroniczne, działające na zasadzie callback, łatwe programowanie. Jest to szczególnie cenne w aplikacjach współbieżnych - komunikacja pomiędzy wątkami poprzez kolejkę zdarzeń jest łatwa i pozwala w wielu przypadkach unikać kosztownej synchronizacji.

Przykład:

Klasa LCompiler działa jako wątek kompilacji kodu.
Rozpoczęcie i zakończenie kompilacji jest sygnalizowane jako zmiana stanu kompilatora.
Tej zmiany nasłuchuje środowisko uruchomieniowe, które odpowiednio aktywuje/deaktywuje jakieś opcje, a także pokazuje informacje o wynikach kompilacji. IDE jest cały czas reaktywne, kompilacja toczy się w tle, jak tylko jest gotowa - IDE odzwierciedla ten fakt.

public class LCompiler {


  private ArrayList errorList;
  private PropertyChangeSupport change = new PropertyChangeSupport(this);
  // obiekt-kompilator
  private com.sun.tools.javac.Main compiler = new com.sun.tools.javac.Main();
  // ....
  private static final LCompiler comp = new LCompiler();

  public static LCompiler getCompiler() { return comp; }


  public static final int STOPPED = 0, RUNNING = 1;
  private int compilerState = STOPPED;

  private int returnCode = -1;

  public int getReturnCode() {
    return returnCode;
  }

  public String getErrorInfo() {
    return errorInfo;
  }


  private LCompiler() {  }

  public void compile(String[] args) {

    StringWriter sout = new StringWriter();
    PrintWriter pout = new PrintWriter(sout);
    change.firePropertyChange("compilerState", STOPPED, RUNNING);
    returnCode = compiler.compile(args, pout);
    errorInfo = "";
    if (returnCode != 0) {
      errorList = new ArrayList();
      SrcError err = null;
      try {
        BufferedReader br = new BufferedReader(
                                new StringReader(sout.toString())
                            );
        String line;
        while ((line = br.readLine()) != null) {
          String tmp =  line.trim();
          if (tmp.equals("")) continue;
          matcher.reset(line);
          if (matcher.matches()) {
            err =  new SrcError (Integer.parseInt(matcher.group(2)), matcher.group(3));
            errorList.add(err);
          }
          else if (line.startsWith("Note")) errorList.add(new SrcError(-1, line));
          else if (Character.isDigit(line.charAt(0))) errorInfo += line + " ";
          else {
            if (err == null) errorList.add( new SrcError(-1, line));
            else err.addDesc(line);
          }
        }
        br.close();
      } catch (Exception exc) { exc.printStackTrace(); }
    }

    change.firePropertyChange("compilerState", RUNNING, STOPPED);
}

// .... metody dodawania/usuwania słuchaczy zmian właściwości

Obserwatorem będzie tu głównie klasa ErrList, która prezentuje w IDE wyniki kompilacji:

class JLErrs extends JList {
  // ....
}

public class ErrList extends JPanel {
  //...........
  JLErrs errs;
  JLabel info = new JLabel("     ");


  public ErrList(View v) {
    super(new BorderLayout());
    view = v;
    errs = new JLErrors(v);
    info.setOpaque(true);
    info.setBackground(Color.white);
    LCompiler compiler = LCompiler.getCompiler();
    compiler.addPropertyChangeListener(this);
    add(info, "North");
    add (new JScrollPane(errs));
    Dimension d = new Dimension(300, 100);
    setPreferredSize(d);
    setMaximumSize(d);
    setMinimumSize(d);
    errs.addListSelectionListener( new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        selectError(e);
      }
    });
  }

  // .....

  public void propertyChange(PropertyChangeEvent e) {
final LCompiler compiler = LCompiler.getCompiler(); if (!e.getPropertyName().equals("compilerState")) return; DockableWindowManager wm = view.getDockableWindowManager(); wm.showDockableWindow("ErrList"); int old = ( (Integer) e.getOldValue()).intValue(); int newv = ( (Integer) e.getNewValue()).intValue(); if (newv == LCompiler.RUNNING) { SwingUtilities.invokeLater( new Runnable() { public void run() { ListModel lm = errs.getModel(); if (lm instanceof ElistModel) ((ElistModel) lm).clear(); info.setText("Compiling..."); } }); } else if (newv == LCompiler.STOPPED) { final int rc = compiler.getReturnCode(); SwingUtilities.invokeLater( new Runnable() { public void run() { if (rc == 0) { info.setText("Compilation finished. Success"); } else info.setText("Compilation finished. " +compiler.getErrorInfo()); } }); if (rc == 0) { if (JLECompiler.isRealJava) new JavaRunner(); else new Runner().start(); } final java.util.List elist = compiler.getErrorList(); if (elist == null) return; SwingUtilities.invokeLater( new Runnable() { public void run() { errs.setModel(new ElistModel(elist)); } }); } }


2. Visitor

Visitor definiuje operacje, które są wykonywane na (strukturach lub zestawach) obiektów innych klas. Odseparowanie tych operacji od klas "odwiedzanych" obiektów pozwala wprowadzać nowe operacje bez konieczności dokonywania zmian w klasach obiektów.


Rozważmy przykład.

Mamy pacjentów, chorych na różne choroby. Możemy zapisać operację "leczenia" w klasach pacjentów:

abstract class Pacjent {
  protected String name;
  Pacjent(String s) {
    name = s;
  }

  // *** funkcjonalność zapisana w klasie
  public abstract String lecz();
}

class ChoryNaGłowę extends Pacjent {
  private static final String opis = "Chory na głowę";
  ChoryNaGłowę(String s) { super(s); }
  public String toString() { return name + " " + opis; }

  // ---- funkcjonalność zapisana w klasie

  public String lecz() {
    return "Stosuję aspirynę";
  }

}

class ChoryNaŻołądek extends Pacjent {
  private static final String opis = "Chory na żołądek";
  ChoryNaŻołądek(String s) { super(s); }
  public String toString() { return name + " " + opis; }

  public String lecz() {
    return "Stosuję węgiel";
  }

}

class ChoryNaNogę extends Pacjent {
  private static final String opis = "Chory na nogę";
  ChoryNaNogę(String s) { super(s); }
  public String toString() { return name + " " + opis; }

  public String lecz() {
    return "Zakładam gips";
  }

}
Ale:
Gdybyśmy stworzyli klasę Leczenie i zdefiniowali w niej metodę lecz(Pacjent p), to musielibyśmy w tej metodzie zapisać rozgałęzione if-else, rosnące przy pojawianiu się coraz to nowych "rodzajów" pacjentów.

Lepiej jest (i należy) zdefiniować metody:

lecz(ChoryNaGłowę)
lecz(ChoryNaŻołądek)
lecz(ChoryNaNogę)

ale wtedy mielibyśmy kłopot z ich wywołaniem:

List<Pacjent> listaPacjentów = // tu chorzy na głowe i nogę i brzuch....
Leczenie leczenie = new Leczenie();
for (Pacjent p :  listaPacjentów)  leczenie.lecz(p);  // błąd w kompilacji

Na czym polega problem?

Wywołanie leczenie.lecz(p) jest polimorficzne względem zmiennej leczenie (klasa Leczenie może - i powinna - implementować jakiś ogólniejszy interfejs operacji), ale nie jest polimorficzne względem zmiennej p (tak to już jest w javie i C++ i C# - tylko pojedyńczy polimorfizm).
A my potrzebujemy tu "podwójnego polimorfizmu" - względem operacji i względem pacjenta.


Na pomoc przychodzi wzorzec Visitor:
Taki sposób wywołania, umożliwiający wykorzystanie polimorfizmu "po dwóch argumentach", nazywa się double-dispatching. Można go rozwinąć ogólniej w tzw multiple-dispatching.


Diagram Gof dla wzorca Visitor podano poniżej.


 


Zobaczmy to na przykładzie z pacjentami. Funkcje "leczenia" pacjentów definiujemy zewnętrznie jako Visitor (klasę implementującą interfejs Visitor).

interface Visitor {
  public void visit(ChoryNaGłowę p);
  public void visit(ChoryNaŻołądek p);
  public void visit(ChoryNaNogę p);
}


class Leczenie2 implements Visitor {

  public void visit(ChoryNaGłowę p) {
    System.out.println(p);
    System.out.println("Stosuję aspirynę");
  }

  public void visit(ChoryNaŻołądek p) {
    System.out.println(p);
    System.out.println("Stosuję węgiel");
  }

  public void visit(ChoryNaNogę p) {
    System.out.println(p);
    System.out.println("Zakładam gips");
  }
}

W klasach pacjentów musimy dodać metody akceptacji - wywołujące metodę visit przekazanego wizytora.
abstract class Pacjent {
  protected String name;
  Pacjent(String s) {
    name = s;
  }

  public abstract void accept(Visitor v);
}


class ChoryNaGłowę extends Pacjent {
  private static final String opis = "Chory na głowę";
  ChoryNaGłowę(String s) { super(s); }
  public String toString() { return name + " " + opis; }

  public void accept(Visitor v) {
v.visit(this);
}
} class ChoryNaŻołądek extends Pacjent { private static final String opis = "Chory na żołądek"; ChoryNaŻołądek(String s) { super(s); } public String toString() { return name + " " + opis; } public void accept(Visitor v) {
v.visit(this);
}
} class ChoryNaNogę extends Pacjent { private static final String opis = "Chory na nogę"; ChoryNaNogę(String s) { super(s); } public String toString() { return name + " " + opis; } public void accept(Visitor v) {
v.visit(this);
}
}
No i teraz zastosujemy double-dispatching w działaniu:

 public static void main(String[] args) {
    List<Pacjent> lista = new ArrayList<Pacjent>();
    lista.add(new ChoryNaGłowę("Jan Kowalski"));
    lista.add(new ChoryNaGłowę("Stefan Kowalewski"));
    lista.add(new ChoryNaNogę("Janusz Malinowski"));
    lista.add(new ChoryNaŻołądek("Adam Mickiewicz"));

    // Leczenie
    Visitor v = new Leczenie2();
    for(Pacjent p : lista) p.accept(v);
}


Dzięki temu wzorcowi w łatwy sposób możemy dodać nowe rodzaje operacji na obiektach (nie musząc przy tym nic zmieniać w klasach odwiedzanych obiektów).

Np. operację wypisywania ze szpitala
// dodajemy nową funkcjonalność (zewnętrznie!)
// w klasach pacjentów nic nie musimy zmieniać

class Wypis implements Visitor {
  public void visit(ChoryNaGłowę p) {
    System.out.println(p);
    System.out.println("Do domu!");
  }
  public void visit(ChoryNaŻołądek p) {
    System.out.println(p);
    System.out.println("Do domu, ale stosować dietę");
  }
  public void visit(ChoryNaNogę p) {
    System.out.println(p);
    System.out.println("Należy przewieźć do domu");
  }
}
 No i teraz możemy pacjentów nie tylko leczyć ale i wypisywać do domu.

    List<Pacjent> lista = new ArrayList<Pacjent>();
    // ...
    // Leczenie
    Visitor leczenie = new Leczenie2();
    for(Pacjent p : lista) p.accept(leczenie);

    // Wypisy
    Visitor wypis = new Wypis();
    for(Pacjent p : lista) p.accept(wypis);


Bardzo wyraźnie "double-dispatching" i podwójnie polimorficzną naturę wizytora można  dostrzec, jeśli zdefiniować metodę wykonywania róznych operacji (tutaj założymy dodatkowo, że w klasach wizytorów są zdefiniowaen metody toString() opisując rodzaj wykonywanej operacji).

static void wykonajOperację(Visitor v, List<Pacjent> lista) {
    System.out.println("Wykonuję operację - " +  v);
    for(Pacjent p : lista) p.accept(v);
    // Ho, ho! efekt wwwołania p.accept(v) jest polimorficzny
    // zarówno względem p jak i względm v.
  }


i użycie:
   List<Pacjent> lista = new ArrayList<Pacjent>();
   // ....
   Visitor leczenie = new Leczenie2();
            wypis = new Wypis();

    wykonajOperację(leczenie, lista);
    wykonajOperację(wypis, lista);

co w końcu da nam wynik:


Wykonuję operację - Leczenie
Jan Kowalski Chory na głowę
Stosuję aspirynę
Stefan Kowalewski Chory na głowę
Stosuję aspirynę
Janusz Malinowski Chory na nogę
Zakładam gips
Adam Mickiewicz Chory na żołądek
Stosuję węgiel
Wykonuję operację - Wypisywanie ze szpitala
Jan Kowalski Chory na głowę
Do domu!
Stefan Kowalewski Chory na głowę
Do domu!
Janusz Malinowski Chory na nogę
Należy przewieźć do domu
Adam Mickiewicz Chory na żołądek
Do domu, ale stosować dietę


Demonstracja dzialania programu.



Wady tradycyjnego visitora:
W Javie można tych wad uniknąć poprzez zastosowanie refleksji (ale skoro refleksja - to kosztem efektywności działania). Pomysł: Jeremy Blosser. Java Tip 98: Reflect on the visitor design pattern. JavaWorld, July 2000.

import java.lang.reflect.*;
import javax.swing.*;
import java.awt.*;

interface DynamicVisitor {
  public void visit(Object o);
}

abstract class AbstractDynamicVisitor implements DynamicVisitor {
  public void visit(Object o) {
    // Klasa konkretnego wizytora
    Class visitorClass = getClass();
    // Klasa wizytowanego obiektu
    Class objectClass = o.getClass();
    // W konkretnym wizytorze szukamy metody visit dla danego obiektu
    try {
      Method visitMethod = visitorClass.getMethod("visit", objectClass);
      visitMethod.invoke(this,o);
    } catch (Exception exc) { defaultDispatch(o); }
  }

  // Jeśli dla danej klasy obiektu nie ma metody visit z takim argumentem
  public void defaultDispatch(Object o) {}
}


class PrzydziałDoOddziałów extends AbstractDynamicVisitor {
  public void visit(ChoryNaGłowę p) {
    System.out.println(p + " - na oddział psychiatrii");
  }
  public void visit(ChoryNaNogę p) {
    System.out.println(p + " - ortopedia");
  }
  public void visit(ChoryNaŻołądek p) {
    System.out.println(p + " - na oddział zakaźny");
  }
  public void defaultDispatch(Object o) {
    System.out.println(o + " - przypadek nierozpoznany");
  }
}



public class Visitor2 extends JFrame {

  public Visitor2() {
    java.util.List<Pacjent> lista = new java.util.ArrayList<Pacjent>();
    lista.add(new ChoryNaGłowę("Jan Kowalski"));
    lista.add(new ChoryNaGłowę("Stefan Kowalewski"));
    lista.add(new ChoryNaNogę("Janusz Malinowski"));
    lista.add(new ChoryNaŻołądek("Adam Mickiewicz"));

    // Uwaga! Teraz nie musimy stosować metody accept!!!
    DynamicVisitor dv = new PrzydziałDoOddziałów();
    for(Pacjent p : lista) dv.visit(p);

    // klasy nie muszą nic wiedzieć o tym, że są wizytowane
    // czyli możemy coś robić "z zewnątrz" zamkniętym, gotowym klasom

    setTitle("Kliknij Gui change - zmieni się wygląd");

    final DynamicVisitor guiChanger = new AbstractDynamicVisitor() {

      public void visit(JButton b) {
        b.setBackground(Color.yellow);
      }

      public void visit(JTextField tf) {
        tf.setBorder(BorderFactory.createLineBorder(Color.red, 3));
      }

    };

    setLayout(new FlowLayout());
    add(new JButton("Jakiś przycisk"));
    add(new JTextField(10));
    add(new JButton("Inny przycisk"));

    JButton chgGui = new JButton("Gui change");
    chgGui.addActionListener( new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        Component[] clist = getContentPane().getComponents();
        for (Component c : clist) if (c != e.getSource()) guiChanger.visit(c);

      }

    });
    add(chgGui);

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    setVisible(true);
  }

  public static void main(String[] args) {
    Visitor2 visitor2 = new Visitor2();
  }


} 
Zobacz demo programu.






3. Composite

Composite  komponuje części w całość w taki sposób, że zarówno elementy,  jak obiekty zawierająee inne elementy mogą być traktowane (pod względem wykonywanych na nich operacji) w taki sam sposób


Tak naprawsę chodzi tu o to, by móc - przynajmniej w niektórych sytuacjach  - tak samo traktować elementy terminalne, jak i te (nazwijmy je kontenerami) które zawierają inne elementy.
A do tego potrzebne jest aby w ramach wzorca Composite klasa obiektów-kontenerów implementowała ten sam interfejs co klasa komponentów terminalnych.
Zwykle uzyskuje się tu struktury drzewiaste i bardzo często implementuje się operacje w klasach kontenerów w taki sposób aby wykonywane były rekurencyjnie na składowych (częściach).

GoF prezentuje  następujący diagram dla wzorca Composite.

 


W Javie przykładem realizacji tego wzorca są kontenery (klasa Container dziedziczy klasę Component), a metody w klasie Container są prawie takie same jak na powyższym diagramie.
Również  w AWT dziedziczenie przez Menu klasy MenuItem (w Swingu: JMenu - JMenuItem) jest zastosowaniem tego wzorca, dzięki czemu możemy łatwo tworzyć wielopoziomowe menu.

Ale oczywiście można taki wzorzec implementować we własnych klasach, dodając przy tym funkcjonalność o charakterze rekursywnego wykonywania operacji.

Przykład
Operacja: show() prezentuje obiekt.
Obiekty: węzły drzewa (klasa Node) do których mogą być dodawane elementy terminalne (klasa Leaf) oraz inne węzły.

Kluczowe dla rozwiązania:
import java.util.*;

interface Component {
  void show();
}



class Leaf implements Component {

  private String id;
  private int value;

  public Leaf(String s, int v) {
    id = s;
    value = v;
  }

  public String toString() { return id + " " + value; }

  public void show() {
    System.out.println(this);
  }
}

class Node extends LinkedList<Component> implements Component {
  private String id;

  public Node(String s) { id = s; }

  public void show() {   // rekursywnie!
    System.out.println(id);
    for(Component c : this ) c.show();
  }

}


class Composite {

  public static void main(String args[]) {

    Node root = new Node("Zwierzęta");
    Node node;
    root.add(node = new Node("Psy"));
    node.add(new Leaf("Azor", 5));
    node.add(new Leaf("Aza", 7));
    root.add(node = new Node("Koty"));
    node.add(new Leaf("Pusia", 5));
    node.add(new Leaf("Mruczek", 10));

    root.show();  // jakże wygodne!
  }

}


Zwierzęta
Psy
Azor 6
Aza 8
Koty
Pusia 6
Mruczek 11



Zobacz demo programu:




Inny wariant implementacji wzorca Composite prezentuje poniższy diagram.

 


Tutaj klasę Component (terminalne obiekty) traktuje się tak jak by jej obiekty mogły zawierać inne obiekty. To nie musi być prawdą (ale może), co oczywiście stanowi problem, natomiast zaletą takiego rozwiązania jest to, że niekiedy oszczędza nam trochę kodowania.

Przykład:
klasa JComponent dziedziczy Container - zatem wszystkie lekkie komponenty Swingu mogą być traktowane jak kontenery, co czasem może ułatwić pisanie kodu. Poniżej w łatwy sposób zmieniamy kolory wszystkich komponentów w hierarchii zawierania się kompoenentów (nie sprawdzając nawet - przy wchodzeniu w kolejne poziomy hirerachii - czy mamy do czynienia z kontenerami, które trzeba dalej eksplorować czy z komponentami terminalnymi.

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class KontKomp extends JFrame {

  public KontKomp() {
    setLayout(new FlowLayout());
    JPanel p1 = new JPanel();
    p1.add(new JButton("B1"));
    p1.add(new JButton("B1"));
    JPanel p2 = new JPanel();
    p2.add(new JButton("BUTT3"));
    p2.add(new JButton("BUTT4"));
    add(p1);
    add(p2);

    JButton show = new JButton("Zmiana koloru");
    show.addActionListener( new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        setBackground((JComponent) getContentPane(), Color.yellow);
      }
    });
    add(show);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }

  public void setBackground(JComponent jc, Color color) {
    jc.setBackground(color);
    java.awt.Component[] components = jc.getComponents();
    for (java.awt.Component c : components) setBackground( (JComponent) c, color);
  }



  public static void main(String[] args) {
    KontKomp kont = new KontKomp();
    kont.pack();
    kont.setLocationRelativeTo(null);
    kont.show();
  }

Zobacz demo programu:




W implementacji wzorca Composite wg pierwszego diagramu (tak jak jest w AWT) musielibyśmy explicite sprawdzać czy komponent w hierarchii jest kontenerem czy tylko kompoenntem terminalnym i nasza metoda zmiany kolorów wyglądałaby tak:
public void setBackground(Component jc, Color color) {
    jc.setBackground(color);
    if (jc instanceof Container) {
      Component[] components = jc.getComponents();
      for (Component c : components) setBackground( (JComponent) c, color);
    }
  }

Dużą siłą wyrazu charakteryzuje się połączenie wzorców Composite i Visitor


Przykład:
import java.util.*;

interface Component {
  void accept(Visitor v);
}

interface Visitor {
  void visit(Leaf l);
  void visit(Node n);
}


class Leaf implements Component {

  private String id;
  private int value;

  public Leaf(String s, int v) {
    id = s;
    value = v;
  }

  public int getValue() { return value; }

  public void setValue(int i) { value = i; }

  public String toString() { return id + " " + value; }

  public void accept(Visitor v) {
v.visit(this);
}
} class Node extends LinkedList<Component> implements Component { private String id; public Node(String s) { id = s; } public String getId() { return id; } public void accept(Visitor v) {
v.visit(this);
}
} class PrintVisitor implements Visitor { public void visit(Leaf l) { System.out.println(l); } public void visit(Node n) { System.out.println(n.getId()); for(Component c : n) c.accept(this); // Kluczowe! // nie wolno: System.out.println(c); } } class IncreaseValueVisitor implements Visitor { public void visit(Leaf l) { l.setValue(l.getValue()+1); } public void visit(Node n) { for(Component c : n) c.accept(this); } } class Composite { public static void main(String args[]) { Node root = new Node("Zwierzęta"); Node node; root.add(node = new Node("Psy")); node.add(new Leaf("Azor", 5)); node.add(new Leaf("Aza", 7)); root.add(node = new Node("Koty")); node.add(new Leaf("Pusia", 5)); node.add(new Leaf("Mruczek", 10)); root.accept(new IncreaseValueVisitor()); root.accept(new PrintVisitor()); } }

Zobacz demo programu:






4. Podsumowanie

Poznaliśmy trzy ważne wzorce projektowe:





5. Zadania

Zad. 1

(kontynuacja zadania z poprzedniego wykładu)

Kwiaty mogą być kupowane w skrzynkach.
Skrzynki mogą być ładowane na samochody.
Samochody mogą być przewożone promami.

Zmienić architekturę aplikacji tak by w w/w warunkach można było zastosować wzorce Composite i Visitor.
Zapewnić możliwość dodawania dowolnych visitorów.
Jako przykład: visitor obliczający koszt kwiatów i visitor obliczający ilość kwiatów.