14. hét: visitor tervezési minta

Czirkos Zoltán · 2019.02.27.

Heti kiegészítő feladatok

1. Elődeklarációk

Ami majd kelleni fog később:

class Valami;           /* 1 */

class ValamiTarolo {
    Valami &r;          /* 2 */
    Valami *p;          /* 3 */

    void f1(Valami &v); /* 4 */
    void f2(Valami v);  /* 5 */
    Valami f2();        /* 6 */
};

Az 1-es helyen egy elődeklarációt látsz. Ezzel megmondhatod a fordítónak, hogy a Valami egy osztály lesz, ugyanakkor nem definiálod azt (incomplete type). Így példányt még nem hozhatsz létre belőle, ugyanakkor rá mutató referenciát (2) és pointert (3) már igen. Deklarálhatsz olyan függvényt, amelyik ilyen pointerrel vagy referenciával dolgozik (4). Sőt, deklarálhatsz olyan függvényt is, amelyik érték szerint (!) vesz át vagy ad vissza ilyet (5, 6). Persze amikor ez utóbbi függvényeket definiálni vagy hívni szeretnéd, akkor már szükség van az osztály teljes definíciójára, mert tudni kell a méretét, és hogy milyen a másoló konstruktora és a destruktora.

Leggyakrabban ezt akkor használjuk, amikor körkörösen egymásra hivatkozó osztályokat definiálunk:

class A;
class B;

class A {
    B *b;   /* ok, B deklarálva van */
};
class B {
    A *a;   /* itt meg már mindegy */
};

2. A heterogén kollekció

Itt egy heterogén kollekció, kiindulási alapnak:

class Alakzat {
    public:
        virtual void kiir() const = 0;
        virtual ~Alakzat() {}
};

class Tegla : public Alakzat {
    public:
        virtual void kiir() const {
            std::cout << "Egy egy teglalap vagyok.\n";
        }
};

class Kor : public Alakzat {
    public:
        virtual void kiir() const {
            std::cout << "En egy kor vagyok.\n";
        }
};

class Rajztabla {
    private:
        std::vector<Alakzat *> a;
    public:
        /* ... */
};

Először:

  1. Írj a rajztáblának hozzaad(Alakzat *) és listaz() függvényt! Tiltsd le a másoló konstruktort és az értékadó operátort, most nem kellenek majd. Persze azért a destruktort írd meg.
  2. Írj rövid teszt kódot, amelyben hozzáadsz a rajztáblához néhány dinamikusan foglalt alakzat.

3. Hány darab alakzat?

Írj programkódot, amelyben megszámolod, melyik típusú alakzatból hány darab van! Ehhez hozz létre két számlálót, és add azt oda a rajztáblának! Szükséged lesz az Alakzat osztályban egy virtuális függvényre is: mindegyik alakzat a hozzá tartozó számlálót növeli majd meg.

int kor, tegla;
rajztabla.mindet_szamlal(kor, tegla);
std::cout << kor << " db kör és " << tegla << " db téglalap.\n";

Ha ez kész, képzeld el, mi történne egy új alakzat (pl. rombusz) hozzáadásakor. Kellene egy új számláló, addig rendben van: a nagyobbik baj az, hogy az összes alakzat osztályt módosítani kell, mert eggyel több paramétere lenne a virtuális függvényeiknek.

4. A számlálók összekötése

Az előző feladat tapasztalatából kiindulva, tedd át a számlálókat egy struktúrába. Így az egyes alakzatok virtuális függvényei (amelyek megnövelik a saját típusukhoz tartozó számlálót) egyparaméterűvé válnak. Megkapják a számlálót tartalmazó objektumot, és megváltoztatják benne valamelyik adattagot.

Valahogy így:

AlakzatSzamlalo sz = { 0, 0 };
rajztabla.mindet_szamlal(sz);
std::cout << sz.kor << " db kör és " << sz.tegla << " db téglalap.\n";

Most már nem kellene minden új alakzattípushoz átírni a számlálós virtuális függvényeket, hanem csak a számlálókat tartalmazó osztálynak lenne egy új adattagja.

5. Számláló osztály

A számlálókat tartalmazó struktúra okosítható, osztályt készíthetünk belőle. Az osztály feladata lehet a számlálók inicializálása, és megnövelése is. Sőt akár az eredmény kiírása is. Valahogy így:

class AlakzatSzamlalo {
    private:
        /* ... */
    public:
        /* ... */
        void kort_szamlal();
        void teglat_szamlal();
        void kiir() const;
};

AlakzatSzamlalo sz;
rajztabla.mindet_szamlal(sz);
sz.kiir();

Alakítsd át így a kódot!

Vedd észre, hogy ez OOP-sebb megoldás, mint az előző. Most a számlálással kapcsolatos összes teendő ebbe az osztályba került. Ennek az osztálynak a számlálás az egyetlen feladata, nem csinál mást, se többet, se kevesebbet. Maguk a számlálók pedig privát adattagjai, nem fér más hozzá – nem is kell másnak törődnie sem azokkal.

6. AlakzatFeldolgozo osztály

Vizsgáld meg az előbb kapott kódod egy újabb szemszögből. Valami ilyesmi a helyzet most:

class Rajztabla {
    public:
        void mindet_szamlal(AlakzatSzamlalo &sz) {
            for (size_t i = 0; i < a.size(); ++i)
                a[i]->virtualis_fuggveny(sz);
        }
};

void Tegla::virtualis_fuggveny(AlakzatSzamlalo &sz) {
    sz.teglat_szamlal();
}

void Kor::virtualis_fuggveny(AlakzatSzamlalo &sz) {
    sz.kort_szamlal();
}

Ennek a függvények és osztályok neveitől eltekintve már semmi köze a megszámláláshoz. Emiatt bevezethetnénk egy újabb absztrakciót, az AlakzatFeldolgozo osztályt!

Az AlazatFeldolgozo egy interfész, amely minden lehetséges alakzattípushoz tartalmaz egy virtuális függvényt. Ezt az interfészt kell majd megvalósítani, ha az alakzatokkal szeretnénk csinálni valamit:

class AlakzatFeldolgozo {
    public:
        virtual void teglat_feldolgoz(Tegla & t) = 0;
        virtual void kort_feldolgoz(Kor & k) = 0;
        virtual ~AlakzatFeldolgozo() {}
};

Ha szeretnénk tudni, melyikből hány darab van, származtatunk ebből egy számlálót, és megvalósítjuk a feldolgozó függvényeket. Ha szeretnénk kilistázni úgy, akkor származtatunk egy listázót, megvalósítjuk a függvényeit... És így tovább. A feldolgozó függvények mindig megkapják a konkrét alakzatokat is, tehát a konkrét példánnyal is tudnak dolgozni. A rajztábla osztálynak egyetlen egy dolga marad, megkapva egy AlakzatFeldolgozo osztályt, mindegyik alakzatnak szólni, hogy végezze el azt a teendőt, amit az adott feldolgozó csinálna vele:

class Rajztabla {
    public:
        void mindet_feldolgoz(AlakzatFeldolgozo &af) {  // OOP-ish af
            for (size_t i = 0; i < a.size(); ++i)
                a[i]->feldolgoz(af);
        }
};

Alakítsd át így a kódot!

Vedd észre, hogy most egy új művelet bevezetéséhez:

  • Nem kell módosítani a rajztábla osztályt. A mindet_feldolgoz() függvény látja el a tároló bejárásának feladatát, de az alakzattípustól függő teendők már az AlakzatFeldolgozo osztályba kerültek.
  • Nem kell módosítani az egyes alakzat osztályokat sem. Eddig az új műveletekhez mindig az alakzathoz adtunk hozzá új virtuális függvényt: virtual kiir(), virtual szamlal(). Most azonban ott csak egyetlen virtuális függvény van: az, amelyik tudja, hogy a feldolgozónak melyik függvényét kell meghívja.

Az alakzatok neveit kiíró virtuális függvényt is „refaktoráld ki” egy feldolgozóba! Végül ezt a kódot fogod kapni:

AlakzatKiiro k;
rajztabla.mindet_feldolgoz(k);  /* kör kör tégla kör */

AlakzatSzamlalo sz;
rajztabla.mindet_feldolgoz(sz);
sz.kiir();                      /* 3 kör 1 tégla */

7. Visitor tervezési minta

Amit a fenti átalakításokkal kaptál, azt visitor tervezési mintának nevezik („látogató”). Ezt akkor használjuk, ha van egy osztályhierarchiánk (mint most az alakzatok), és arra számítunk a projektben, hogy gyakran kell majd újfajta műveleteket hozzáadni: megszámlálás, kiírás, terület, kerület.

Az angol nyelvű elnevezések:

class ShapeVisitor {
    public:
        virtual void visit_rectangle(Rectangle & r) = 0;
        virtual void visit_circle(Circle & r) = 0;
};

class Shape {
    public:
        virtual void accept_visitor(ShapeVisitor &sv) = 0;
};

class Container {
    public:
        virtual void visit_all(ShapeVisitor &sv) {
            for (...)
                shapes[i]->accept_visitor(sv);
        }
}

Tehát a visitor „meglátogatja” az alakzatokat, az alakzatok pedig „elfogadják a látogatást”. A ShapeVisitor összes függvényét egyébként lehetne visit()-nek is nevezni, mert a paraméter típusa alapján is történhet a függvény kiválasztása – de ez OOP szempontból lényegtelen.

A visitor tervezési minta hátránya, hogy új típus bevezetését nehezíti meg. Eddig egy új művelet hozzáadásához kellett módosítani az összes alakzatot – most azt nem kell. Mert most új típusú alakzat hozzáadásához kell módosítani az összes visitort – eddig azt nem kellett.