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 */
};
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:
- Írj a rajztáblának
hozzaad(Alakzat *)
éslistaz()
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. - Írj rövid teszt kódot, amelyben hozzáadsz a rajztáblához néhány dinamikusan foglalt 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.
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.
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.
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 azAlakzatFeldolgozo
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 */
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.