Látogató programtervezési minta

Az objektumorientált programozásban és a szoftverfejlesztésben a Látogató tervezési minta segítségével tudjuk szétválasztani az algoritmust és az objektum szerkezetét. A gyakorlati eredménye ennek a szétválasztásnak az, hogy képessé válik a program arra, hogy új műveleteket adjunk hozzá a létező objektumstruktúrához anélkül, hogy módosítanánk annak szerkezetét. Ez az egyik útja az OCP (Open-Closed Principle) tervezési alapelv megvalósításának.

A lényege, hogy lehetővé teszi, hogy egy új virtuális funkciót adjunk az osztályokhoz anélkül, hogy az osztályok szerkezetét meg kellene változtatni. Helyette létrejön egy látogató osztály, amely implementálja az összes létező megvalósítását az adott virtuális funkciónak. A látogató tartalmazza a példány referencia beviteli értékét, és lehetővé teszi a kettős metódust.

Definíció szerkesztés

A négyek bandája (angolul Gang of Four, röviden GoF) szerinti tervezési alapelv így definiálja a Látogató mintát:

Bevezet egy műveletet, ami leírja az elemeket az objektum struktúrájában. Az új műveletet anélkül hozza létre, hogy megváltoztatná az osztályok szerkezetét, amiben működik.

A Látogató a természetéből fakadóan ideális minta ahhoz, hogy csatoljon publikus alkalmazásokat, lehetővé téve, hogy a felhasználók úgy tudjanak ezekben dolgozni, hogy a tényleges osztályok szerkezetét nem változtatják meg.

Motiváció szerkesztés

Tekintsük például a 2D CAD rendszert. Lényegét tekintve több típusa van, amik leírják a geometriai alakzatokat, mint amilyenek a körök, vonalak és az ívek. Az egységek rétegek szerint elkülönülnek, és a típushierarchia tetején történik a rajzolás, ami egy metódus és a rétegek listájából, valamint néhány property-ből áll.

Az alapművelet ezen a típushierarchián az, hogy mentjük a rajzot egy egyedi fájlkiterjesztésben. Első pillantásra úgy tűnhet, hogy a rajzolást hozzáadtuk minden helyi mentési metódushoz a hierarchia összes típusában. De aztán mi el szeretnénk menteni ezt egy másik fájlformátumban is, és hozzáadni egyre több és több metódust, hogy elmenthessük még sok-sok formátumban, amivel hamarosan telezsúfoljuk a kezdetben viszonylag szegényes geometriai adatstruktúránkat.

A megfelelő út, hogy megfejtsük azt, hogy mik lennének a főbb funkcióink, amik minden fájlkiterjesztéshez használatosak. Ilyen a mentési funkció is, ami tartalmazza a rajzolást, mint bemenetet, és azon áthaladva kódolja azt a specifikus fájlformátumban. Hogyha ezt megtesszük számos különböző formátumban, hamarosan túl sok funkciók közötti duplikációval találkozhatunk. Például, ha mentünk egy körívet raszter formátumban. Ekkor olyan kódot kapunk, ahol nem számít, hogy a raszter kódot milyen speciális esetekre használjuk, mert különbséget tud tenni a primitív alakzatok között, mint amilyenek a vonalak és sokszöget, egyebek. A kód tehát egy nagy külső hurkon halad át az objektumon, ami egy szerteágazó döntési fát tartalmaz, ami lekérdezi menet közben az objektum típusát. A másik probléma a megközelítéssel, hogy nagyon könnyen el tudja hibázni a megfelelő alakzatot egy, vagy több mentésnél, vagy akkor, ha bejön egy új alakzat a képbe, de a belső mentési rutin csak egy fájltípust implementál és többet nem. Ez vezető kód és karbantartási problémákhoz egyaránt vezethet.

Ehelyett alkalmazhatjuk a Látogató tervezési mintát. Ez a minta kódolja a logikai kapcsolatokat az egész hierarchiában, méghozzá egy önálló osztályban, ami minden típusra tartalmaz egy metódust. A mi CAD példánk esetében minden mentési funkció implementálva és elkülönítve lesz jelen a Látogató alosztályaiban. Ez el tudja kerülni a duplikációt típusellenőrzésekkel, valamint bejáró algoritmusokkal, akkor, pedig figyelmeztet, ha egy alakzat kimarad.

Másik motiváció, hogy a beépített ciklusokat újra fel lehet használni. Például lehet egy mappastruktúra feletti ciklus, ami implementálódni tud a Látogató tervezési mintával. Ez lehetővé tudná tenni például, hogy külön funkciókat építsünk be, mint amilyenek a keresések, a mentések, a könyvtártörlések és egyéb hasonlók, valamint a Látogató több funkciójára is használja ugyanazt a ciklust.

Részletek szerkesztés

A látogató tervezési mintához ajánlott olyan programozási nyelv, amely támogatja az egyszeri küldést. Ebben két alapesetet tekintünk a kód felépítését tekintve. Az egyes osztálytípusok összességét "elemnek", míg a másikat "látogatónak" nevezzük. Az elemnek van egy „elfogadó” metódusa, amely látogatót fogad argumentumként. Az „elfogad” metódus meghívja a „látogat” metódust a látogatóból, amivel az elem átadja önmagát argumentumként a „látogat” metódusban. Így:

  • Amikor az „elfogadó” metódust meghívja a program, annak a végrehajtása az alábbiak szerint választ metódust:
    1. Az elem dinamikus típusa.
    2. A látogató statikus típusa.
  • Amikor a kapcsolódó „látogat” metódust meghívja a program, akkor annak végrehajtása az alábbiak szerint választ metódust:
    1. A látogató dinamikus típusa.
    2. Az elem statikus típusa, ami már ismert az „elfogadó” metódusból, ahol dinamikus típusú az elem. (Bónuszként, ha a látogató nem tudja kezelni a megadott elemnek a típusát, a fordítóprogram elkapja a hibát.)
  • Következésképpen, a látogató implementációja az alábbiak szerint végzi el a beállítást:
    1. Az elem dinamikus típusa.
    2. A látogató dinamikus típusa.

A Common Lisp programozási nyelv a fenti módon implementálva támogatja a több küldő (nem csak feladó) rendszert, amivel egyszerűbben megvalósítható a látogató tervezési minta, mert egy függvényt többféleképpen is túl lehet terhelni. Ez lehetséges más nyelvekben is, például a C# Dynamic Language Runtime (DLR) segítségével.

Egy egyszerű algoritmus ezen az úton áthalad az elemek gráfján, és közben nagyon sok különböző műveletet tud végrehajtani, úgy, hogy a bejárás során átadja azokat a látogatónak, amit az le is tud kezelni, hiszen az összes elem dinamikus típuson alapul az elemben és a látogatóban is. A látogató megfelel a nyílt/zárt elvnek, mivel csak publikus adatokkal dolgozik, illetve az egyértelmű felelősség elvének, mert a mintát egy külön objektum hozzáadásával valósítja meg.

Java példa szerkesztés

A következő példa Java programozási nyelven van, és láthatjuk benne azokat a csomópontokat, amiből felépül a logikai fa (konkrétan egy autó összetevőit írja le), illetve annak a nyomtatását mutatja. Ahelyett, hogy létrehoznánk egy „nyomtat” metódust a struktúra minden alosztályában (Wheel (Kerék), Engine (Motor), Body (Kaszni) és Car (Autó)), egy egyszerű látogató osztály (CarElementPrintVisitor) elvégzi a feladatot. Mivel a különböző alosztályok kissé eltérő módon nyomtatnak, úgy a CarElementPrintVisitor az irányítása alá vonja a folyamatot és az adott osztályokban lévő „látogat” metódus alapján hajtja azt végre. A CarElementDoVisitor osztály ugyanígy analóg a különféle formátumra történő mentési művelettel.

Diagram szerkesztés

  //UML ábra//

Források (Bemenet) szerkesztés

interface ICarElementVisitor {
    void visit(Wheel wheel);
    void visit(Engine engine);
    void visit(Body body);
    void visit(Car car);
}

interface ICarElement {
    void accept(ICarElementVisitor visitor);
}

class Wheel implements ICarElement {
    private String name;

    public Wheel(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void accept(ICarElementVisitor visitor) {

                    /*
                     * elfogadja (ICarElementVisitor) a Wheel implementációjaként
                     * elfogadja (ICarElementVisitor) az ICarElement-ben, ahol meghívták
                     * elfogadja a kötődést futás közben. Ez tekinthető az
                     * első küldésnek. Mindazonáltal, dönt, hogy
                     * hívja a látogatót ezt ((Wheel) (Engine) stb.) tudja
                     * futási időben is, mivel ez ismert a fordításkor
                     * és ekkor következik a Wheel. Ezen kívül, minden egyes futáskor implementálja az
                     * ICarElementVisitor a látogatót (Wheel), amelyik egy másik döntést hoz futás közben. Ez tekinthető
                     * a második küldésnek.
                     */

        visitor.visit(this);
    }
}

class Engine implements ICarElement {
    public void accept(ICarElementVisitor visitor) {
        visitor.visit(this);
    }
}

class Body implements ICarElement {
    public void accept(ICarElementVisitor visitor) {
        visitor.visit(this);
    }
}

class Car implements ICarElement {
    ICarElement[] elements;

    public Car() {
        this.elements = new ICarElement[] { new Wheel("front left"),
            new Wheel("front right"), new Wheel("back left") ,
            new Wheel("back right"), new Body(), new Engine() };
    }

    public void accept(ICarElementVisitor visitor) {
        for(ICarElement elem : elements) {
            elem.accept(visitor);
        }
        visitor.visit(this);
    }
}

class CarElementPrintVisitor implements ICarElementVisitor {
    public void visit(Wheel wheel) {
        System.out.println("Visiting " + wheel.getName() + " wheel");
    }

    public void visit(Engine engine) {
        System.out.println("Visiting engine");
    }

    public void visit(Body body) {
        System.out.println("Visiting body");
    }
 
    public void visit(Car car) {
        System.out.println("Visiting car");
    }
}

class CarElementDoVisitor implements ICarElementVisitor {
    public void visit(Wheel wheel) {
        System.out.println("Kicking my " + wheel.getName() + " wheel");
    }

    public void visit(Engine engine) {
        System.out.println("Starting my engine");
    }

    public void visit(Body body) {
        System.out.println("Moving my body");
    }

    public void visit(Car car) {
        System.out.println("Starting my car");
    }
}

public class VisitorDemo {
    public static void main(String[] args) {
        ICarElement car = new Car();
        car.accept(new CarElementPrintVisitor());
        car.accept(new CarElementDoVisitor());
    }
}

Jegyzet: A minta rugalmasabb felhasználása végett ez a tervezési minta létrehoz egy wrapper (csomagoló) osztályt, ami implementálja az interfész által elfogadott metódust. A "csomagoló" tartalmazza azt a metódust, amelyik az ICarElement nevű interfészt meghívja, és amelyik a konstruktoron keresztül beállítja az értékeket. Ez a megközelítés elkerüli, hogy implementálja az interfész az összes elemet.

Kimenet szerkesztés

Visiting front left wheel

Visiting front right wheel

Visiting back left wheel

Visiting back right wheel

Visiting body

Visiting engine

Visiting car

Kicking my front left wheel

Kicking my front right wheel

Kicking my back left wheel

Kicking my back right wheel

Moving my body

Starting my engine

Starting my car

Common Lisp példa szerkesztés

Források (Bemenet) szerkesztés

defclass auto ()
  ((elements :initarg :elements)))

(defclass auto-part ()
  ((name :initarg :name :initform "<unnamed-car-part>")))

(defmethod print-object ((p auto-part) stream)
  (print-object (slot-value p 'name) stream))

(defclass wheel (auto-part) ())

(defclass body (auto-part) ())

(defclass engine (auto-part) ())

(defgeneric traverse (function object other-object))

(defmethod traverse (function (a auto) other-object)
  (with-slots (elements) a
    (dolist (e elements)
      (funcall function e other-object))))

;; itt "látogatja"

;; elfogja az összeset
(defmethod do-something (object other-object)
  (format t "don't know how ~s and ~s should interact~%" object other-object))

;; visitation involving wheel and integer
(defmethod do-something ((object wheel) (other-object integer))
  (format t "kicking wheel ~s ~s times~%" object other-object))

;; rálát a wheel-re és a symbol-ra
(defmethod do-something ((object wheel) (other-object symbol))
  (format t "kicking wheel ~s symbolically using symbol ~s~%" object other-object))

(defmethod do-something ((object engine) (other-object integer))
  (format t "starting engine ~s ~s times~%" object other-object))

(defmethod do-something ((object engine) (other-object symbol))
  (format t "starting engine ~s symbolically using symbol ~s~%" object other-object))

(let ((a (make-instance 'auto
                        :elements `(,(make-instance 'wheel :name "front-left-wheel")
                                    ,(make-instance 'wheel :name "front-right-wheel")
                                    ,(make-instance 'wheel :name "rear-right-wheel")
                                    ,(make-instance 'wheel :name "rear-right-wheel")
                                    ,(make-instance 'body :name "body")
                                    ,(make-instance 'engine :name "engine")))))
  ;; áthalad a nyomtatandó elemeken
  ;; leírja a szabványos kimenetre vonatkozó szabályokat az other-object-ben
  (traverse #'print a *standard-output*)

  (terpri) ;; új sort nyomtat

  ;; bejárja a többi tárgyat, az ahhoz szükséges tetszőleges összefüggésekkel
  (traverse #'do-something a 42)

  ;; bejárja a többi tárgyat, az ahhoz szükséges önkényes összefüggésekkel
  (traverse #'do-something a 'abc))

Kimenet szerkesztés

"front-left-wheel"
"front-right-wheel"
"rear-right-wheel"
"rear-right-wheel"
"body"
"engine"
kicking wheel "front-left-wheel" 42 times
kicking wheel "front-right-wheel" 42 times
kicking wheel "rear-right-wheel" 42 times
kicking wheel "rear-right-wheel" 42 times
don't know how "body" and 42 should interact
starting engine "engine" 42 times
kicking wheel "front-left-wheel" symbolically using symbol ABC
kicking wheel "front-right-wheel" symbolically using symbol ABC
kicking wheel "rear-right-wheel" symbolically using symbol ABC
kicking wheel "rear-right-wheel" symbolically using symbol ABC
don't know how "body" and ABC should interact
starting engine "engine" symbolically using symbol ABC

Jegyzet szerkesztés

Az other-object (más tárgy) nem fontos a traverse osztályban, de ezzel tudunk névtelen funkciót is használni, amelyik meghívja a cél eléréséhez kívánt metódust az objektummal együtt.

(defmethod traverse (function (a auto)) ;; other-object törölve
  (with-slots (elements) a
    (dolist (e elements)
      (funcall function e)))) ;; innen is

;; ...

  ;; alternatív út keresése a print bejárására
  (traverse (lambda (o) (print o *standard-output*)) a)

  ;; alternatív úton csinál valamit
  ;; az elemekkel egy integer 42-vel
  (traverse (lambda (o) (do-something o 42)) a)

Mármost, ha több küldő fordul elő, a hívás a névtelen funkció testéből történik, és így a traverse osztálynak (a mapping funkció segítségével) csak szét kell osztania a funkciókat az objektumok fölött. Így az összes nyom eltűnik a Látogató tervezési mintából, kivéve a mapping funkciót, ahol nem marad bizonyíték arra, hogy a folyamatban több objektum is szerepet játszott. A két objektum és a küldő összes tudását a lambda funkcióban tárolja.

Kapcsolódó programtervezési minták szerkesztés

  • Parancs programtervezési minta: Hasonlóan a látogató mintához, egy objektumot használunk, hogy reprezentáljunk és párosítsunk minden olyan információt, amely szükséges lehet egy metódus későbbi meghívásához.
  • Iterator programtervezési minta: Az Iterátor minta lényege, hogy segítségével szekvenciálisan érhetjük el egy aggregált objektum elemeit, a mögöttes megvalósítás megismerése nélkül.

További információk szerkesztés

Források szerkesztés

Fordítás szerkesztés

  • Ez a szócikk részben vagy egészben a Visitor pattern című angol Wikipédia-szócikk fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.