Miskolci Egyetem Általános Informatikai Tanszék
Osztály tervezési szempontok és C++ implementációs ajánlások Oktatási segédlet levelező hallgatók részére
Összeállította: Ficsor Lajos
Miskolc 2005
1
Tartalomjegyzék
1. Bevezetés ................................................................................................................. 1 2. Az osztály fogalma .................................................................................................. 1 3. Osztályok közötti kapcsolatok ................................................................................. 2 3.1 Az általánosítás / pontosítás kapcsolat (is-a) ..................................................... 2 3.2. A tartalmazás kapcsolat (has-a) ........................................................................ 3 3.2.1. Az aggregáció implemetálása .................................................................... 3 3.2.2. A kompozíció implementálása................................................................... 4 3.3. A használat kapcsolat (use)............................................................................... 4 4. Az osztály interface.................................................................................................. 5 4.1. Az osztály interface fogalma ............................................................................ 5 4.2. A jól tervezett osztály interface ........................................................................ 5 4.3. Az osztály interface részei ................................................................................ 6 4.4 Az elérési függvények........................................................................................ 6 5. Irodalomjegyzék ...................................................................................................... 7
i
1. Bevezetés Egy általános célú programozási nyelv szabályrendszere számos konstrukciót megenged. Egy szintaktikailag helyes (tehát a fordító által elfogadott és technikailag működőképes) program azonban tartalmazhat olyan részleteket, amelyek praktikus szempontok miatt kerülendők. Ezeket (valamint a jól használható konstrukciókat is) a programozók több évtizedes tapasztalatai jelölik ki. Egy szintaktikailag helyes és a gyakorlati tapasztalatok alapján kerülendőnek tartott konstrukciókat sem tartalmazó program már technikai szmepontból megfelelő, azonban még nem biztos, hogy értelmes feladatot old meg. A software fejlesztés feladata viszont épp egy adott, a gyakorlatban felmerült probléma megoldása. Ezért egy nyelv szintaktikai szabályainak ismerete csak szükséges, de nem elégséges feltétele annak, hogy a gyakorlati életben hasznot hajtó alkalmazást készítsünk. Az objektum orientált programozás a progamot a valóság valamely szeletének modelljeként tekinti. Ezért egy osztály megtervezése és implementálása olyan meggondolásokat is igényel, amelyek nem egyszerűen csak a C++ nyelv szabályainak alkalmazását jelentik. Ebben a részben tehát nem szintaktikai szabályokat fogunk ismertetni, hanem arra próbálunk példákat adni, hogy hogyan lehet jól használható, és az objektum orientált szemléletmódot tükröző osztályokat tervezni. Azaz: hogyan érdemes a C++ nyelv lehetőségeit használni.
2. Az osztály fogalma Az osztály programozástechnikai szempontból egy típus. Programtervezési szempontból az osztály a valóság valamely fogalmának modellje. Az objektum orientált programokban alapvetően kétféle osztályokat használunk: •
követlenül az alkalamazási terület (application domain) fogalmait modellező osztályok
•
a feladat megoldásához technikailag szükséges implementációs osztályok.
Az alkalmazási terület osztályai modellezhetnek •
felhasználói fogalmakat (például teherautó),
•
felhasználói fogalmak általánosításait (például jármű).
Az implementációs osztályok lehetnek az alábbiak modelljei: •
hardware / software erőforrások (például memória, i/o stb.)
1
•
más osztályok implementálásához szükséges osztályok (például listák, vermek)
•
beépített adattípusok és vezérlési szerkezetek
3. Osztályok közötti kapcsolatok Az osztályok tervezése során foglalkozni kell az egyes osztályok közötti kapcsolatokkal. Az osztályok között a modellezés szempontjából különböző jellegű logikai kapcsolatok lehetnek (Általánosítás-pontosítás, tartalmazás, használat stb). Ugyanakkor a C++ nyelv biztosít olyan nyelvi szerkezeteket, amelyek implementációs kapcsolatokat teremtenek az osztályok között. Az osztály implementációja során az egyik feladat a logikai kapcsolatok leképezése ezekre a nyelvi szerkezetekre.
3.1 Az általánosítás / pontosítás kapcsolat (is-a) Implementációja a public leszármaztatás. Ez a kapcsolat azt jelenti, hogy egy leszármazott objektum minden szempontból úgy viselkedik, mint egy ős objektum, (azaz egy leszármazott objektum egyben mindig ős objektum is), de ez fordítva nem igaz. Egyszerű példa: class CSzemely { … } class CHallgato : public CSzemely { … } void sortIszik(const CSzemely& sz); void feladatotBead(cont CHallgato& h); Ezután CHallgato nagypista; CSzemely kispista; sortIszik(kispista); // OK // OK, mert egy Hallgato egyben Szemely is sortIszik(nagypista) // Hibás, mert egy Szemely nem biztos, hogy Hallgato is feladatotBead(kispista); Vigyázni kell azonban, mert a természetes nyelv nem mindig fogalmaz pontosan. Erre egy gyakran emlegetett tanpélda: class CMadar { public: void repul(void); 2
} class CStrucc : public CMadar { … } Ebben az esetben a CStrucc osztály örökli a repülés képességét a CMadar osztálytól. A félreértést nyilván az okozza, hogy a "A madár tud repülni" mondat nem azt jelenti, hogy "Minden madár tud repülni", hanem azt, hogy "A madarak általában tudnak repülni". Egy lehetséges javítása a fenti példának az alábbi: class CMadar { public: CKaja MitEszik(void); Bool TudRepulni(void); } class Cstrucc : public CMadar {…} Ebben az esetben a CStrucc osztály csak a repülés lehetőségét örökli a CMadar osztálytól, ami közelebb van a valóságos viszonyokhoz. Egy talán pontosabb javítás: class CMadar { public: CKaja MitEszik( void ) ; }; class CGyalogMadar : public CMadar { … }; class CRendesMadar : public CMadar { public : void Repul( void ) ; }; class CStrucc : public CGyalogMadar{ }; class CSas : public CRendesMadar{ }; Ez a megoldás pontosabban tükrözi azt a tényt, hogy vannak repülni tudó és repülésre képtelen madarak is.
3.2. A tartalmazás kapcsolat (has-a) Kétféle tartalmazás jellegű kapcsolatot különböztethetünk meg: •
aggregáció: a rész az egészhez tartozik, de önállóan is létező entitás
•
kompozíció: a rész önmagában nem létezhet, csak valaminek a részeként
3.2.1. Az aggregáció implemetálása Lehetséges megoldások:
3
•
a tag objektum pointere vagy referenciája a tartalmazó osztályban Hátránya, hogy szoros a kapcsolat a tartalmazó és a tartalmazott objektum között. Biztosítani kell a konzisztenciát, hiszen például a tartalmazott objektum megszűnése a rá vonatkozó pointer vagy referencia érvénytelenné válását jelenti.
•
A tartalmazott private leszármaztatása a tartalmazó osztályból Ennek a megoldásnak a hátránya az, hogy a tartalmazott önálló voltát csak osztály szinten fejezi ki.
3.2.2. A kompozíció implementálása Lehetséges megoldások: •
tag objektum
•
a tartalmazott osztályban lokális osztálydefiníció a tartalmazott számára
3.3. A használat kapcsolat (use) Osztályok az alábbi módokon használhatják egymást: •
CX használja a CY nevet (feltételezi, hogy CY deklarált a használat helyén) o adattag típusaként o tagfüggvény visszatérési értékének vagy paramétereinek típusaként
•
CX használja a CY osztályt (láthatósági szabályok alapján) o meghívja a CY egy tagfüggvényét o írja/olvassa a CY egy adattagját
•
CX létrehoz egy CY típusú objektumot o statikus definíció o dinamikus definíció
•
CX veszi CY méretét
A használat kapcsolat implementálásának alapvető problémája, hogy csak közvetett nyelvi eszközök jelzik az ilyen kapcsolatokat
4
Korlátozó tényezők: •
azonosítók lexikális érvényességi tartománya
•
osztály tagjainak láthatósági attribútumai
4. Az osztály interface Az osztályokat úgy kell megtervezni, hogy azokat az interface-ük segítségével lehessen használni, anélkül, hogy az adattagok implementációjáról bármit is kellene tudni. Egy osztály megtervezése során gondolni kell arra, hogy azt bázisosztályként esetleg most még gondolatban sem létező osztályok is fogják használni. Ezért törekedni kell arra, hogy az esetleges leszármazott osztályokról csak a legszükségesebbeket tételezzük fel, és hogy egy leszármazott osztály tervezése során ne legyen szükség a bázisosztály implementációs részleteinek ismeretére (hiszen ez nem is biztos, hogy rendelkezésre áll).
4.1. Az osztály interface fogalma Egy osztály interface-e szűkebb értelemben a public tagfüggvényeinek összessége. Ezt kell ismernie az osztály használójának Tágabb értelemben ide számíthatjuk C++ programok esetén a friend függvényeket és osztályokat is. Ezt a lehetőséget azonban csak feltétlenül szükséges esetben használjuk, mert ellentmond az információ rejtés alapelvének A leszármazott osztályok számára még az osztály interface-ét képezik a protected függvények és adattagok is.Ezzel is mértékletesen kell bánni, mert implementációs függést hoz létre az ős és a leszármazott osztály között. 4.2. A jól tervezett osztály interface A jó interface az alábbi három alapvető követelménynek kell megfeleljen: •
Legyen teljes, azaz minden olyan funkciót valósítson meg, amely az adott osztálytól elvárható (függetlenül attól, hogy a jelen alkalmazás során szükségesnek látszik-e). Az osztályt tehát funkció-orientáltan és ne az adott felhasználás szempontjából vizsgáljuk. Ezzel egyrészt újrafelhasználható elemeket alkotunk, másrészt nem korlátozzuk magunkat hiányzó funkciókkal a program további tervezése - készítése során.
•
Legyen minimális, azaz ne legyen az interface része olyan funkció, amely a külső felhasználó számára érdektelen. Az adott osztály belső használatára szánt függvények legyenek mindig private (esetleg a leszármazott osztályokra is gondolva protected) módúak. Ezzel egyben megkönnyítjük az osztály esetleges áttervezését is: az adattagok és a segédfüggvények implementációjának módosítása a felhasználó program egyetlen pontján sem jelent változást.
5
•
Legyen kezelhető méretű. Ha a teljesség követelményének betartása túlságosan sok funkció megvalósítását eredményezi, az az osztály használójának munkáját nehezíti. (Egy programozó általában legfeljebb néhány tíz tagfüggvényt tart kezelhetőnek egy osztályon belül.) A sok funkció között nagyobb valószínűséggel lesznek hasonlóak, ami pedig könnyen vezet az egyes funkciók tényleges feladatának félreértéséhez. A terjedelmes interface mindig tervezési hibára utal, a két leggyakoribb ok: − olyan funkciókat is az interface részének ítéltünk, amelyek inkább belső funkciók − az osztály határait nem jól állapítottuk meg, és túl sok feladatot akarunk rábízni. A helyes architektúra kialakítása érdekében az eredetileg tervezett osztályt több osztályra kell bontani, és ezek között leszármaztatással vagy más mechanizmussal megteremteni a kapcsolatot.
4.3. Az osztály interface részei A public tagfüggvényeket funkciójuk szerint az alábbi csoportokba sorolhatjuk: •
kezelő függvények Idetartoznak az inicializáló, értékadó, konverziós stb. függvények, amelyeket sokszor nem is direkt módon a programozó aktivizál, hanem a fordítóprogram implicite hív meg.
•
elérési függvények Az adattagok értékének elérésére vagy azok értékének módosítására.
•
munkavégző függvények Az osztály lényegi funkcióit aktivizáló függvények.
Míg a munkavégző függvények természetesen osztály-specifikusak, az első két csoportba tartozó függvények a különböző osztályok esetén is számos hasonlóságot mutatnak, előállításuk részben automatizálható is.
4.4 Az elérési függvények Az elérési függvények általában egyszerűek, implementálásuk értelemszerű. Segítségükkel az információ rejtés elvének megsértése nélkül tesszük használhatóvá az osztály objektumainak adattagjait. A kezelő fügvények megírása többletmunkát jelent, használatuk azonban a következő előnyöket biztosítja: •
Szabályozhatjuk azon adatok körét, amelyekhez egyáltalán hozzáférést biztosítunk az osztály objektumait használó program számára.
6
•
Szabályozhatjuk az egyes adattagok használatának módját (csak az értékét felhasználni, csak beállítani, vagy mindkét műveletre van-e módja a programnak).
•
A függvények elrejtik a programozó elől az adattagok belső ábrázolásának implementációs részleteit, ezért ha azt megváltoztatjuk, az osztályt használó programban nem kell változtatni, ha ugyanolyan formában adjuk vissza a értéket. Például ha egy egész értéket ad vissza az alérési függvény, a felhasználó programozó számára lényegtelen, hogy az ténylegesen egy egész típusú adattag értéke, vagy esetleg egy bonyolult adatbázis lekérdezés eredménye. Természetesen, ha az adat tárlosi módja jelentősen eltér az elérési függvény által mutatott formátumtól, a megfelelő transzformációt biztosító függvény implementálása komoly programozói munkát is jelenthet.
•
A kezelő függvényeket sok esetben a fejlesztő eszközök automatikusan generálni képesek, csökkentve ezzel az osztályt implementáló programozó munkáját.
5. Irodalomjegyzék Az alább felsorolt könyvekben – amelyeket a jelen segédlet készítése során magam is felhasználtam – a témára vonatkozó számos további információ található. Bjarne Stroustrup: A C++ progamozási nyelv Kiskapu Kiadó/ Addison-Wesley, Budapest, 2001. Scott Meyers: Hatékony C++ (50 jó tanács programjaink és programterveink javítására) Scolar Kiadó, Budapest, 2003 Bruce Eckel: Thinking In C++, 2nd edition, Volume 1 Prentice Hall , 2000 Elérhető elektronikus formában a szerző honlapjáról (http://mindview.net), illetve a tanszéki szerveren található tükrözésről: http://www.iit.uni-miskolc.hu/BruceEckel.
7