Az UML osztálydiagram

Egy nagy objektumorientált program tervezése nem egyszerű feladat. A Prog1 tárgyban megismert tervezési módok - funkcionális dekompozíció, top-down tervezés - ad-hoc alkalmazása nem elég. Amint osztályaink vannak, azoknak vannak felelősségi körei és viselkedésük, amiket a tervezés elején tisztázni kell. Ha nem tesszük meg időben, az OOP elvek miatt (pl. adatrejtés) ezek utólagos megváltoztatása nehézkessé válik.

A tervezést emiatt célszerű (vagy inkább kell) a kódírás előtt elkezdeni. Milyen osztályaink legyenek, melyik miért legyen felelős? Ez a két legfontosabb kérdés. Ezek megválaszolása, az elképzelések részletezése közben új kérdések merülnek fel, és sokszor derül ki egy korábbi ötletünkről, hogy rossz. Na de hogyan lássuk át sok-sok osztály egymáshoz való viszonyát azelőtt, hogy leírnánk egy sor kódot?

Ha a kód elkészült, akkor sem sokkal jobb a helyzet. Ha van 15 osztályunk, - ami nem számít soknak, - a konvenciók szerint az 15 header és 15 cpp fájl, összesen 30 fájl. Ennyit kell megnézni ahhoz, hogy lássuk, mely osztályok vannak egymással kapcsolatban. A lehetséges kapcsolatok száma pedig O(n2), hiszen bármely osztály bármelyik másikkal kapcsolatban állhat.

Mindkét probléma azt igényli, hogy legyen egy egyszerűsített leírási módja - ha úgy tetszik, modellje - az osztályok viselkedésének és a közöttük lévő kapcsolatoknak. Az UML (Unified Modeling Language) szabvány osztálydiagramjai éppen erre valók.

Az objektumorientált programok tervezése jó esetben UML osztálydiagram rajzolásával, tehát papírral, ceruzával és radírral kezdődik. A radír is fontos eleme a folyamatnak, ugyanis a legritkább esetben sikerül elsőre a tervezés, sokszor kell javítani, módosítani.

Ha a tervezési fázison túljutottunk, és demonstrációs vagy dokumentációs célból szeretnénk kulturáltabb formába önteni a diagramunkat, ebben számos diagramszerkesztő áll rendelkezésünkre. A szerzők tapasztalata szerint ezeket az eszközöket érdemes használni C++ projekt esetén:

Az UML ezen kívül számos hasznos diagramtípust definiál, mi azonban csak az osztálydiagramokat, azoknak is csak a számunkra lényeges elemeit mutatjuk be: előbb az osztályok közötti kapcsolatokat, utána az egyes osztályok leírását. Azért pont ebben a sorrendben, mert tervezni is így érdemes. Először csak egy-egy dobozt rajzolunk az osztályoknak, amik közé különféle nyilakat húzunk. Ilyenkor hanyagságból lehagyjuk az adattagokat és a tagfüggvényeket, így egy osztálynak csak egy téglalapot rajzolunk. Ha ezzel nagyjából készen vagyunk, utána érdemes a tagfüggvényekkel és az adattagokkal foglalkozni.

Az UML jelölésben egy osztály egy három részre osztott téglalap. A felső részbe kerül a neve, a középsőbe az adattagjai, az alsóba pedig a tagfüggvényei. Először tekintsünk el az adattagoktól és a tagfüggvényektől, és nézzük meg, hogy az osztályok téglalapjai - a dobozok - között milyen kapcsolatokat definiál az UML.

2. Asszociáció, aggregáció, kompozíció

Fogalmuk

Mindhárom fogalom annak egy formáját takarja, hogy az egyik osztály példányainak köze van a másik osztály bizonyos példányaihoz (ismeri, használja). Ez az UML-beli jelölésük:

Asszociáció, aggregáció, kompozíció

A jelentésük közel áll egymáshoz, sokszor nem könnyű eldönteni, miről is van szó. A két szélső eset - kompozíció és asszociáció - tipikus esetei könnyen felismerhetőek:

Kompozíció: rész-egész viszonyt fejez ki, A objektumok tartalmaznak B-t. Onnan ismerhető fel, hogy az objektumok élettartama szorosan egymáshoz kötődik: ha megszűnik a tartalmazó objektum, vele együtt megszűnik a tartalmazott objektum is. UML-beli jelölése a teli rombusz a tartalmazó oldalán. Ilyen például:

  • Az autó és a kerék viszonya: ha az autó a roncstelepre megy (vagy beleszakad a Balatonba), a kerekei is mennek vele.
  • Cimke és std::string.
  • String és a karakterek viszonya: a sztring megszűnésével a karakterei is megszűnnek.

Asszociáció: gyenge ismeretség, azaz az objektum ismeri a másik létezését, de az élettartamára semmiféle hatással nincs. UML-beli jelölése az ismert objektum felé mutató nyíl (fontos, hogy nem zárt a feje, csak két vonalból áll). Például:

  • Egy autó és a tulajdonosa: az autó bezúzása után a tulajdonos tovább él.
  • Egy könyv és a szerzője: a szerző halálával a könyve tovább él.

Aggregáció: bármi, ami a kettő között van: az A objektum élettartamához kötődik a B-k élettartama, de nem annyira szorosan, mint kompozíció esetén. Jele az üres fejű rombusz a tartalmazó oldalán. Például:

  • Egy autó és a rendszámtáblája: az autó rendszámtábla nélkül is autó, csak nem mehet közútra. Nem feltétlenül hal együtt az autóval, például az autó bezúzásakor átkerülhet másik autóra. A Nemzeti Közlekedési Hatóságnak is van hatása a rendszámtábla élettartamára, pl. bevonathatja, így az NKH és a rendszámtábla között is aggregáció van.
  • Szoba és a falai: a szoba ledózerolása nem feltétlenül vonja maga után minden falának a lebontását, mert a fal több szobához is tartozhat.

Mint a példákból is látszik, az aggregáció "gumi-fogalom", sokféle különböző kapcsolatot takarhat: közös, megoszott objektumot, lazán kapcsolódó, de összefüggő élettartamot, stb.

Megjelenésük a kódban

Fontos az elején tisztázni, hogy ezek a fogalmak logikai kapcsolatokat takarnak, a kódban lehetnek ettől teljesen eltérőek. Mégis, a C++ egyes nyelvi elemei ezekhez jól igazodnak, így a tipikus esetek könnyen felismerhetőek.

Kompozíció: érték szerinti tartalmazás. A C++ nyelv biztosítja, hogy a tartalmazó objektum élettartamával megegyezik a tartalmazott objektumok élettartama. Erre valók a 3. fejezetben megismert speciális "tagfüggvények": konstruktor, másoló konstruktor, operator=, destruktor.

class Cimke /* ... */ {
    std::string felirat;
    // ...
};

Asszociáció: pointer az ismert objektumra. A C++ logikája szerint a pointer által mutatott terület érvényességére nekünk kell figyelni, arra a pointernek mint objektumnak semmi ráhatása nincs.

Ilyenkor fennáll a veszélye, hogy a pointer alól felszabadul a memóriaterület, azaz dangling pointer lesz belőle. Ez ellen általában felsőbb szintű kódnak kell védekeznie.

Egy példa a félrevezető nyelvi megjelenésre: a String egy char* pointert tartalmaz, tehát messziről asszociációnak tűnik. Logikailag azonban ez kompozíció, és a String biztosítja, hogy a karakterek élettartama is feleljen meg ennek.

Aggregáció: mivel sokféle kapcsolatot takarhat, elég változatos módokon jelenhet meg a kódban.

Az egyik leggyakoribb speciális esetére, a megosztott, közös erőforrásra van bevett megoldás, a shared_ptr. C++11 óta a szabványos könyvtár - <memory> - része, de C++98-ban is megvalósítható, így tipikusan minden közepes vagy nagyobb projekt C++11 előtt is használt ehhez hasonlót, tipikusan a boost::shared_ptr-t.

Érdekesség, hogy az UML leginkább a Java nyelv fogalmaihoz igazodik, ezért néhány C++-specifikus dolognak - mint látni fogjuk - nincs rendes jelölése. Ezzel szemben Java-ban nincs megkülönböztetett nyelvi eleme a "sima" asszociációnak és a kompozíciónak, technikailag minden aggregációként viselkedik.

C++11 sarok: shared_ptr, weak_ptr

A megosztott közös erőforrás legegyszerűbb, automatikus memóriakezelést biztosító módja a shared_ptr. Ez egy olyan okos pointer, ami számlálja, hány ugyanilyen okos pointer mutat az adott objektumra. Amint megszűnik az utolsó okos pointer, ami hivatkozik az objektumra, azaz a referenciaszámláló 0-ra csökken, felszabadítja az objektumot.

Ilyen esetekben előfordulhatnak körkörös hivatkozások: pl. ha két objektum egymásra mutat, és mindkettő referenciaszámlálója 1. Tehát egyik sem szabadul fel, de egyik sem érhető el máshonnan a programból: tehát memóriaszivárgás történt. Ilyenkor az egyik irányban weak_ptr-t érdemes használni, ami engedi, hogy felszabaduljon az objektum. A weak_ptr annyival tud többet egy sima pointernél, hogy tudjuk ellenőrizni, hogy a hivatkozott objektum létezik-e még (ha nem, NULL pointert kapunk). Ennek használatával az aggregációt átalakíthatjuk asszociációra, így szüntetve meg a körkörös hivatkozást.

A shared_ptr-ről még lesz szó bővebben az STL-es fejezetben.

Multiplicitás, szerep

Ezek a kapcsolatok nem feltétlenül 1-1 objektum között állnak fenn. Egy tartalmazó objektum több másikat is tartalmazhat, és bármely objektumot ismerhet több másik. Ha ennek van jelentősége, a nyilakra írva jelezhetjük.

  • Lehet konkrét szám: 2
  • Lehet intervallum: 3..5, szigorúan két darab ponttal elválasztva, szóköz nélkül.
  • A számok helyén állhat *, ami akárhányat jelent: pl. simán * vagy 3..*

Ezeket a nyíl bármelyik végére írhatjuk, és fontos, hogy melyik oldalra. Ebben a példában az autónak 3..* kereke lehet, egy kerék azonban csak egy autón lehet rajta:

Multiplicitás

A nyilakra azt is ráírhatjuk, hogy a két objektum kapcsolatában melyik objektumnak mi a szerepe, ha nem egyértelmű:

Szerep

Feladat: milyen kapcsolatban áll egymással az autó, a motorkerékpár, az oldalkocsi és a kerék?

  • A motorkerékpárnak része 2 db kerék, ez kompozíció.
  • Az oldalkocsinak 1 db kerék része, ez is kompozíció.
  • A motorkerékpár élettartama néha kötődik az oldalkocsiéhoz (például ha összekapcsolva a Dunába hajtanak), de nem feltétlenül (pl. könnyű róla leszerelni, külön értékesíteni). Ez aggregáció.
  • Az autónak legalább 3 kereke van (pl. Reliant Robin), de lehet akármennyi, az emberi hülyeségnek nem szabhat határt (American Dream).
Szerep

3. Öröklés

Két osztály közötti ősosztály-leszármazott viszonyt üres fejű nyíllal jelöljük, ami az ősosztály felé mutat. Fontos, hogy ez csak az OOP értelemben vett (publikus, is-a) öröklésre vonatkozik, a "privát öröklés" objektumorientált szempontból kompozíciónak számít!

Öröklés

A virtuális öröklés jelölésére az UML-ben nincs szabványos mód, ilyenkor a nyílra szokás ráírni, hogy virtual, esetleg stereotype-ként, két kacsacsőr közé: <<virtual>>. Az örökléssel kapcsolatban semmi további tudnivaló nincs.

Virtuális öröklés

4. Függőség

Akkor mondjuk, hogy A osztály függ B osztálytól, ha a B osztály interfészének a módosítása az A osztályban is módosítást igényelhet.

A fenti relációk – kompozíció, aggregáció, asszociáció, öröklés – mind ilyenek. Az ezeken túli függőségeket jelöljük UML-ben függőségnek. Ilyenek lehetnek például:

  • A valamilyen függvénye paraméterként B objektumot kap
  • A példányosítja B-t
  • A meghívja B statikus függvényét
  • A valamilyen függvénye B objektummal tér vissza

Jelölése a sima, két vonalból álló fejű, szaggatott nyíl:

Függőség

Ezek általában látszanak UML diagramon is, az adattagoknál és a tagfüggvényeknél. Vigyázz, nem minden esetben, például az "A példányosítja B-t" esetben nem!

5. Template osztályok

A template osztályok jobb felső sarkában van egy szaggatott téglalap, amibe a template paramétereket írhatjuk. Ha a template paraméter typename, akkor kész is vagyunk, ha viszont változó a template paraméter, odaírhatjuk a típusát.

Ahol használjuk az osztályt, a nyílra rá kell írnunk, hogy milyen template paraméterekkel, a <<bind>> stereotype használatával.

Template osztály

6. Adattagok, tagfüggvények

A fejezet elején említettük, hogy az osztályok dobozából a tervezés elején lehagyjuk az adattagokat és a tagfüggvényeket, és csak egy téglalapot rajzolunk nekik. A szabványos UML osztálydiagramon egy osztálynak igazából egy három részre osztott téglalap jár: a felső részbe kerül a neve, a középsőbe az adattagjai, az alsóba pedig a tagfüggvényei:

Adattagok

Az első furcsaság, hogy az adattagok neve után, kettősponttal elválasztva következik a típusa. Függvényeknél ugyanez a helyzet: a paraméterlistája után, kettősponttal elválasztva szerepel a visszatérési értéke, és a paraméterek is ugyanígy, fordított sorrendben szerepelnek. A konstruktornak természetesen itt sincs típusa.

Sorrend

A sorok elején lévő karakterek a láthatóságot befolyásolják: kötőjel a private, pluszjel a public, és kettőskereszt a protected. A statikus függvényeket, adattagokat aláhúzással kell jelölni.

Láthatóságok

Az absztrakt osztályok neve dőlt betűvel írandó. A virtuális függvények jelölésére nincs bevett mód, szokás dőlt betűvel írni, vagy a visszatérési érték végére tenni, hogy virtual.

Ez a hiányosság is abból ered, hogy a Java nyelvhez igazodik az UML, ott ugyanis minden függvény alapból virtuális.

Virtuális, absztrakt

Összetett példa: Vonatjegy

Ne felejtsük: az UML csak egy módszert ad arra, hogyan ábrázoljuk az osztályaink közti kapcsolatokat és kommunikációt. Azt viszont nekünk kell eldönteni, hogyan határozzuk meg ezeket. Ez nem egy egzakt folyamat, mindig sok emberi döntésre van szükség, így sok hasonló modell lehet jó. Szerencsére segít minket néhány elv és módszer, amivel el tudunk indulni.

  • Érdemes úgy kezdeni a modellalkotást, hogy nyelvtanilag elemezzük a feladatot vagy specifikációt. A főnevekből lesznek az osztályok, az igékből pedig a tagfüggvények. Természetesen ezzel nem kapunk meg minden osztályt (pl. az absztakt osztályokat), és nem minden főnévből lesz osztály.
  • A folytatásban érdemes követni azt az elvet, hogy próbáljuk a valóságot modellezni. Az osztályok felelőssége és tagfüggvények működése lehetőleg egyezzen azzal, ami a valóságban létezik és történik.
  • Ezután keressünk egyszerűsítéseket: felesleges, esetleg máshonnan átemelhető osztályokat tudunk kihúzni, vagy a modellben máshogy megjeleníteni.

Ezek természetesen nem szükségszerűen ilyen sorrendben követik egymást, sőt, egy-egy ilyet sokszor újra előveszünk, és átgondoljuk még egyszer. Kreatívan kell tudni felhasználni azokat a gondolatokat, amik itt megjelennek.

Jelen leírás célja, hogy bemutassunk néhány fontos gondolatot, ugyanakkor demonstráljuk, hogy a modellek felépülése tudatos döntések eredménye, és a főbb gondolatok logikailag levezethetők.

8. Feladat

Nézzünk meg egy konkrét példát, amin ezeket be tudjuk mutatni. A példafeladat a hivatalos nagyházi-listából származik, így az itt leírtakat a plágiumgyanú elkerülése érdekében lehetőleg ne vegye senki a nagyházija kiindulási alapjának.

Tervezze meg egy vonatjegy eladó rendszer egyszerűsített objektummodelljét, majd valósítsa azt meg! A vonatjegy a feladatban mindig jegyet és helyjegyet jelent együtt. Így egy jegyen minimum a következőket kell feltüntetni:

  • vonatszám, kocsiszám, hely
  • indulási állomás, indulási idő
  • érkezési állomás, érkezési idő

A rendszerrel minimum a következő műveleteket kívánjuk elvégezni:

  • vonatok felvétele
  • jegy kiadása

A rendszer később lehet bővebb funkcionalitású (pl. késések kezelése, vonat törlése, menetrend, stb.), ezért nagyon fontos, hogy jól határozza meg az objektumokat és azok felelősségét.

9. Értelmezés

Gyűjtsük ki az osztályokat (főneveket) a szövegből!

  • vonatjegy
  • vonat
  • kocsi
  • hely
  • állomás
  • idő

Vegyük észre, hogy a feladat néhol pongyolán fogalmaz. A legfontosabb kérdés, amit tisztázni kell, hogy mit jelent az, hogy veszünk egy jegyet.

Mivel a valóságot modellezzük, képzeljük el, mi történik! Ha mi veszünk egy (hely)jegyet Pécs - Budapest között egy vonatra, akkor azt is meg kell adnunk, hogy melyik járatra: 7:14-kor indulóra, vagy 9:14-kor indulóra, esetleg valamelyik későbbire. Ugyanaz a vonat többször is közlekedhet ugyanazon a vonalon, oda-vissza, napjában akár többször is, de amikor jegyet veszünk, akkor csak az adott járatra szól a jegyünk, nem utazhatunk egyetlen jeggyel oda-vissza. Ebből az következik, hogy fel kell vennünk egy új osztályt a járatnak.

A vasúttársaságnak a jegy eladása után arra is figyelnie kell, hogy ugyanazt a helyet nem adhatja el másnak, tehát nyilván kell tartania a foglalásokat az adott járatra. A vonatjegy igazából egy információ és igazolás arról, hogy azt a helyet nekünk foglalták le, tehát a foglalásnak is kell egy új osztály.

10. Definíciók, kapcsolatok

Definiáljuk kicsit pontosabban a fogalmakat, hogy meg tudjuk határozni a felelősségi köröket! Ez elengedhetetlen ahhoz, hogy az osztályok közötti kapcsolatokat kialakítsuk.

  • Az állomásról semmit nem kell tudnunk a nevén kívül. A pontos helye sem érdekes, az utasoknak kell utánajárni.
  • A hely a jegyeladás szempontjából csak egy szám. Az utazást magát nem kell modelleznünk, tehát nem kell figyelembe vennünk az utasokat, stb.
  • Egy kocsiban sok hely van, és van száma, ezen felül semmi teendőnk vele.
  • A vonatnak a feladat szerint száma van, a valóságban inkább neve (pl. IC 802 Tubes). A vonat, azaz fizikai valójában a szerelvény, kocsikból áll.
  • A járat az, amikor egy vonatot elküldünk A-ból B-be egy adott időpontban. Egy járatra minden helyet csak egyszer adhatunk el, így a járatnak nyilván kell tartania a foglalásokat.
  • A foglalás egy bejegyzés egy járatban, hogy a vonatán egy kocsiban lévő egyik hely már nem adható ki.
  • A vonatjegy pedig egy papírdarab, ami igazolja, hogy az adott foglalás a miénk.

Ebből már látszik, hogy mely osztályok melyik másik osztályokat ismerik. Nézzük meg közelebbről, hogy az egyes relációk a három "ismerik" – kompozíció, aggregáció, asszociáció – közül melyikek lehetnek! Ha kicsit máshogy gondolkodunk, ebben komoly eltérések lehetnek, mert ez az erőforrás-kezelést fogja meghatározni.

  • A kocsi helyekből áll: kompozíció.
  • A vonat kocsikból áll: kompozíció.
  • A járat ismeri a vonatát, de nincs hatással az élettartamára (a vasúttársaság felelőssége, hogy működő vonatot küldjenek a járatra): asszociáció.
  • A járat tudja, hogy melyik állomások között közlekedik, de az állomások élettartamára nincs hatással: asszociáció.
  • A járat része az összes foglalás (járat törlésekor a foglalások is törlődnek): kompozíció.
  • A foglalás egy adott járatra, adott kocsira és adott helyre vonatkozik: ezek asszociációk.
  • A foglaláshoz hozzá tartozik, hogy melyik vonatra szól (mert pl. nem minden vonatban van 18-as kocsi), de a foglalás a vonatot el tudja érni a járaton keresztül is.
  • A vonatjegy egy foglalásra hivatkozik: asszociáció.

Ennyiből már tudunk rajzolni egy kezdetleges osztálydiagramot.

class1

Egy fontos dolog azonban hiányzik. Azt mondtuk, hogy a vasúttársaság felelőssége, hogy a járatok létező vonattal, létező állomások között közlekedjenek. Szintén a vasúttársaság feladata nyilvántartani a járatokat, hogy a felhasználó azok közül választhasson, és vehessen rájuk jegyet. Tehát kell még egy osztály a vasúttársaságnak, ami tartalmazza a vonatokat, járatokat és vasútállomásokat.

Ezzel kiegészítve így néz ki az osztálydiagram:

class2

A fenti diagramot kiegészítettük a további, nyilakkal nem jelölt adattagokkal, és a multiplicitással, hogy a későbbi egyszerűsítési lépések jól követhetők legyenek.

class3

11. Egyszerűsítések

Mivel a modellünknek kizárólag a jegyeladásra kell koncentrálnia, néhány osztálynak nincs felelőssége. Ezeket akár el is hagyhatjuk, és a kódban máshogy jelenítjük meg.

  • A feladat nem követeli meg, hogy egy járaton legyenek köztes állomások, így könnyítsük meg a dolgunkat azzal, hogy a vonatok csak a két végállomásnál állnak meg. A valóságban nem egészen így van, de mivel a feladat nem kérte, ezért elegendő ezzel foglalkoznunk. A valóságtól egyébként nem áll messze, pl. a repülőjáratok így közlekednek. Ezzel sokat fog egyszerűsödni a helyfoglalás algoritmusa is.
  • Az állomás számunkra csak egy sztring. Akár helyettesíthetjük is egy sztringgel. Egyszerű, ismert típusokat pedig nem szoktunk a osztálydiagramon feltüntetni, így ezt inkább sima adattagként ábrázoljuk.
  • A hely nem más, mint egy szám, használhatunk helyette inteket is. Sőt, ha feltételezzük, hogy egy kocsiban 1-n-ig vannak számozva a helyek, akkor elég azt tárolnunk, hogy hány hely van a kocsiban.
  • A foglalás a vasúttársaság rendszerében van rögzítve, a vonatjegy így valójában egy foglalás adatai kinyomtatva. Mondhatnánk azt is, hogy a vonatjegy egy pointer a foglalásra, amivel egy egyszerű, konzolos programban semmit nem kell csinálnunk azon kívül, hogy kiírjuk. Így ezt egyszerűen elhagyhatjuk.
class4

12. Műveletek

Keressük meg a feladat szövegében a műveleteket, azaz az igéket!

  • jegyen feltüntetni adatokat: azaz kiírni egy foglalást
  • jegyet kiadni: azaz létrehozni, és kiírni egy foglalást
  • vonatot felvenni: a feladatkiírás nem különbözteti meg a vonatot és a járatot, így valószínűsíthetően arra gondolt, hogy járatot is kell tudni felvenni

További, józan ész elvárásai szerinti funkciókat is meghatározhatunk, ami ahhoz kell, hogy működőképes legyen a program:

  • vonathoz kocsit felvenni: így kezdetben lehet kocsi nélküli a vonat
  • állomást felvenni

Az osztálydiagram kiegészítve ezekkel a műveletekkel:

class5

13. Adatszerkezet

Eltöprenghetünk azon, hogy a kompozíciókat - asszociációkat hogyan jelenítsük meg a kódban, ez azonban közel sem egyszerű.

Az UML-es részben írtak szerint a kompozíciót érték szerinti tartalmazással, az asszociációt pedig sima pointerrel érdemes megvalósítani. Ahol n objektumot kell tartalmazni, a kézenfekvő megoldás az std::vector, amit pont az ilyen esetekre találtak ki. Esetünkben azonban ez több problémával is járna.

Az std::vector egy dinamikus tömb, ami képes dinamikusan "megnyúlni", azaz ha betelik a memóriaterülete, újrafoglalni és az újat használni. Az újrafoglalás során a memóriában máshova kerülhetnek az objektumok. Így ha van egy pointerünk, ami a vektor egyik elemére mutat, akkor az újrafoglalás során kikerülhet alóla a memóriaterület, így a pointer érvénytelenné válik. Ezt hívják invalidálódásnak (angolul invalidation), ahogy az iterátoros fejezetben részletesen kifejtettük. Az érvénytelen pointer neve angolul dangling pointer. A mi esetünkben például vonat hozzáadásánál fordulhat elő (az ábrán pirossal):

invalidal1

Így ha új vonatot szúrunk be, a korábban hozzáadott járatok Vonat-ra mutató pointere invalidálódhat.

Szintén sorszám szerint indexelhető tároló, azaz sorozattároló az std::deque. A deque garantálja, hogy a végeire beszúrásnál nem invalidálódnak a létező elemekre mutató iterátorok (vagy pointerek), azonban ezt két okból sem érdemes használnunk. Egyrészt, mert nem erre való, hanem egy két végű sor (double-ended queue). Másrészt pedig a feladat kiköti, hogy későbbi funkciókra – köztük a vonat törlésére – készen kell állnia a modellnek. Márpedig egy deque közepéről való törlés már sajnos invalidál.

Ez a probléma – mint minden probléma, ahogy a "programozás alaptétele" kimondja – megoldható az indirekció egy új szintjének bevezetésével. A mi esetünkben érdemes a vektorba dinamikusan foglalt elemeket tenni, így az elemek újrafoglalásnál csak a tényleges vonatokra mutató pointer helyét foglaljuk újra, az egyes vonatokat nem:

invalidal2

A dinamikusan foglalt vonatoknál azonban ránk hárulna a dinamikus memóriakezelés feladata, ami a kódot feleslegesen bonyolíthatja. Ennek elkerülése végett érdemes sima pointer helyett valamilyen okos pointert használni, ami megoldja a dinamikus memóriakezelést.

C++11 sarok: std::unique_ptr

Ehhez a C++11-es std::unique_ptr illik, ami a dinamikusan foglalt vonatok élettartamát menedzseli. A konstruktorában dinamikusan foglalt pointert vesz át, amire a destruktorában delete-et hív, és gondoskodik arról, hogy véletlenül ne tudjuk elszivárogtatni a memóriát. Ez az okos pointer egyébként nem másolható, de pl. függvényből érték szerint visszaadható. Ennek a mikéntje a Prog2 kereteibe sajnos nem fér bele. A részletes magyarázathoz egyes C++11 nyelvi elemek ismeretére lenne szükség. A használat szempontjából azonban ez nem lényeges.

Ugyanez a helyzet áll elő, ha egy vonatot másolunk, akkor a foglalásokban lévő Kocsi* mutathat rossz helyre. Ezt úgy tudjuk kikerülni, hogy a foglalások a kocsi számát ismerik (ahogy a helynek is csak a számát tartalmazza, hiszen Hely osztály nincs is).

A vonatokat másolni egyébként valószínűleg nem kell (azonos névvel két különböző vonat amúgy sem létezhet), ahogy járatokat sem. Ráadásul járatokat nem is szabad másolni, ott ugyanis a Foglalas objektumoknak van pointere a tartalmazó Jarat-okra. Így ha egy járatot átmásolunk, majd a régit megszüntetjük, a benne lévő foglalások Jarat pointerei mind érvénytelenné válnának.

Ez a probléma kikerülhető úgy, hogy a Foglalas-ok nem ismerik a Jarat-okat, mert egyedül a kiírás miatt kell. A kiírást pedig úgyis a Jarat-on keresztül intézzük, tehát ha nincs más ilyen műveletre szükség, és a foglalásokra nincs máshol nyilvántartás, ezt elhagyhatjuk.

A Jarat-ok tárolásánál érdemes megfontolnunk azt, hogy a felhasználó a járatok között valamilyen szempont (idő, állomás) szerint keresni szeretne. Ehhez kapóra jönne egy rendezett tároló, ami a keresést egyszerűsítené és gyorsítaná. Érdemes lenne az indulási idő szerint rendezni, hiszen a felhasználónak azt úgyis meg kell adnia, és úgy könnyebb pl. átszállásos útvonalakat keresni.

Ilyen, valamilyen kulcs szerint rendezett tároló az std::map (és az std::multimap), amelynek az alapszintű használatát az STL-es fejezet mutatja be. Az STL-es extra írásban is olvashattok róla. A számunkra legfontosabb tulajdonsága, hogy a map-be való beszúrás nem invalidálja a korábban beszúrt elemeket. Ezt is használhatnánk dinamikusan foglalt járatok helyett.

Ezek olyan tervezési megfontolások, amihez hasonlókkal ki-ki a saját nagyházijában szembesülni fog. Elképzeltünk egy lehetséges "felállást", az ebből kiinduló megoldás kódban nagyjából így jelenik meg:

class Kocsi
{
    int szam;
    int max_hely;
};

class Vonat
{
    std::string nev;
    std::vector<Kocsi> kocsik;
};

class Foglalas
{
    int kocsi;
    int hely;
};

class Jarat
{
    Vonat * vonat;
    std::vector<Foglalas> foglalasok;

    DatumIdo indulasi_ido;
    DatumIdo erkezesi_ido;
    std::string indulo_allomas;
    std::string erkezo_allomas;
};

class MAV
{
    std::vector<std::string> allomasok;
    std::vector<std::unique_ptr<Vonat>> vonatok;
    std::multimap<DatumIdo, Jarat> jaratok; // indulasi_ido szerint kulcsolva
};

Javasoljuk az STL tárolók használatát az ilyen esetekre. Ha a feladat és a laborvezető nem engedélyezi, akkor is érdemes a tervezésnél ezeket figyelembe venni, majd az implementáció során a felhasználandó STL tárolóhoz hasonlót magunknak írni. Ezekről a tárolókról a következő fejezetben lesz részletesebben szó.