Visitor (návrhový vzor)

Návrhový vzor Visitor umožňuje rozšiřovat možnosti objektu bez nutnosti modifikace jeho třídy. Snaží se o podobný cíl jako aspektově orientované programování. Visitor patří mezi návrhové vzory, které ovlivňují chování tříd a jejich instancí, tzv. behavioral patterns.

Účel

Návrhový vzor Visitor lze využít v situaci, kdy navrhujeme množinu tříd, do které již nebudeme žádnou třídu přidávat, ale je pravděpodobné, že budeme potřebovat přidat nějakou funkcionalitu. Připravíme se tedy na situaci, kdy jsme nuceni do všech tříd naší konečné množiny přidat další metodu.

Princip

Principem je, že pro každou novou akci, kterou chceme dodat původní množině tříd, vytvoříme novou třídu. Tato nová třída představuje „návštěvníka“. Instanci tohoto návštěvníka pak předáme původní třídě a ta v podstatě sama na sebe zavolá odpovídající metodu návštěvníka. Původní třída tedy představuje „navštíveného“. Jinými slovy: návštěvník umí vykonat novou akci, navštívený ho přijme a nechá ho se sebou vykonat onu novou akci. Takže platí, že kolik bude dodatečných akcí, tolik bude návštěvnických tříd.

Implementace

Pokud chceme implementovat návrhový vzor Visitor, budeme postupovat takto: Z předchozího textu vyplývají 2 skutečnosti:

  1. Instance navštěvované třídy musí umět přijmout návštěvnickou třídu.
  2. Instance třídy návštěvníka musí umět provést danou akci s instancí třídy, kterou navštívil.

Tohoto dosáhneme tak, že definujeme rozhraní pro navštěvovanou třídu a rozhraní pro návštěvnickou třídu. Dále si popíšeme, co budou tato rozhraní obsahovat:

Rozhraní navštěvované třídy (INavstiveny)

  • Toto rozhraní musí implementovat všechny navštěvované třídy.
  • Rozhraní musí definovat metodu, která přijme návštěvníka a parametry. Tedy například:
public interface INavstiveny {
	public Object prijmi(INavstevnik navstevnik, Object parametry);
}

Návratová hodnota a atribut parametry mají definován obecný typ Object. Díky tomu může tato metoda přijímat a vracet v podstatě jakékoliv parametry. V případě, že má metoda přijímat více parametrů, vložíme parametry do nějaké přepravky (Objekt, který obsahuje jiné objekty) a tu pak předáme v parametru.

Rozhraní návštěvnické třídy (INavstevnik)

  • toto rozhraní musí implementovat všechny návštěvnické třídy. Tedy třídy, které budou dodávat novou funkcionalitu původním třídám. Toto rozhraní může vypadat například takto:
public interface INavstevnik{
	public Object aplikujNa(Navstiveny1 navstiveny, Object parametry);
	public Object aplikujNa(Navstiveny2 navstiveny, Object parametry);
	public Object aplikujNa(Navstiveny3 navstiveny, Object parametry);
	public Object aplikujNa(Navstiveny4 navstiveny, Object parametry);
	public Object aplikujNa(Navstiveny5 navstiveny, Object parametry);
}

Rozhraní INavstevnik ukládá návštěvníkům implementovat konkrétní verzi metody pro každou třídu z množiny navštěvovaných tříd. Příklady navštěvovaných tříd jsou nyní Navstiveny1, Navstiveny2, Navstiveny3, Navstiveny4, Navstiveny5.

Když se podíváme na rozhraní INavstevnik, zjistíme, proč je důležité, aby byla množina navštěvovaných tříd neměnná. V případě, kdy bychom do množiny přidali další třídu, museli bychom do rozhraní INavstevnik a do všech tříd, které jej implementují doplnit metodu pro přidanou třídu. To sice lze, ale v okamžiku, kdy je již návštěvnických tříd velké množství se může tato situace stát neudržitelnou. Každá ze tříd, která bude implementovat rozhraní INavstiveny potom implementuje metodu prijmi takto:

public Object prijmi(INavstevnik navstevnik, Object params){
	return navstevnik.aplikujNa(this, params);
}

Jak jsem již dříve zmínil, navštívená třída zavolá sama na sebe metodu návštěvnické třídy.

Ukázka

Vysvětlování a teorie již bylo uvedeno dost, nyní si pojďme ukázat použití návrhového vzoru Visitor na příkladu. Představte si situaci, kdy máme navrhnout program pro jednoduchou evidenci územních celků v České republice. Územními celky rozumíme kraje, obce a části obce. Z toho tedy definujeme množinu tříd: Kraj, Obec, CastObce. Takže nyní máme skupinu tříd, která představuje územní celky, a také potřebujeme, aby instance těchto tříd uměly přijmout návštěvníka. Kromě těchto tříd definujeme také tři rozhraní: IUzemniCelek, INavstiveny, INavstevnik. Výsledný diagram tříd najdete na obrázku 1.

Obrázek 1: Diagram tříd (Class diagram)

(Obrázek bude doplněn)

Pojďme si nyní stručně vysvětlit, co nám diagram na obrázku 1 říká. Vidíme, že třídy Kraj, Obec a CastObce implementují rozhraní INavstiveny a IUzemniCelek. Jinými slovy, že jsou tyto třídy územními celky a že umějí přijmout návštěvníka. Dále si můžeme všimnout, že v kraji může být žádná a více obcí a v obci může být žádná a více částí obce. Územní celky tedy mohou tvořit jakousi strukturu. Dále na diagramu figuruje rozhraní INavstevnik, které je společným rozhraním pro všechny návštěvníky. V našem případě jsem vytvořil jednu návštěvnickou třídu NVystup. Třída NVystup je návštěvník, který umí poslat navštěvovanou třídu na jakýkoliv výstupní proud. Nyní se podívejme na výpis konkrétních rozhraní a tříd, které můžeme vidět na diagramu.

Výpis rozhraní IUzemniCelek

package navstevovani;

/**
 * Rozhraní územních celků.
 * @author Michal Náprstek
 */
public interface IUzemniCelek {

    /**
     * @return počet obyvatel s trvalým pobytem hlášeným v konkrétním územním
     * celku.
     */
    public int getPocetObyvatel();

    /**
     * @return rozloha územního celku v ha.
     */
    public long getRozlohu();

    /**
     * @return název územního celku.
     */
    public String getNazev();

}

Předpokládáme, že množina druhů územních celků je pro náš systém konečná. V takovém případě můžeme vytvořit kód i výše zmíněných tříd. Budeme tedy chtít, aby implementovaly rozhraní IUzemniCelek. Dále bychom rádi připravili třídy na rozšíření o další metody. Necháme tedy všechny třídy implementovat také rozhraní INavstiveny, aby uměly přijmout návštěvníka.

Výpis třídy Kraj pak bude vypadat takto:

Výpis třídy Kraj
package navstevovani;

import java.util.ArrayList;

/**
 * Instance této třídy představují konkrétní kraje. Tato třída umí přijmout
 * návštěvníka podle návrhového vzoru Visitor.
 * @author Michal Náprstek
 */
public class Kraj implements IUzemniCelek, INavstiveny {

    private int pocetObyvatel = 0;
    private long rozloha = 0;
    private String nazev = new String("");
    private ArrayList<Obec> obce = new ArrayList<Obec>();

    /**
     * Vytváří novou instanci kraje, a naplní parametry své atributy
     * @param pocetObyvatel počet obyvatel v kraji
     * @param rozloha rozloha kraje
     * @param nazev název kraje
     * @param obce obce v daném kraji
     */
    public Kraj(int pocetObyvatel, long rozloha, String nazev, ArrayList<Obec> obce) {
        this.pocetObyvatel = pocetObyvatel;
        this.rozloha = rozloha;
        this.nazev = nazev;
        this.obce = obce;
    }

    /**
     * @inheritDoc
     */
    public int getPocetObyvatel() {
        return pocetObyvatel;
    }
    
    /**
     * @inheritDoc
     */
    public long getRozlohu() {
        return rozloha;
    }

    /**
     * @inheritDoc
     */
    public String getNazev() {
        return nazev;
    }

    /**
     * @return seznam obcí v daném kraji. Pokud kraj žádné obce nemá, vrací
     * prázdný seznam.
     */
    public ArrayList<Obec> getObce() {
        return obce;
    }

    /**
     * @inheritDoc
     */
    public Object prijmi(navstevnici.INavstevnik navstevnik, Object parametry) {
        return navstevnik.aplikujNa(this, parametry);
    }

}

Ostatní dvě třídy budou vypadat podobně jako třída Kraj:

Výpis třídy Obec

package navstevovani;

import java.util.ArrayList;
import navstevnici.INavstevnik;

/**
 * Instance této třídy představují konkrétní obce. Tato třída umí přijmout
 * návštěvníka podle návrhového vzoru Visitor.
 * @author Michal Náprstek
 */
public class Obec implements IUzemniCelek, INavstiveny {

    private int pocetObyvatel = 0;
    private long rozloha = 0;
    private String nazev = new String("");
    private ArrayList<CastObce> castiObce = new ArrayList<CastObce>();
    
    /**
     * Vytváří novou instanci obce, a naplní parametry své atributy
     * @param pocetObyvatel počet obyvatel v obci
     * @param rozloha rozloha obce
     * @param nazev název obce
     * @param castiObce části dané obce
     */
    public Obec(int pocetObyvatel, long rozloha, String nazev, ArrayList<CastObce> castiObce) {
        this.pocetObyvatel = pocetObyvatel;
        this.rozloha = rozloha;
        this.nazev = nazev;
        this.castiObce = castiObce;
    }

    /**
     * @inheritDoc
     */
    public int getPocetObyvatel() {
        return pocetObyvatel;
    }

    /**
     * @inheritDoc
     */
    public long getRozlohu() {
        return rozloha;
    }

    /**
     * @inheritDoc
     */
    public String getNazev() {
        return nazev;
    }

    /**
     * @return seznam částí obce v dané obci. Pokud v obec nemá žádné části
     * obce, vrací prázdný seznam.
     */
    public ArrayList<CastObce> getCasti() {
        return castiObce;
    }

    /**
     * @inheritDoc
     */
    public Object prijmi(INavstevnik navstevnik, Object parametry) {
        return navstevnik.aplikujNa(this, parametry);
    }

}

Výpis třídy CastObce

package navstevovani;

import java.util.ArrayList;
import navstevnici.INavstevnik;

/**
 * Instance této třídy představují konkrétní části obce. Tato třída umí přijmout
 * návštěvníka podle návrhového vzoru Visitor.
 * @author Michal Náprstek
 */
public class CastObce implements IUzemniCelek, INavstiveny {

    private int pocetObyvatel = 0;
    private long rozloha = 0;
    private String nazev = new String("");

    /**
     * Vytváří novou instanci části obce, a naplní parametry své atributy
     * @param pocetObyvatel počet obyvatel v části obce
     * @param rozloha rozloha části obce
     * @param nazev název části obce
     */
    public CastObce(int pocetObyvatel, long rozloha, String nazev) {
        this.pocetObyvatel = pocetObyvatel;
        this.rozloha = rozloha;
        this.nazev = nazev;
    }

    /**
     * @inheritDoc
     */
    public int getPocetObyvatel() {
        return pocetObyvatel;
    }

    /**
     * @inheritDoc
     */
    public long getRozlohu() {
        return rozloha;
    }

    /**
     * @inheritDoc
     */
    public String getNazev() {
        return nazev;
    }

    /**
     * @inheritDoc
     */
    public Object prijmi(INavstevnik navstevnik, Object parametry) {
        return navstevnik.aplikujNa(this, parametry);
    }

}

Jak je vidět, všechny tři třídy implementují metodu prijmi, aby mohly přijímat návštěvníka, a všechny ji implementují obdobně. Dále si ukážeme, jak bude vypadat v tomto případě rozhraní pro všechny návštěvníky INavstevnik:

Výpis rozhraní INavstevnik

package navstevnici;

import navstevovani.*;

/**
 * Toto je rozhraní návštěvníka podle návrhového vzoru Visitor. Konkrétní návštěvník
 * umí nějakou konkrétní akci. Definuje pro každou třídu, která jej umí přijmout
 * jednu metodu, která provede danou akci s konkrétní instancí dané třídy.
 * @author Michal Náprstek
 */
public interface INavstevnik {

    /**
     * Metoda, která provádí konkrétní akci, kterou daný návštěvník umí s instancí
     * třídy Kraj
     * @param kraj kraj, se kterým se provede určitá akce
     * @param params parametry akce
     * @return návratová hodnota akce
     */
    public Object aplikujNa(Kraj kraj, Object params);

    /**
     * Metoda, která provádí konkrétní akci, kterou daný návštěvník umí s instancí
     * třídy Obec
     * @param obec obec, se kterým se provede určitá akce
     * @param params parametry akce
     * @return návratová hodnota akce
     */
    public Object aplikujNa(Obec obec, Object params);

    /**
     * Metoda, která provádí konkrétní akci, kterou daný návštěvník umí s instancí
     * třídy CastObce
     * @param castObce část obce, se kterým se provede určitá akce
     * @param params parametry akce
     * @return návratová hodnota akce
     */
    public Object aplikujNa(CastObce castObce, Object params);

Vidíme tedy, že každý návštěvník bude mít pro každou navštěvovanou třídu jednu verzi konkrétní metody. Nyní nám již nic nebrání se podívat, jak bude vypadat třída NVystup, která bude konkrétní implementací návštěvníka a bude mít na starost výpis navštěvované třídy do zadaného proudu.

Výpis třídy NVystup

package navstevnici;

import java.io.IOException;
import java.io.OutputStream;
import navstevovani.CastObce;
import navstevovani.Kraj;
import navstevovani.Obec;

/**
 * Instance této třídy umí zařídit, aby byly přijímané třídy poslány na daný výstup.
 * Umožňuje tedy poslat strukturovaný výpis na jakýkoliv výstupní proud.
 * @author Michal Náprstek
 */
public class NVystup implements INavstevnik {

    /**
     * Posílá kraj na daný výstup. Pošle na výstup i obce v kraji, a části obcí, 
     * které jsou v obcích v daném kraji.
     * @param kraj kraj, který má být poslán na výstup
     * @param params výstupní proud. Pokud se nejedná o instaci výstupního proudu je vyhozena výjimka
     * @return null
     */
    public Object aplikujNa(Kraj kraj, Object params) {
        OutputStream os = (OutputStream) params;
        String out = kraj.getNazev();
        try {
            os.write(out.getBytes());
        } catch (IOException ex) {
            System.out.println("kraj se nepodarilo poslat na vystup.");
        }
        for(Obec o : kraj.getObce()){
            o.prijmi(this, params);
        }
        return null;
    }

    /**
     * Posílá obec na daný výstup. Pošle na výstup i části obcí v obci.
     * @param obec obec, která má být poslána na výstup
     * @param params výstupní proud
     * @return null
     */
    public Object aplikujNa(Obec obec, Object params) {
        OutputStream os = (OutputStream) params;
        String out = obec.getNazev();
        try {
            os.write(out.getBytes());
        } catch (IOException ex) {
            System.out.println("obec se nepodarilo poslat na vystup.");
        }
        for(CastObce co : obec.getCasti()){
            co.prijmi(this, params);
        }
        return null;
    }

    /**
     * Posílá část obce na daný výstup.
     * @param castObce část obce, která má být poslána na výstup
     * @param params výstupní proud
     * @return null
     */
    public Object aplikujNa(CastObce castObce, Object params) {
        OutputStream os = (OutputStream) params;
        String out = castObce.getNazev();
        try {
            os.write(out.getBytes());
        } catch (IOException ex) {
            System.out.println("cast obce se nepodarilo poslat na vystup.");
        }
        return null;
    }
}

Nyní program může fungovat například tak, že máme v paměti seznam krajů. Jak již víme, každý kraj obsahuje seznam obcí v daném kraji a každá obec má seznam částí obce. Potřebujeme-li za těchto okolností kompletní výpis poslat na výstup, vytvoříme návštěvníka s určitým výstupním proudem a tohoto návštěvníka postupně předáme všem krajům v seznamu. Návštěvník, tak jak jsme jej vytvořili, už sám projde v rámci kraje všechny jeho obce a jejich části a pošle je postupně na daný výstup.

Literatura

  • Rudolf Pecinovský: Návrhové vzory, ISBN 978-80-251-1582-4

Související články

Externí odkazy

Média použitá na této stránce

Wikitext.svg
wiki written in wikitext, for template use