Tartalmi kivonat
Szofvertechnológia Tilly Károly, Kiss István 1998 Tartalom 1. Bevezetés 5 1.1 A szoftverrendszerek bonyolultságáról 6 1.11 A megoldandó feladat bonyolultsága 6 1.12 A fejlesztési folyamat kézben tartásának nehézségei 7 1.13 A szoftvereszközök kínálta rugalmasság 7 1.14 A diszkrét rendszerek viselkedésének leírásából fakadó nehézségek 8 1.2 Szoftver életciklus modellek 8 2. Implementáció 12 2.1 A szoftverrendszerek osztályai 12 2.2 Eseményvezérelt rendszerek 14 2.21 Alapfogalmak 14 2.22 Konkurens- és valós idejű ütemezők 15 2.23 Folyamatok leírásának eszközei 16 2.24 Kommunikáció, szinkronizáció és kölcsönös kizárás 17 2.3 Deklaratív rendszerek 19 2.31 Problémák és problématerek 20 2.32 A következtetési algoritmus egyes lépéseinek megvalósítási módszerei 22 2.34 Keresési stratégiák 24 2.4 Procedurális fejlesztőeszközök 30 2.41 A procedurális programozási nyelvek fejlődése 30 2.42
Objektumorientált programozás 31 2.43 Java 44 2.5 Deklaratív fejlesztőeszközök 69 2.51 Deklaratív programozási nyelvek 69 2.52 4GL fejlesztőeszközök 96 3. Objektumorientált elemzés és tervezés 108 3.1 Dekompozíció, hierarchia, absztrakció 109 3.2 Az észszerű tervezési folyamat 110 3.3 A strukturált elemzési és tervezési módszerek csoportosítása 111 3.31 Strukturált elemzés és tervezés (SA/SD) 111 3.32 A Jackson módszer (JSD) 112 3.4 A folyamat: Objectory 112 3.41 Két dimenzió 113 3.42 A folyamat statikus szerkezete 114 3.43 Az életciklus szerkezete 115 3.44 A folyamat ábrázolása 117 3.5 Jelrendszer: az UML nyelv alapjai 131 1. Bevezetés Mindenekelőtt azt a kérdést kell tisztáznunk, mit is jelent számunkra az a szó, hogy szoftvertechnológia. A technológia általánosságban és a mi esetünkben is sokrétű fogalom. A technológia olyan műveletek sorozata, mely egy kezdeti állapotból, adott erőforrások (pl.
anyagok, idő, pénz, munkaerő, szakértelem, berendezések) felhasználásával egy kívánt végállapot eléréséhez vezet, mégpedig pontosan megismételhető módon. Korunkban a technológia egyre meghatározóbb szerepet játszik, mely eldönti, hogy mindenki által áhított, a versenytársakat leköröző "high tech" terméket állítunk elő, vagy éppenséggel érdektelen, idejétmúlta kacatot. A szoftvertechnológia tehát a mi megközelítésünkben magába foglalja a nagybonyolultságú számítógépes programrendszerek létrehozására alkalmas paradigmákat, módszereket és eszközöket. Bár a nagy szoftvergyártók tapasztalatai és a mértékadó programtervezők és fejlesztők egybehangzó állítása szerint ezek nélkül a módszerek nélkül nem képzelhető el sikeres, működő szofverrendszerek megvalósítása, a helyzet itt látszólag mégsem olyan egyértelmű, mint a hagyományos ipari technológiák esetében. Vegyünk egy egyszerű
példát. Egy építési vállalkozó azt a feladatot kapja, hogy cserélje ki egy ház tetején a cserepeket, majd újítsa föl a bádogozást és az esőcsatornákat. Biztosra vehetjük, hogy a megbízó páros lábbal és hihetetlen sebességgel kirúgja a vállalkozót, ha az a ház kertjében mély gödröket kezd ásni agyag után kutatva, hogy abból a helyben felépített kemencében cserepeket égessen, vagy megpróbál helyben ónból és ólomból bádoglemezeket önteni, és csatornaidomokat hajlítani. Persze ehelyett a vállalkozó ezeket az elemeket készen veszi, és pusztán csak felhasználja az adott feladat megvalósításának érdekében. Így jár mindenki a lehető legjobban Ha a fenti példát megpróbáljuk egy szoftverfejlesztési feladat nyelvére lefordítani, koránt sem lehetetlen, hogy nem megfelelő fejlesztőeszköz vagy éppen a tervezés során hibás absztrakciós szintek kiválasztása miatt egy mély gödör alján találjuk magunkat, ahol éppen
agyag után kutatunk, miközben a tetőt és a cserepeket talán már régen el is felejtettük, és talán minderre csak akkor döbbenünk rá, amikor a megbízó a kitűzött határidők és előirányzott költségek sokszoros túllépése után bennünket rúg ki páros lábbal. Mindez annak a közvetkezménye, hogy minden szoftverrendszer végső soron algoritmusokból és adatstruktúrákból áll, melyekről köztudott, hogy kezelésükre a legáltalánosabb és legegyszerűbb számítógép konstrukció, a Turing gép is alkalmas. Így tehát, míg az építési vállalkozó számára nyilvánvaló, hogy nincsen eszköze a cserépkészítéshez, mi szoftverfejlesztők látszólag bármire képesek vagyunk, ezért különösen fontos a tervezés és implementáció során a helyes absztrakciós szint, helyes tervezési és implementálási módszer és a legmegfelelőbb eszközök kiválasztása. Ez utóbbi feladat nem is olyan könnyű, mint amilyennek első pillantásra
látszik, mert ami egy szoftvermérnököt megkülönböztet a legtöbb hagyományos szakma szakértőitől (így például egy bádogostól vagy tetőfedőtől), az éppen a rendelkezésére álló eszközök hihetetlen sokfélesége, bonyolultsága, és ráadásul folyamatosan változó volta. Ha egy tetőfedő megtanulta, hogy a hódfarkú cserép meredek tetőfelületre való felszögelése közben hogyan tartsa kezében a cserepet, a kalapácsot és a szöget (két kéz, három tárgy, és lehet, hogy közben még kapaszkodni is kell - a szerzők minden esetre a maguk részéről epszilon idő alatt kalapácsostul és cserepestül leesnének), a tetőfedő számíthat rá, hogy az adott műveletet egész életében ugyanúgy tudja majd végrehajtani. Ezzel szemben a szoftverfejlesztő kalapácsa, még ha csak a legalapvetőbb eszközöket, a programozási nyelveket tekintjük is, évről évre megújul, és mindezt olyan szinten teszi, hogy az eszköz előző változatának
használatát olykor szinte el is felejthetjük. Ezek után azt gondoljuk, már érzékelhető, miért is fontos az embernek a szoftverfejlesztés módszereinek és eszközeinek világában jól kiismernie magát, ha sikeres rendszerfejlesztő vagy rendszertervező akar lenni. Ahogy említettük, a világ e téren rendkívül gyorsan változik, így az igazságok, amelyeket ebben a jegyzetben leírunk, a világ jelenlegi, ezredforduló környéki állapotát tükrözik. Így aztán nem gondoljuk azt sem, hogy jegyzetünk minden szava örökérvényű alapigazság lenne (az ilyennek kikiáltott könyvek amúgyis borzasztó veszélyesek), törekvésünk mindössze annyi, hogy összefoglaljuk azokat az elveket, eszközöket és módszereket, amelyekről a szoftverfejlesztés terén ezidáig bebizonyosodott, hogy életképesek és használhatók. Ha lesznek a jövőben ezeknél jobbak, akkor majd a jegyzetünk is azokról fog szólni. 1.1 A szoftverrendszerek bonyolultságáról A
jelenleg működő szoftverrendszerek közismert módon az emberkéz alkotta legbonyolultabb rendszerek közé tartoznak. Hogy a tervezésük során alkalmazott módszerek és eljárások megalapozottságát jobban alátámasszuk, a következőkben megvizsgáljuk azokat az általános tulajdonságokat, amelyek a komplex rendszereket, így természetesen a szoftverrendszereket is jellemzik. 1. Hierarchikus felépítés: A komplex rendszerek alrendszerekből épülnek föl, emelyek újabb alrendszereket tartalmaznak. Ez a felépítés sok szinten keresztül folytatódhat, egészen addig, amíg az elemi komponensek szintjéig el nem jutunk. 2. Az elemi komponensek halmaza absztrakciófüggő: Hogy mit tekintünk elemi komponensnek, nagymértékben a nézőpontunktól és céljainktól függ. 3. A komponenseken belüli kapcsolatok erősebbek, mint az azonos szintű komponensek közötti kapcsolatok. Ennek alapján elkülöníthető egymástól a komponensek közötti laza és ritka
kapcsolat a komponens belsejében létező gyakori és szoros kapcsolattól. 4. A hierarchikus rendszerek általában néhány alapvető komponens változatos kombinációjából és kapcsolatából épülnek föl. 5. Minden működő komplex rendszerrel kapcsolatban megállapítható, hogy egy egyszerűbb, működő rendszerből fejlődött ki: Más szóval egy olyan komplex rendszer, amit előzmények nélkül, a nulláról fejlesztenek, sohasem lesz működőképes, amíg be nem jártuk az egyszerűbb, működő rendszerek létrehozásának lépcsőfokait. Ha jól megfigyeljük, a mérnöki tervezésben használatos sikeres módszerek mindegyike visszavezethető ezen tulajdonságok egyikének vagy másikának kihasználására, és ez alól a szoftverrendszerek tervezése és létrehozása sem kivétel. A komplex szoftverrendszerekkel kapcsolatos problémák a következő négy alapvető okra vezethetők vissza: - a megoldandó feladat bonyolultsága; - a fejlesztési folyamat
kézben tartásának nehézségei; - a szoftvereszközök kínálta rugalmasság; - a diszkrét rendszerek viselkedésének leírásából fakadó nehézségek. A következő pontokban ezeket a problémákat részletezzük. 1.11 A megoldandó feladat bonyolultsága A gyakorlatban megvalósítandó szoftverrendszerekkel szemben támasztott kívánalmak száma rendkívül nagy, a megkívánt funkcionalitások bonyolultak, és az egyes részfeladatok megvalósítása során gyakran egymásnak ellentmondó igényeket kell kielégítenünk. Mindezt tovább bonyolítják a szoftverrendszerekkel szemben támasztott olyan általános követelmények, mint az egyszerű használhatóság, nagy teljesítmény, elfogadható költségek, megbízhatóság, hibatűrés és még sorolhatnánk. Mindemellett általában magának a feledat specifikációjának a megértése is komoly nehézségeket okozhat, mert a megrendelő a megoldandó feladat nyelvén beszél (pl. harangkemencés hőkezelési
technológiát üzemeltet), a fejlesztő pedig a számítógépek és szoftvereszközök világában mozog. A helyzetet tovább bonyolítja, hogy a feladat megfogalmazásakor a megrendelő gyakran maga sem tudja pontosan, mire is lenne szüksége. Mivel a fejlesztőnek időbe telik, míg kirajzolódnak előtte a megoldandó feladat egyes elemei, gyakran előfordul, hogy lényeges, tisztázatlan részletkérdésekre csak akkor derül fény, amikor a fejlesztés már előrehaladott stádiumban van, mindez a készülő rendszer kisebb-nagyobb átalakításához, és persze nagyobb fejlesztési időhöz és többletköltségekhez vezet. Megfigyelhető, hogy a fenti okokból, vagy éppen tervszerű döntések eredményeként az elkészült nagy szoftverrendszerek időben változnak. Ezt a műveletet karbantartásnak nevezik, ha felbukkanó hibákat szüntetünk meg; fejlesztésnek nevezik, ha a módosítás egy újonnan fölmerült igényt elégít ki; konzerválásnak, ha pusztán csak
azért dolgozunk, hogy egy régi, idejétmúlt, de valamilyen oknál fogva fontos szoftverkomponens a változó környezet ellenére is üzemképes maradjon. 1.12 A fejlesztési folyamat kézben tartásának nehézségei A komplex szoftverrendszerek belső bonyolultságát a fejlesztőknek a felhasználó elől és egymás elől is el kell rejteniük. Ha ez nem történik meg, a szoftver kezelhetetlenül bonyolulttá válik, és a kezelhető feladatok komplexitása csökken. Hosszú program írása csak a kezdő számára tűnik csábító tettnek, a profi tudja, hogy ami igazán számít, az a lehető legrövidebb idő alatt a lehető legmegbízhatóbb megoldás létrehozása, és ez rövidségre ösztönöz. Ezen a téren különös hangsúlyt kap a fejlesztés során az egyszerűségre való törekvés, a komplex részfeladatok megoldására alkalmas előregyártott elemek, az ún. keretek (framework) használata, a modularizás és az újrafelhasználhatóságra való törekvés.
Az említett alapelveket követve lehetővé válik a feladat szétosztása több fejlesztő között (manapság nem ritkák az olyan szoftver projektek, amelyeken egyidőben több száz vagy esetleg ennél is több fejlesztő dolgozik), mindez pedig kulcsfontosságú, ugyanis egy komplex szoftverrendszer kidolgozására egyetlen ember manapság már nem csak azért nem képes, mert nem bírja szusszal, hanem egész egyszerűen nem létezik olyan szoftverfejlesztő, aki egy komplex rendszer minden részrendszerét képes lenne megírni. Gondoljuk csak el, hogy egy komplex ipari mérésadatgyűjtő rendszerben az alacsonyszintű eszközmeghajtóktól kezdve, a hálózati kommunikációs alrendszeren, a relációs adatbáziskezelőkön és grafikus megjelenítő eszközökön át az intelligens diagnosztikai és tanácsadó rendszerekig mindenféle részrendszer megtalálható, melyek megvalósítása a lehető legkülönbözőbb programozói szaktudást, gondolkodásmódot és
hozzáállást igényli. Másrészt a gyakorlatban azt tapasztaljuk, hogy a szaktudás ízlés dolga: aki verhetetlen drivereket ír, az legtöbbször nemigen jeleskedig a magasszintű tervező eszközök használatában, viszont aki kiválóan ért a CASE eszközökhöz és módszertanokhoz, többnyire beismeri, hogy az alacsonyabb szinteken már nem mozog olyan magabiztosan. De erre szerencsére nincs is szükség, mert a lényeg éppen a fejlesztők együttműködése. 1.13 A szoftvereszközök kínálta rugalmasság A problémát lényegében körüljártuk a bevezetőben. E szerint nagyon fontos, hogy megtaláljuk azt az absztrakciós szintet és azokat az eszközöket, amelyekkel a megoldandó feladat megfelelő módon kezelhető. A túl alacsony szintű absztrakció és az ehhez illeszkedő eszközök túlságosan apró lépésekből rakják össze a megoldást, ami így persze sokkal bonyolultabb és hosszadalmasabb lesz. A túl magas szintű absztrakció és az ezt támogató
eszközök nagy valószínűséggel elfednek olyan lehetőségeket, amelyekre pedig a megoldás során alapvető szükségünk lenne. A bevezetőben emlegetett tetőfedő példánkhoz még csak annyit, hogy a szoftveripar alapvető gondja a nagymúltú iparágakhoz képest, hogy a szabványosítás gyerekcipőben jár - bár a szükségességével mindenki tisztában van, és példákat is találunk rá. Így aztán helyenként - akár akarjuk, akár nem minden jószándékunk ellenére kénytelenek leszünk nekiállni cserepet fabrikálni Ettől van az, hogy a szoftverfejlesztés mind a mai napig borzasztó munkaigényes iparág maradt. 1.14 A diszkrét rendszerek viselkedésének leírásából fakadó nehézségek A szoftverrendszerek diszkrét állapotú rendszerek, melyek pillanatnyi viselkedését a bennük működő algoritmusok, és számtalan változó (adatelem) értéke határozza meg. Ilyen változók bonyolultabb alkalmazásokban ezrével fordulnak elő, és ezek
határozzák meg az alkalmazás mindenkori állapotát. Ebből következően a bonyolult szoftverrendszerek viselkedése óriási állapotterekkel, és az ezek fölött értelmezett állapotgráfokkal írható le. Az állapottér elemeinek és az állapotgráf csomópontjainak száma gigantikus (tipikusan a változók számának és a változók lehetséges értékeinek exponenciális függvénye). Ebből az következik, hogy a nagy szoftverrendszerek tesztelése igen nehéz feladat (hiszen az állapotgráf teljes bejárását igényelné, ami gyakorlatilag lehetetlen), és a tesztelés részlegessége olyan viselkedésekhez vezethet, amely nem következik az aktuális állapotból, és előre megjósolhatatlan. A diszkrét rendszerek ellenpárjai a folytonos rendszerek. Így például, ha eldobunk egy követ, számíthatunk rá, hogy az egyenes vagy ferde hajítás mozgáspályáját leíró egyenletek szerint fog mozogni, majd végül a földre hull. Ha ugyanerre a feladatra
szimulációs programot írunk, de a kódban ezt-azt elvétünk, könnyen előfordulhat például, hogy a kő egy darabig az elvárt módon mozog, majd ahelyett, hogy leesne, elindul függőlegesen felfelé. Az ilyen fajta rendellenes viselkedések megjósolására sajnálatos módon nincsen semmiféle eszközünk. 1.2 Szoftver életciklus modellek Az előzőekben vázolt problémákon a gyakorlatban csak úgy lehet segíteni, ha megpróbáljuk a szoftverfejlesztési folyamat során szükségessé váló esetleges (tehát nem jól definiált, főként intuíción alapuló) műveletek számát a minimálisra csökkenteni. Ennek a törekvésnek az eredményeként ún. szoftverfejlesztési módszertanok alakíthatók ki, melyek a technológia reprodukálhatóságát célozzák megvalósítani. Elemzés (mit?) Tervezés (hogyan?) Implementáció Üzemeltetés A módszertanok azon a nyilvánvaló tényen alapulnak, hogy a szoftverrendszerek élete több, 11. ábra: A klasszikus vízesés
szoftver világosan elkülöníthető szakaszra bontható. Mivel életciklus modell ezeknek a szakaszoknak az elkülönítése és jellemzése bizonyos mértékig szemlélet dolga, és a szakirodalom is többféle osztályozást használ, mi a kezdeti vizsgálódásaink alapjául a legközismertebb életciklus modellt, az 1. ábrán látható ún vízesés modellt használjuk Konkrét módszertanokban az életciklus modell egyes lépései rendkívül aprólékosan kifejthetők, és így számos további lépésre bonthatók, a kérdést azonban ilyen részletességgel csak a 3. fejezetben vizsgáljuk A vízesés modell alapvető elemei a következők: - Elemzés: Az elemzési lépés célja, a fejlesztésben résztvevők által elfogadott szabályok szerint, a megrendelővel karöltve kidolgozni a megoldandó feladat precíz specifikációját, amelynek alapján a későbbi lépések során a kívánt funkciók megvalósíthatók. Ennek a lépésnek az eredményeként választ kell
kapni olyan kérdésekre is, hogy a megoldandó feladathoz hány emberre, mennyi időre, és végső soron mennyi pénzre lesz szükség. - Tervezés: Ennek a lépésnek a során meg kell konstruálni a megvalósítandó rendszer architektúráját. Ki kell választani az implementációs eszközöket, végig kell gondolni, milyen módon dekomponálható a feladat, hogyan osztható részekre, és a feladatokat ki kell adni az erre alkalmas fejlesztőknek. - Implementáció: Az elkészített tervek alapján az egyes fejlesztők létrehozzák és letesztelik a rájuk kirótt részrendszereket, majd a részekből összerakják az egész rendszert. Ez utóbbi lépést rendszerintegrálásnak nevezik. - Üzemeltetés: Ennek a fázisnak első lépése a rendszer átadása, melyet aztán a megrendelő használatba vesz. A használat során megjelenő esetleges hibákat ki kell javítani (karbantartás), illetve az üzemeltetés során a felhasználók oldaláról fölmerülő - nem
feltétlenül hibákhoz kötődő problémákat kezelni kell (támogatás). A jegyzet a fenti szoftver életciklus modell analízis, tervezés és megvalósítás lépéseivel foglalkozik, míg az üzemeltetési lépést férfiatlanul elhanyagolja. Ennek magyarázata, hogy az első három lépés végrehajtására különböző szintű, általánosan tárgyalható eszközök léteznek (ezek lehetnek elemzési és tervezési módszertanok, az ezeket támogató CASE eszközök, vagy éppenséggel az implementációt lehetővé tévő alacsonyabb szintű fejlesztő eszközök), az üzemeltetés azonban koncepcionálisan nem illeszkedik bele ebbe a körbe. Ennek oka lényegében az, hogy komplex szoftverrendszerek sikeres és zökkenőmentes üzemeltetéséhez általában nem általános, hanem specifikus, gyakran nagyon mély és aprólékos, rendszerfüggő tudás szükséges, melynek ismertetését már csak terjedelmi és időkorlátok miatt sem tűzhetjük ki célul. Másrészt, a
dolog úgy is fölfogható, hogy éppen úgy, ahogy az orvosok nagyrészt anatómiai ismeretek alapján diagnosztizálnak betegségeket., az üzemeltetéshez nekünk is az adott szoftver anatómiáját kell ismernünk. A baj csak az, hogy míg az ember anatómiáját az orvosnak nem kell minden újszülött esetén újra megtanulnia, a mi esetünkben sajnos más a helyzet. A szoftverek néhány évig élnek csupán, és az utódok - mint valami sci-fiből előbukkant mutánsok - egyre nagyobbak, és egyre kevésbé hasonlítanak a szülőkre. Tehát azok a fajok, amelyeknek most megtanulnánk az anatómiáját, már régen kihalnak, mire oda jutunk, hogy gyógyíthatnánk őket. A további fejezetek az analízis, tervezés és megvalósítás lépéseit és az ezek végrehajtásához kapcsolódó elveket és eszközöket induktív módon (tehát az ábrához képest fordított sorrendben) mutatják be, magyarán világképünket alulról, az egyszerűbb és alapvetőbb elemek
irányából építjük föl ("töviről hegyire" 1). Ez a szemlélet ugyan nem alkalmazkodik az általános szoftver életciklus modellhez, de ne feledjük, hogy nekünk, fejlesztőknek is van saját "életciklusunk", ez pedig szerencsés esetben 2 pontosan a szoftver életciklus modellel ellentétes irányú: Először támogatnak bennünket (ha nem is üzemeltetnek), aztán beosztott fejlesztőként szoftverek implementálásában veszünk részt, majd pályánk későbbi szakaszában elemzési és tervezési feladatokat kapunk. Ha a megoldandó részfeladat viszonylag összetett, az analízishez és tervezéshez már mindig főnöki beosztás kapcsolódik. Ha mindezt összevetjük jegyzetünk további fejezeteivel, a következőket mondhatjuk el. Ha valaki csak a jegyzetünk második, implementációs kérdésekkel foglalkozó fejezetét olvassa el, az elvileg elboldogulhat szakmai pályafutása első néhány évében (feltéve persze, hogy a vizsgán sikerül
ellepleznie azt a tényt, hogy az anyag közel feléről fogalma sincsen, és persze csak abban az ideális esetben, ha a szakma nem fejlődik jelentős mértékben tovább). Néhány év után azonban várhatóan főnök lesz az illetőből, így egyre gyakrabban kap nagyobb elemzési és tervezési feladatokat, melyeket egy csapat vezetőjeként kell megoldania. Ehhez már minden bizonnyal szüksége lesz a jegyzetünk 3 fejezetére, mely a szoftvertervezés és elemzés módszereit és eszközeit vizsgálja. Míg a 2 fejezetben bizonyos szempontból teljességre törekedtünk (azaz megpróbáltuk összeszedni mindazon implementációs elveket, módszereket és eszközöket, melyek jelenleg a világban igazán fontosnak mondhatók), a 3. fejezetben erre nem volt módunk Itt szerepünkhöz híven sokkal inkább a projektvezetés informatikai aspektusaira, főként tervezési és elemzési módszertanokra és eszközökre koncentráltunk, elhanyagolva a menedzsmenttel kapcsolatos olyan
kédéseket, melyekhez egyrészt 1 Ugyan nem szabad a dolgot túl komolyan venni, de tény, hogy ez a jó magyar kifejezés minket igazol: töviről hegyire azt jelenti, hogy az alapoktól indulva egészen a magasabb rendű, összetett fogalmakig tart a magyarázat. A "hegyiről tövire" ezzel szemben kissé furcsán hangzik, ami legalábbis azt sejteti, hogy már eleink is az induktív magyarázatokhoz vonzódtak jobban. 2 Tanúi lehetünk, hogy - feltehetően valamilyen gonosz varázslat következtében - a szoftverfejlesztők egy része manapság hirtelen brókerré vagy kereskedővé változik. hivatalból nem értünk, másrészt azt gondoljuk, hogy ezeket a tapasztalatokat és képességeket nagyrészt úgyis az élet hozza, bár a sikerhez nélkülözhetetlenek. (Itt olyasmikre gondolunk, mint pl a csapat motiválása, a főnöki tekintély megszerzése és fenntartása, a feladatok kiosztása és számonkérése, a személyes problémák kezelése.) 2.
Implementáció Ebben a fejezetben a különböző célkitűzésekkel létrehozott szoftverrendszerek megvalósításakor alkalmazott alapelvekkel és a legfontosabb fejlesztőeszközökkel ismerkedünk meg. Míg azok az alapelvek, amelyekről szólni fogunk, nem mondhatók esetlegeseknek - azaz a számítástudomány ezekkel kapcsolatban már túljutott a kezdeti tapogatódzásokon -, nem mondható el ugyanez a fejlesztőeszközökről, amelyeknek elképesztő sokasága létezik. A fejlesztőeszközök (programozási nyelvek és programfejlesztési környezetek) közül igyekeztünk olyanokat bemutatni, melyek valamilyen okból elterjedtek és nagy hatással voltak vagy vannak a számítástudomány és az informatikai ipar fejlődésére (itt alapvető szempontunk volt a rendszer alapkoncepciójának szilárdsága és szépsége). Mindenekelőtt az egyes feladatosztályok alapvető tulajdonságait tisztázzuk, majd vázoljuk az adott osztályokhoz tartozó feladatok megoldásának
elvi alapjait. Ezt követően térünk rá azoknak az eszközöknek (nyelveknek és programfejlesztői környezeteknek) az ismertetésére, amelyek az egyes feladatosztályokhoz kapcsolódó problémák megoldását alapjaiban támogatják. 2.1 Programozási paradigmák A gyakorlatban megoldandó problémák rendkívül sokfélék. Ezek közül a néhány legfontosabb: 1. Numerikus feldolgozást végző programok: tudományos, statisztikai és mérnöki számításokat támogató programcsomagok. 2. Rendszerprogramok: operációs rendszerek és komponenseik (pl ütemezők, meghajtók, kommunikáció és védelemi). 3. Beágyazott rendszerek: ipari és egészségügyi mérésadatgyűjtő és felügyelő rendszerek, fedélzeti vezérlő berendezések (pl. autók, repülők), ipari folyamatirányító rendszerek 4. Kommunikációs rendszerek: hálózati rendszerek, digitális távközlési rendszerek 5. Fordítóprogramok: ez alatt első sorban magasszintű nyelvek fordító- és
értelmező programjait értjük. A rendszer Szoftverrendszer 6. Adatbáziskezelő rendszerek: környezete relációs és tömbszervezésű adatbáziskezelők, kliens oldali Algoritmusok alkalmazások (pl. adatbevitel, adatmódosítás, lekérdezés, riportgenerálás); Események 7. Intelligens szoftverrendszerek: beszédfelismerés, autonóm robotok, intelligens jáAdatok tékprogramok (pl. sakkautomaták), szakértőrendszerek, intelligens diagnosztikai és tanácsadó rendszerek. 2.1 ábra: Szoftverrendszerek és környezetük 8. Irodai alkalmazások: admiegyszerűsített sémája nisztrációs és szervezési feladatok megkönnyítésére szolgáló rendszerek (pl. szövegszerkesztők, táblázatkezelők, e-mail rendszerek) Az egyes feladatosztályokban felbukkanó problémák nem alkotnak diszjunkt halmazt, emiatt a rendszerezésükre általunk követett módszer csak egy a lehetséges sok közül. A rendezőelvünk elemeit a mellékelt ábra mutatja. Abból az alapvető
tényből indulunk ki, hogy minden szoftverrendszer bizonyos adatokon hajt végre meghatározott algoritmusokat, és viselkedését, gyakran működésének eredményeit a környezetéből érkező események befolyásolják. Ennek az egyszerű modellnek a segítségével a szoftverrendszerek aszerint osztályozhatók, hogy a modell egyes elemei milyen súllyal vesznek részt a megoldás kialakításában. Eszerint három alapvető feladatosztály képezhető: Procedurális (1 csoport): Számításintenzív, algoritmusvezérelt működés. A rendszer többnyire nem túl nagymennyiségű adaton végez bonyolult, és gyakran hosszadalmas műveleteket. - Deklaratív: Adatintenzív, adatvezérelt működés (5, 6, 7, 8 csoport). A rendszer által végrehajtott műveleteket, illetve a rendszer által szolgáltatott eredményeket döntő mértékben a rendszerbe bevitt adatok határozzák meg. - Eseményvezérelt: Párhuzamos működés (2, 3, 4 csoport). A megoldandó feladatok olyan
természetűek, hogy a megoldást rendkívül nehéz egyetlen, közös vezérlési szálon megadni. A sikeres megoldás ehelyett egymással együttműködő, párhuzamos tevékenységek jól strukturált halmazára alapozható. A procedurális és deklaratív megközelítés egymást helyettesítő programozási paradigmák, az eseményvezéreltség azonban sokkal inkább az első két kategória kiegészítőjének tekinthető. Az eseményvezérelt rendszerekkel kapcsolatos problémák és tudnivalók bonyolultsága és gyakorlati fontossága miatt azonban ezekkel a módszerekkel mégiscsak külön kell foglalkoznunk, különben nincs esélyünk a különböző nyelvekbe ágyazott párhuzamos programszerkezetek megértésére és kihasználására. - Az alapelvek részletesebb vizsgálatát az eseményvezérelt programozás alapjainak ismertetésével kezdjük, majd a deklaratív programozás elvi alapjaival folytatjuk. A procedurális programozással kapcsolatban föltételezzük,
hogy az olvasó ismer már legalább egy procedurális programnyelvet (pl. C, Pascal) és a strukturált programozás elvét, így az ezekhez kapcsolódó fogalmakat ez a jegyzet nem tárgyalja (pl. vezérlési szerkezetek, blokkstruktúra, változók láthatósága és élettartama, strukturált programozás). Ehelyett részletesen bemutatjuk az objektumorientált programozással kapcsolatos alapvető fogalmakat. Bár létezik olyan felfogás, mely szerint az objektumorientált programozás a deklaratív módszerek sorába tartozik, ezt a szerzők nem vallják magukénak, és remélhetőleg az olvasóval is sikerül elhitetnünk igazunkat, vagyis hogy az objektumorientált programozás a procedurális programozás fejlődésének jelenleg ismert legfelsőbb foka. 2.2 Eseményvezérelt rendszerek Ebben a fejezetben röviden összefoglaljuk a párhuzamos, eseményvezérelt rendszerek fejlesztésekor alkalmazott legfontosabb fogalmakat és módszereket. 2.21 Alapfogalmak - - - -
- 3 4 Szekvenciális program: A program utasításainak végrehajtási sorrendjét a programban megfogalmazott vezérlési szerkezetek önmagukban egyértelműen3 definiálják. Folyamat: Az egymással párhuzamosan futó, önmagukban szekvenciális programrészeket, melyek egymás nélkül értelmes működésre többnyire nem képesek, folyamatoknak nevezzük. Párhuzamos (konkurens) rendszer: Több folyamat egymással párhuzamosan működve hajt végre egy adott feladatot. Esemény: A párhuzamos program futását befolyásoló, véletlen időpontban bekövetkező történés. Az esemény fogalma a párhuzamos programozásban központi szerepet játszik, a párhuzamos rendszereket eseményvezérelt rendszereknek is nevezik. Belső esemény: A párhuzamos program egyik folyamata által előállított esemény (pl. egy adat egy másik folyamat számára). Külső esemény: A párhuzamos programon kívüli esemény (pl. periféria IT) Valós idejű rendszer: Olyan párhuzamos
rendszer, mely az őt érő külső eseményekre adott időkorláton belül reagál 4. Erőforrás: minden olyan hardver vagy szoftver eszköz, melyet a folyamatok futásuk során felhasználhatnak. (Az erőforrások közé sorolhatók különböző I/O perifériák, tárterületek, pl pufferek és rendszerprogramok). Közös erőforrás: Olyan erőforrás, melyre több folyamatnak is szüksége van. Kölcsönös kizárás: Ha egy olyan közös erőforrást, melyet egyidőben csak egy folyamat használhat értelmesen (pl. printer), egyszerre többen is használni akarnak, ez a rendszer hibás működését okozza. Kölcsönös kizárásnak azt nevezik, mikor egy erőforrásnak egy tetszőleges folyamathoz való hozzárendelése mindaddig kizárja az erőforrásnak más folyamathoz való rendelését, míg az erőforrást birtokló folyamat az erőforrás használati jogáról le nem mond. Kritikus szakasz: A folyamatok azon részei kritikus szakaszok, amelyeken belül meg kell oldani a
kölcsönös kizárást. Kommunikáció: A folyamatok futásuk közben egymással adatokat cserélnek, ez az egységes rendszerré való összekapcsolásuk elengedhetetlen feltétele. Szinkronizáció: A folyamatok alapvetően egymástól függetlenül, aszinkron módon futnak, azonban időnként (pl. a sikeres kommunikáció érdekében) futásukat egymáshoz vagy külső eseményekhez kell igazítaniuk (szinkronizálniuk) Holtpont (deadlock): Hibás szinkronizáció, erőforráskezelés következtében két vagy több folyamat kölcsönösen egymásra kezd várakozni, ami a rendszer végérvényes befagyását okozza. Természetesen egy szekvenciális program lefutása sem mindig azonos. Különböző bemeneti adatsorozatokkal befolyásolható, de a műveletek mindig a programban megadott sorrendben hajtódnak végre, és a sorrend nem függ előre nem látható események (pl. külső megszakítások) bekövetkezésének időpontjától. A real time rendszereket gyakran - tévesen
- automatikusan gyors rendszereknek tekintik, holott a két dolog egymástól független. Ha az időkorlát kicsi, a rendszer gyors, ha nagy (lehet akár több tíz perc is), akkor nem feltétlenül. 2.22 Konkurens- és valós idejű ütemezők A gyakorlatban alkalmazott párhuzamos rendszerek döntő többsége egyprocesszoros architektúrákon működik, tehát a párhuzamosság csak virtuális (egyszerre fizikailag mindig csak egy folyamat fut). Többprocesszoros architektúrák esetén is valószínű, hogy nem jut minden folyamatnak külön processzor. Mindkét esetben több folyamat osztozik egy processzoron. Ilyen esetekben szükség van egy valós idejű ütemezőre, mely a processzoron az egyes folyamatokat megfelelő módon futtatja. Ezt a feladatot a folyamatok maguk képtelenek ellátni, hiszen egymásról csak minimális információval rendelkeznek. 2.4 ábra: Ütemezési algoritmusok fő jellegzetességei Az ütemező feladatai a következők: - nyilvántartja a
folyamatok állapotát; - kezeli az eseményeket, az események hatására a folyamatok futását átütemezi; 2.2 ábra: Konkurens és valós idejű ütemezők állapotai - bizonyos algoritmus szerint kiválasztja a következő futtatandó folyamatot; - elindítja illetve továbbindítja a folyamatokat. Az ütemező minden folyamatot a következő állapotok szerint tart nyilván: - Futó: egy ilyen folyamat lehet, amelyik éppen a processzort birtokolja. Egyprocesszoros rendszerben csak egy ilyen folyamat lehetséges; - Futásra kész az a folyamat, melynek futásához semmi más feltétel nem hiányzik, csak a processzor felszabadulása. Ha a futó folyamatot az ütemező a folyamat "akaratán kívül" futásra kész állapotba helyezheti, (mert pl. egy nála nagyobb prioritású folyamat futásra kész állapotba került), akkor az ütemező preemptív; - Belső eseményre vár minden olyan folyamat, melynek folytatásához egy másik folyamat által generált 2.3
ábra: Két korutin párhuzamos esemény (pl. jelzés, adat) szükséges; működése - Külső eseményre vár minden olyan folyamat, melynek folytatásához külső esemény (gyakorlatban IT) bekövetkezése szükséges. - Stop: A folyamatok futó állapotból kerülhetnek saját maguk által kiadott speciális utasítás hatására stop állapotba, és addig inaktívak maradnak, amíg egy futó folyamat külön utasítással újra nem indítja őket. Az ütemező a következőkben ismertetett (vagy ezekhez hasonló) módszerekkel dönti el, hogy egy adott pillanatban a futásra kész folyamatok közül melyiknek adja át a futás jogát. Az algoritmusok vizsgálatát a következő szempontok alapján végezzük: - Kiéheztetés: Ha egy folyamat kiszolgálása más, újonnan érkező folyamatok által elvileg tetszőleges ideig késleltethető, akkor az ütemezési algoritmus kiéheztetős. - Prioritás: A rendszerben kiszogálásra váró folyamatok egyformán fontosake vagy
nem (vannak-e egyenlőbbek?). - Preemptivitás: A nagyobb prioritású folyamat elsőbbséget élvez-e az éppen futó folyamattal szemben is? Ha preemptiv a rendszer, akkor a futó folyamat félbeszakad, amint egy nála nagyobb prioritású folyamat kezd el várni a processzorra, ellenkező esetben a futó folyamat befejezi az aktuális részműködést. - Járulékos futásidő-információ: a felhasználónak meg kell-e adnia a folyamat futtatásához olyan időkorlátot, melyen belül a folyamat futása befejeződik (főként operációs rendszerekben lényeges). A legelterjedtebben használt algoritmusok a következők: 2.5 ábra: Több korutin párhuzamos működése FCFS (First Come First Served): Egyszerű sorbanállás; minél korábban "érkezik" egy folyamat, annál előbb kerül sorra. Prioritásos ütemező: Akinek a prioritása nagyobb, előbb kerül sorra. Az azonos prioritásúak között FCFS. SJF (Shortest Job First): A legrövidebb várható futásidejű
folyamatot futtatják elsőnek. A kezdeti futásidő tkp. a folyamat prioritását adja, mely a futás során változatlan Ha a felhasználó által a folyamathoz megadott futási idő letelik (pl. mert a felhasználó csalafinta akart lenni), a folyamat félbeszakítható. SRT (Shortest Remaining Time): Hasonló az SJF-hez, azonban a prioritást mindig a megadott végrehajtási időből még megmaradt idő alapján számítják. SET (Shortest Elapsed Time): Az futhat először, aki az adott pillanatig a legkevesebbet futott. RR (Round Robin): Körforgó prioritás (a kiszolgált folyamat visszaáll a sor végére). Általában időosztással együtt használják. 2.23 Folyamatok leírásának eszközei Korutinok: Egymás mellé rendelt rutinok, közöttük nincsen hívó-hívott kapcsolat, ahogyan egy eljárás (szubrutin) hívása esetén. A korutinok futásuk során speciális utasítással (pl transfer) egymásnak adják át a vezérlést. A korutinok általában végtelen
ciklusban futnak A korutinok megszokottól eltérő vonásainak megvalósításához az szükséges, hogy munkaterületük ne szűnjön meg, ha a korutin a vezérlést átadja, így a félbehagyott működés folytatható. A korutin futása mindig a legutóbbi vezérlésátadás helyétől folytatódik. Eljárásoknak magasszintű nyelvekben általában veremben allokálnak memóriát, mely híváskor felépül, visszatéréskor lebomlik. Korutinoknál (és folyamatoknál) ez a megoldás nem használható, mert a félbehagyott működés nem okozhatja a munkaterület megszűnését. A legegyszerűbb esetben a korutinok számára statikusan kell a memóriát lefoglalni, így azonban új korutinok csak fordítási időben generálhatók. Egy másik - a gyakorlatban is elterjedten használt - módszer az ún. kaktusz tár Ennek lényege, hogy a korutinok egy adott környezethez (pl. blokkstruktúra) kötődnek, melyhez stack keret tartozik A korutinok munkaterületét ezután az adott
stack keretben elhelyezett pointereken keresztül dinamikusan, futásidőben allokálják. így a stack keretből oldalágak "nőnek ki" Korutinnal maga a konkurens ütemező is egyszerűen megvalósítható (az ábrának megfelelően). Ekkor a folyamatok olyan korutinok lesznek, melyekben a kommunikációs és szinkronizációs eszközöket korutin transzfer segítségével valósítják meg. A korutinok a folyamatok leírásának viszonylag alacsony szintű eszközei, ezért - bár megvalósítási mechanizmusaikat tekintve a többi módszer is hasonló - léteznek szintaktikailag kifejezőbb, ezért biztonságosabb és olvashatóbb módszerek is. COBEGIN - COEND pár: Olyan speciális utasítászárójel pár, mely az általa közrefogott utasításokat (pl. eljáráshívásokat) párhuzamosan futó folyamatokká teszi. Processz (folyamat) deklaráció: Ez tekinthető a szintaktikailag legkifejezőbb formának. Segítségével tetszőleges eljárásból létrehozható
(általában tetszőleges számú) folyamat. Pl.: (MODULA 2): newprocess( p: proc; wpsize: cardinal; pr: cardinal ); A newprocess eljárással a p nevű eljárásból létrejön egy új folyamat, melynek wpsize méretű munkaterülete van és a prioritása pr (az ütemező prioritásos algoritmussal dolgozik). Szálak (thread): A szál (thread) viszonylag új fogalom. A szálak és a folyamatok közötti különbségek megértését segíti, ha megismerjük a szálakra használt másik elnevezést: pehelysúlyú folyamat (lightweight process). Míg egy korutin jellegű tevékenység egészen önálló életet él, saját tárterülettel rendelkezik, amelyre a rendszer féltékenyen vigyáz, addig a szálak közös tárterületet, állományokat és egyéb rendszer-erőforrásokat használnak, csak a vermük és a regisztereik a sajátjuk. A közös erőforrások tulajdonosa általában egy folyamat, a szálak ennek a környezetében (context) futnak. Ha vannak folyamataink, miért
kellenek a szálak? Gyorsabban lehet közöttük váltogatni, a várakozó tevékenység gyorsabban felébredhet, reagálhat valamilyen eseményre. A közös memória megkönnyíti a tevékenységek közötti információcserét is. Persze az előnyök mellett a szálakkal problémák is adódhatnak: közös lónak túros a háta, a közös tárban turkálásnál nagyon kell vigyázni arra, hogy mikor melyik szál következhet. Egy jól megtervezett nyelv, rendszer igyekszik kivédeni ezeket a hibákat, de a programozók zseniálisak, ha új programozási hibákat kell kitalálni, és sajnos a párhuzamos tevékenységekkel együtt megjelentek a rettegett időfüggő, nem reprodukálható hibák. Újabban divattá váltak a szálak, lassan néhány operációs rendszer - pl. a Sun Solaris 2x rendszere, vagy a Windows NT - rendszerszinten is támogatja ezeket, de megjelentek olyan programcsomagok (pl. PThreads), amelyek segítségével a hagyományos UNIX rendszereken is használhattunk
C nyelvi programokból szálakat. A Java nyelv túllép ezen, nyelvi szintre emeli a szálak fogalmát, használatuk támogatását. 2.24 Kommunikáció, szinkronizáció és kölcsönös kizárás A következőkben egy helyen tárgyaljuk a kommunikáció, a szinkronizáció és a kölcsönös kizárás alapvető eszközeit, ezek ugyanis egymással meglehetősen szorosan összefonódnak, így az eszközöket bizonyos esetekben nehéz szétválasztani. Lock bit: Kezdetleges eszköz. A lock biten háromféle műveletet értelmezünk: - lock: a bit zárt állapotba helyezése; - unlock: a bit nyitott állapotba helyezése; - test: a bit állapotának vizsgálata. Ha pl. kritikus szakaszokat akarunk ilyen módon létrehozni, minden szakaszhoz hozzárendelünk egy lock bitet. Azok a processzek, amelyek osztoznak az erőforráson, osztoznak a lock biten is Ha egy folyamat egy kritikus szakasz bejáratához érkezik, megvizsgálja a szakaszhoz rendelt lock bit állapotát. Ha a bit
zárt, megvárja, míg nyitott állapotú nem lesz Ha a bit nyitott, lezárja, és belép a kritikus szakaszba. A kritikus szakaszból való kilépéskor a hozzá tartozó lock bitet nyitott állapotba kell hozni. Ez a megoldás meglehetősen veszélyes a következők miatt: - - A lock bit vizsgálata és állítása között a folyamat nem szakítható meg (tehát ezalatt a megszakításokat le kell tiltani), különben a rendszer működése megbízhatatlanná válik (egyszerre több folyamat is nyitott állapotban találhatja a lock bitet); A lock bit állítása és vizsgálata egymástól el van választva, így a lock bit vizsgálat nélkül is átállítható; A kritikus szakasz bejáratánál és kijáratánál egyaránt könnyen elfelejthető a lock bit állítása. Szemafor: Alapvető szinkronizációs eszköz, mely a lock bit hátrányos tulajdonságainak többségét kiküszöböli. A szemafor logikai (bináris szemafor) vagy egész típusú változó, melyen két
alapműveletet értelmezünk: P(s) P operáció 5: Oszthatatlan műveletként hajtódik végre, s egy tetszőleges szemafort jelöl. Ha s>0, akkor P(s) <=> s:=s-1. Ha s=0, a P operációt kiadó folyamat belső eseményre váró állapotba kerül, amíg s>0 értékű nem lesz. - V(s) V operáció 6: Szintén oszthatatlan műveletként hajtódik végre. V(s) <=> s:=s+1 A szemafor a lock bitnél jóval biztonságosabb és hatékonyabb, széles körben alkalmazott eszköz, de nem elég strukturált és nem elég magasszintű (pl. ki lehet adni P operációt V nélkül) - Monitor: Párhuzamos programrendszerek működésének biztonsága jelentősen fokozható, ha a tisztán szekvenciális programrészeket elkülönítjük a párhuzamos végrehajtáshoz szükséges eszközöktől (pl. erőforráskezelés, szinkronizáció, kommunikáció, kölcsönös kizárás). Ilymódon a szekvenciális programrészeket a folyamatok foglalják magukba, a párhuzamos működés
kellékeit pedig speciális modulok, a monitorok7. A folyamatok kommunikációs és szinkronizációs eszközként nem tartalmazhatnak mást, mint monitorbeli eljárásoknak és függvényeknek a hívásait. A monitor alaptulajdonságai közé tartozik, hogy a kölcsönös kizárást megoldja, tehát egyidőben csak egy futó monitorhívás lehet, a többi folyamat, mely monitorbeli eljárást hív, mindaddig várakozó állapotba kerül, míg a futó eljárás a hívó folyamatba vissza nem tér, vagy a hívó folyamat várakozó állapotba nem kerül. A kommunikáció és a szinkronizáció megvalósítására a monitorban is külön megoldások (pl. szemaforok, pufferek) szükségesek. Megoldások többprocesszoros rendszerekben: A monitorkoncepció nagyon tiszta és jól strukturált, de hátránya, hogy a több folyamat által használt eljárások és adatstruktúrák csak közös memóriában helyezhetők el. Ez egyprocesszoros rendszerekben 5 A P a holland passeren
(áthaladni) szó kezdőbetűjéből származik. 6 A V a holland vrijgeven (ejtése kb. vrejhéfen, jelentése felszabadítani, szabaddá tenni) szó kezdőbetűjéből származik. 7 Az elnevezés az operációs rendszerek magjául szolgáló monitortól származik. A feladat mindkét esetben hasonló: erőforrásvédelem és kölcsönös kizárás. természetes, többprocesszoros rendszerekben azonban komoly hátrány, mert a közös tár használata a rendszer hatékonyságát nagyon leronthatja. Ezért többprocesszoros rendszerekben csak olyan megoldások jöhetnek szóba, melyek nem teszik szükségessé a közös tárterületek használatát. Üzenetváltás: Szinkronizációs és kommunikációs eszköz egyszerre. Szintaktikailag a következő jellegű: send <üzenet> to <címzett> (* Küldő oldal ) receive <üzenet> from <feladó> (* Vevő oldal ) Megjegyzések: - Üzenetküldésnél a <címzett> feltétlenül szükséges, vételnél
azonban a <feladó> elmaradhat (bizonyos esetekben ez kedvező is, mert így egyszerűbb). A szinkronizáció fajtái: Egyirányú: Ha az üzenet még nem érkezett meg, csak a vevőnek kell várni, a küldő sohasem vár; Kétirányú: Akár a vevő, akár a küldő ér előbb az üzenetváltás helyére, mindkettőjüknek meg kell várnia a másikat. Az üzenetváltás alapvető módjai: - - - Postaláda: egyirányú szinkronizációt valósít meg közös adatterületen keresztül. Az adó a postaládába teszi az üzenetét, a vevő kiveszi onnan; Távoli eljáráshívás: A folyamatoknak egyáltalán nincs közös adatterületük, egymás bizonyos változóihoz meghatározott eljárások hívásával férhetnek hozzá. A távoli eljárás egy másik folyamatban (és egy másik processzoron is) futhat Az üzenet a távoli eljárás aktuális paramétereként továbbítódik. Randevú: Kétirányú szinkronizáció távoli eljáráshíváson keresztül megvalósított
üzenetváltással. 2.3 Deklaratív rendszerek A deklaratív fejlesztőeszközök közös vonása, hogy számukra adott feladat megoldásához elegendő a megoldandó feladatot leírni, a megoldáshoz vezető egyes lépéseknek - és ezáltal magának a megoldásnak - a megkeresése már a rendszer feladata. Mindez a procedurális rendszerekben a megoldás algoritmusának lépésenkénti leírását igényli. A kétfajta megközelítés között nagyjából akkora különbség van, mint egy szövegszerkesztő program és egy jólképzett titkárnő között. Tételezzük föl, hogy levelet akarunk írni bizonyos ügyben egy üzleti partnerünknek. Ha a problémát procedurális módon kell megoldanunk, ez annak az esetnek felel meg, amikor csak egy szövegszerkesztőnk van. Ilyenkor meg kell adnunk a levél formátumát, ki kell keresnünk az üzletfelünk címét és egyéb adatait, pontosan végig kell gondolnunk, mit is írjunk, végül a levél minden betűjét nekünk kell
legépelnünk, majd a levelet valamilyen módon el kell juttatnunk a címzetthez. Ezzel szemben a deklaratív megoldáshoz szükségünk van ugyan egy titkárnőre (aki persze az életünket sok szempontból bonyolíthatja is), neki azonban elegendő valami ilyesmit mondani: "Léci, írj Nagy úrnak egy levet, hogy elfogadjuk a tegnapi ajánlatát, és a részletekkel kapcsolatban majd telefonon egyeztetek vele még a héten. Köszi!" Ezután a levelet már csak alá kell írnunk (legföljebb el kell olvasnunk). Az említett példa esetében a különbség szembeszökő, a gyakorlatban azonban a legtöbb felhasználó számára nem nyilvánvaló, hová fejlődhetnének még a jelenlegi "fantasztikus" grafikus kezelői felületek. A jövő az olyan számítógépes rendszereké, amelyeket bárki, azonnal tud használni 8, és ha A félreértések elkerülése végett: a bonyolult grafikus kezelői felülettel rendelkező szoftverek nem ilyenek. Bill Gates a Windows95
bejelentése körül állítólag úgy nyilatkozott, hogy a Microsoft olyan programot készített, amelyet még Bill Gates nagymamája is tud használni. Azóta évek teltek el, és minden kétséget kizáróan 8 valahol mégis elakad, a rendszer átsegíti a nehézségeken. Az ilyen rendszerek pedig deklaratív rendszerek lesznek. A továbbiakban a deklaratív rendszerekkel kapcsolatos alapfogalmakat és alapvető módszereket ismertetjük. Ebből a rövid összefoglalóból szándékunk szerint az is kiderül majd, hogy a deklaratív rendszerek rendkívül kívánatos és hasznos voltuk ellenére miért nem tudtak ezidáig széles körben elterjedni. 2.31 Problémák és problématerek A deklaratív megközelítésben a megoldandó probléma specifikációjából kiindulva a megoldáshoz vezető út egyes lehetséges állomásain végighaladva kell a rendszernek megtalálnia a megoldást. Példaképpen tegyük föl, hogy a megoldandó problémánk a sakkjáték. Ez a
következő módon írható le: - A kellékek leírása (8x8-as tábla fekete-fehér mezőkkel, milyenek a bábuk). - Kiindulási állapot (alapállás). - Célállapot (matt). - Szabályok (hogyan kell lépni, ütni stb.) A sakktáblán a játék közben időben változó állások sorozata alakul ki, melyek az egyik fél győzelméhez, és a másik fél vereségéhez (esetleg döntetlenhez) vezetnek. A sakk esetén a lehetséges összes állás halmaza problémateret (vagy más szóval állapotteret) határoz meg. Az egyes állapotok között létezhetnek szabályok által meghatározott átmenetek (a sakkban ilyen átmenetet jelent például egy szabályos lépés, mely egy adott állásból egy másikhoz vezet). A probléma megoldása ebben a megközelítésben nem más, mint a problématér egyes állapotai közötti bolyongás, melynek során egy adott kiinduló állapot (a sakk esetén az alapállás) és egy céllállapot (a sakk esetén matt vagy döntetlen) között kell
megtalálni egy lehetséges utat. A megoldás nehézségét a sakk esetén az állapottér "kozmikus" mérete okozza. A produkciós rendszer olyan általános elvi keret, mely lehetőséget nyújt problémák leírására, és a problématérbeli bolyongás vezérlésére, ezen keresztül a megoldás (célállapot) megkeresésére. A produkciós rendszer a következő általános elemekből áll: - Adatbázisok: - állandó (a problémamegoldás során tartalma nem változik, mint pl. a kellékek leírása); - változó (tartalma a problémamegoldás során változhat, mint pl. a sakkállás) - Szabályok: ha <feltétel> akkor <következmény> alakúak, ahol a feltételrész bizonyos adatbáziselemek felhasználásával kialakított kifejezés, a következmény pedig valamiféle transzformációt ír elő bizonyos adatbáziselemeken. - Vezérlési algoritmus: az alkalmazható szabályok kiválasztására, a szabályok végrehajtására, és a célállapot
felismerésére, végső soron a probléma megoldásának keresésére szolgáló algoritmus. A továbbiakban gyakran keresési algoritmusnak is nevezzük. Belátható, hogy a produkciós rendszer Turing gép erejű eszköz, tehát produkciós rendszer segítségével minden algoritmikusan megoldható feladat megoldható. A problématér általánosságban azon lehetséges állapotok halmaza, melyeket egy adott produkciós rendszer vezérlési algoritmusa az adatbázisok és szabályok felhasználásával generálni képes. Az adatbázisok tárolják a probléma leírását. Mivel a megoldandó problémák nagyon sokfélék, gyakran nehezen formalizálhatók és bonyolultak, ezért az adatbázis felépítéséről általánosságban kevés bebizonyosodott, hogy a vér nem válik vízzé, és alighanem Bill Gates nagymamája is számítástechnikai zseni. Be kell vallanunk, hogy a szerzők nagymamája, de még anyukája sem képes a Windows95 használatára. mondható. Ezzel a
kérdéssel kapcsolatban tudásábrázolás címszó alatt számos megoldás ismeretes, és ma is aktív kutatások folynak. A vezérlési algoritmus általános lépéseit a 2.6 ábra mutatja 0. 1. 2. 3. 4. Legyen az aktuális állapot a kiindulási állapot. Célállapot tesztelése: Ha az aktuális állapot célállapot, akkor kész, állj meg. Mintaillesztés: keresd meg az összes olyan szabályt, melyek feltételrésze teljesül. Ezen szabályok halmazát konfliktushalmaznak nevezzük. Konfliktusfeloldás: Válassz ki a konfliktushalmazból egy Sz szabályt. Szabályalkalmazás: hajtsd végre Sz következményrészét. Folytasd 1-től. 2.6 ábra: Általános vezérlési algoritmus váz A produkciós rendszer lényeges tulajdonsága, hogy - elvileg - tetszőleges probléma megoldását lehetővé teszi egyetlen vezérlési algoritmus segítségével. Ebből következően, konkrét esetekben elegendő a probléma leírását megváltoztatni ahhoz, hogy a rendszer működése
megváltozzon, és az algoritmus átírása nem szükséges. Mindez a rendszer adatvezérelt működését eredményezi, az adatbázisok és szabályok inkrementálisan fejleszthetők (azaz új elemek hozzáadásával a rendszer képességei bővülnek, és az új elemek elvileg nem teszik szükségessé már meglévő elemek módosítását), illetve a programozás módszere deklaratív (azaz elegendő a problémát leírni, a megoldás módja a rendszerben eleve adott, illetve a rendszer automatikusan generálja a megoldást). A produkciós rendszer elvi eleganciája mellett komoly problémák merülnek föl a gyakorlatban megjelenő problématerek hatalmas méretei miatt (hiszen a produkciós rendszer modellt többek között éppen NP-teljes problémák megoldására használják), illetve abból fakadóan, hogy a problémák leírása maga is gyakran komoly elvi nehézségeket okoz. A problématér nagy mérete egyrészt a vezérlési algoritmussal, másrészt a változó
adatbázisokkal szemben támaszt követelményeket (ahol a keresés során generált állapotokat tároljuk). A következtetési gráf a problématér azon részhalmaza, melyet a keresési algoritmus a problémamegoldás során generál. A következtetési gráf csomópontjai a problématér egyes állapotai, élei pedig két adott állapot közötti átmenethez végrehajtott szabálynak felelnek meg. Egy keresési algoritmus annál jobb, minél kisebb méretű következtetési gráfot generál adott probléma megoldása során. Kombinatorikus robbanásnak nevezzük, mikor a következtetési gráf állapotainak száma a keresési algoritmus működése során a generált új állapotok számával exponenciálisan növekszik. A keresési algoritmustól elvárjuk, hogy legyen mozgáskeltő (azaz a generált új állapotok között mindig legyen olyan, amely korábban még nem generálódott), szisztematikus (azaz, ha kell, a teljes problémateret képes legyen generálni) és lehetőség
szerint kerülje el a kombinatorikus robbanást (azaz a problématérnek ténylegesen minél kisebb részhalmazát generálja). A következtetési gráfot a változó adatbázisok tárolják (a gyakorlatban ezeket gyakran munkamemóriának nevezik). Lényeges kérdés a gráf ábrázolásának és a csomópontok (állapotok) tárolásának a módja. A következtetési gráfot ábrázolhatjuk faként vagy valódi gráfként. - Fa: új csomópont hozzáadása egyszerű; ciklusok felismerése nehéz; ha az összes állapotot egyidőben tároljuk, a tárigény nagy. Gyakorlatban a mélységi kereső eljárások azok, amelyek fát generálnak. - Gráf: új csomópont hozzáadásakor meg kell vizsgálni, hogy az adott állapot már eleme-e a gráfnak (tehát a hozzáadás bonyolultabb), ciklusok felismerése egyszerű, helyigény minimális. Ezt a módszert alkalmazzák pl. a best-first kereső eljárások Az egyes csomópontok ábrázolása általában a következő módon
történik. A kezdőállapotból kiindulva azt úgy módosítjuk, hogy mindig az aktuális állapotot írja le, de minden állapothoz megjegyezzük azoknak az adatbázis elemeknek az előző értékét, amelyeket megváltoztattunk (az aktuális értéküket mindig az aktuális állapot leírásába írjuk). Ily módon az aktuális állapot adott, és az esetleges visszalépés viszonylag egyszerű. (A visszalépésről még lesz szó a következtetési algoritmusok részletesebb tárgyalásánál.) Keretprobléma: Ha nem teljes állapotleírásokat tárolunk, gondot okozhat az egyes változók közötti összefüggések kezelése. Ezeket például megfelelő relációkkal lehet kifejezni A keresési algoritmus további jellegzetes tulajdonsága a keresés iránya: - Előre láncolás (forward chaining): a kiinduló állapotból haladunk a célállapot felé, a szabályok feltételrészét illesztjük, és a következményrészeket hajtjuk végre. - Hátrafelé láncolás (backward
chaining): egy célállapotból haladunk a kiinduló állapot felé, az alkalmazható szabályokat a következményrészük alapján választjuk ki, és a következő (tkp. előző) állapotot a feltételrészek alapján állítjuk elő. Általánosságban ugyanaz a szabályrendszer alakalmazható előre és hátrafelé láncolásos módon is. 2.32 A következtetési algoritmus egyes lépéseinek megvalósítási módszerei A továbbiakban a 2.6 ábra alapján a vezérlési algoritmus általános lépéseivel, ezen lépések következményeivel és megvalósításuk módszereivel foglalkozunk. Az algoritmus egyes lépései általánosnak tekinthetők, maga az algoritmus azonban konkrét esetekben alkalmazhatja a lépéseket eltérő sorrendben, illetve a megadott egyszerű sémánál sokkal bonyolultabb is lehet. A 26 ábra célja, hogy segítségével elemezhessük a vezérlési algoritmusok általános sajátosságait és problémáit. Az egyszerűség kedvéért feltételezzük,
hogy az algoritmus előre láncolást használ, a módszerek azonban értelemszerűen hátrefelé láncolás esetén is működnek. 2.321 Mintaillesztés A lépés célja az adott állapotban alkalmazható szabályok kiválasztása. Megvalósítására a következő alapvető módszerek ismeretesek. Kimerítő keresés Vegyük sorra az összes rendelkezésünkre álló szabályt, és válasszuk ki közülük azokat, amelyek feltételrésze igaz. Ezt úgy határozhatjuk meg, hogy a feltételrészeket az adatbáziselemek aktuális értékének figyelembe vételével kiértékeljük. Tulajdonságok: - egyszerű; lassú (ha sok a szabály); nem mindig működik (pl. illesztés változókkal, szabálypéldányok stb) Indexelés Célja a mintaillesztés gyorsítása. Alapgondolat: a szabályrendszert indexelési mechanizmussal (pl Hash tábla) egészíti ki. Az index az aktuális állapot alapján gyorsan meghatározható, ezután az illeszkedő szabályok megkeresése nagyon gyors. Gond:
- maga az indexstruktúra nagyon nagy lehet, karbantartása pedig bonyolult (tárigény, létrehozás); egy szabály több helyen is előfordulhat (ez a méretet és felépítést tovább bonyolítja); - viszonylag merev. Illesztés változókkal (unifikáció) Olyan komplex illesztési mechanizmus, mely általános logikai állítások (predikátumok) és aktuális tények egymásnak való megfeleltetésére alkalmas. Példa: A "Minden egér szereti a sajtot." állítás a következő szabállyal írható le: Sz1: egér(x) szereti(x,sajt) Ha adottak továbbá a: T1: egér(Inci) T2: egér(Finci) T3: egér(Jerry) állítások, és arra a kérdésre kell válaszolnunk, hogy ki szereti a sajtot, akkor az Sz1 szabály (bár csak egy van belőle!) háromféleképpen alkalmazható: Sz11: egér(x/Inci) szereti(x/Inci,sajt) Sz12: egér(x/Finci) szereti(x/Finci,sajt) Sz13: egér(x/Jerry) szereti(x/Jerry,sajt) Sz11, Sz12 és Sz13 szabálypéldányokat jelölnek,
mégpedig az aktuális x/<egérnév> változóérték kötésekkel. Ez azt jelenti, hogy a szabályokban megjelenő tényváltozók értékéhez tetszőleges, ismert tényt köthetünk, a korlátozás csupán annyi, hogy egyetlen szabálypéldányon belül az alkalmazott kötéseknek konzisztenseknek kell lenniük (tehát egy változóhoz csak egyféle kötést rendelhetük, akkor is, ha a változó a szabályon belül többször előfordul). Adatfüggőségi gráfok A mintaillesztés gyakorlatban alkalmazott produkciós rendszerekben olykor a problémamegoldás idejének 80%-át is felemészti, ezért alapvetően fontos a gyorsítása. Az adatfüggőségi gráfok a szabályok közötti szemantikai összefügéseket tárolják olymódon, hogy minden adatbáziselemhez hozzárendelik azokat a szabályokat, amelyeknek a feltételrészében megjelennek (tehát amelyek értékétől az adott szabály alkalmazhatósága függhet), és minden szabályhoz hozzárendelik azokat az
adatbáziselemeket, amelyek értéke az adott szabály alkalmazása során megváltozhat. Az adatfüggőségi gráfok alkalmazhatósága azon a nyilvánvaló tényen alapszik, hogy az egyes állapotátmenetek (szabályalkalmazások) során nem változik meg a teljes adatbázis, annak csak egyes elszigetelt részei, és a mintaillesztőnek (egy kezdeti kimerítő illesztés után) elég csak a megváltozott adatelemek alapján kijelölt szabályokat illeszteni. Ily módon a mintaillesztés keresési tere nagymértékben leszűkíthető, és maga a mintaillesztés rendkívül felgyorsítható. R1: A B C R2: C D E R3: C E F A B R1 C D R2 E R3 F 2.7 ábra: Egyszerű adatfüggőségi gráf 2.322 Konfliktusfeloldás Célja a keresési algoritmus vezérlése az alkalmazandó szabályok kiválasztásával. A konfliktusfeloldás alapvetően befolyásolja a keresés hatékonyságát, de egzakt konfliktusfeloldási stratégia a megoldandó problémák NP-teljes voltából következően nem
létezik. A konfliktusfeloldási stratégiák ún heurisztikák, tehát olyan intuitív módszerek, amelyek az esetek többségében a megoldást gyorsítják, de matematikailag nem bizonyítható a hatásuk. Néhány általános heurisztika: - - - A speciálisnak prioritása van. A speciális az általánost tartalmazza, de annál bővebb (pl. hosszabb feltétel). A speciális bizonyos helyeken változók helyett konstansokat tartalmazhat. Költségfüggvény (a szabálykiválasztás megfelelő numerikus paraméterek alapján számított költségek segítségével történik). A frissebb elemeket alkalmazd elsőként (ezek feltehetően több információ birtokában születtek). Mindig azt a szabályt alkalmazd, amelyik a legkisebb mértékben bővíti a következtetési gráfot. stb. 2.8 ábra: Szélességi keresés 2.34 Keresési stratégiák A tudásalapú rendszerek egyik kulcskérdése, milyen módszerrel járja be a vezérlési algoritmus a következtetési gráfot,
ez ugyanis alapvetően befolyásolja a célállapot elérésének munkaigényét. Három alapvető módszert ismertetünk. Szélességi keresés Lényege, hogy a következtetési gráfot a kiinduló állapotból szintről szintre felépítjük, és minden szinten megvizsgáljuk, hogy elértük-e valahol a célállapotot. Konfliktus feloldás itt nincsen, ugyanis egyidőben, párhuzamosan halad a keresés az összes lehetséges úton. 2.9 ábra: Mélységi keresés A módszer viszonylag egyszerű, de bonyolult problémák esetén a tárigénye kezelhetetlenül megnőhet, és - mivel minden szinten az összes lehetséges állapotot generálni kell, - végrehajtása nagyon munkaigányes. A gyakorlatban ezek miatt viszonylag ritkán alkalmazható Mélységi keresés Nem járjuk be, és nem is generáljuk az összes lehetséges állapotot és a hozzájuk tartozó utakat, egyszerre mindig csak egyet követünk. Ezzel a módszerrel a célállapot keresésének hatékonysága
nagyságrendekkel megnövelhető, de mindez nagyon erősen múlik az alkalmazott konfliktusfeloldási stratégián (azaz az alkalmazható szabály kiválasztására felhasznált megoldásoktól). Az ábrán alkalmazott konfliktusfeloldási stratégia a "balra tarts", azaz a még be nem járt "legbaloldalibb" úton kell elindulni. A keresés során előfordulhat hogy zsákutcába jutunk (azaz a konfliktushalmaz üres). Zsákutca esetén vissza kell lépni az előző állapothoz, és megvizsgálni, van-e még ott alkalmazható szabály. Ha igen, alkalmazni kell, ha nem, még egy csomóponttal vissza kell lépni (az ábrán szaggatott vonal). Ezt az eljárást nevezik visszalépésnek vagy backtrackingnek. A visszalépés végrehajthatóságának feltétele, hogy mindig nyilvántartsuk annak az útnak a 2.10 ábra: Legjobb út (best first) keresés Természetes nyelvű állítás Ítélet kalkulus forma Esik az eső. Esik Süt a nap. Süt Ha esik az eső,
nem süt a nap. esik ¬süt 2.11 ábra: Egyszerű állítások leírása ítélet kalkulusban csomópontjait, amelyen éppen járunk, hogy adott esetben vissza tudjuk állítani a hozzájuk tartozó állapotot. Az ábrán jól látszik, hogy a mélységi keresésnél bizonyos csomópontokat egyáltalán nem kell megvizsgálni, sőt optimális esetben a célállapot közvetlenül, bolyongás nélkül is elérhető. Legjobb út (best first) keresés: Átmenet a két előző módszer között. Mindig a legígéretesebb csomópontot választja ki, és ahhoz generálja az összes következő állapotot. Így a következtetési gráf egy "állapotfront" mentén növekszik (az ábrán számozott vonalak jelölik), és ebből kell kiválasztani a legígéretesebb csomópontot. A szabálykiválasztást sok esetben ügyesen megszerkesztett költségfüggvények segítségével lehet elvégezni, ahol a költségfüggvény a céltól való távolság és egyéb költségek becslésén
alapulhat (pl. a keresés várható időigénye). 2.35 Problémák leírása és megoldása matematikai logikai eszközökkel Kiindulásul természetes nyelvű állítások szolgálnak. Eszközünk a matematikai logika Természetes nyelvű állítások formális leírásának első és legegyszerűbb (már az ókorban felfedezett) módszere az 2.351 Ítélet kalkulus Az ítélet kalkulus építőelemei szimbólumok egy halmaza, a logikai negálás (¬) és logikai implikáció () valamint zárójelek. Az ezek segítségével felírható logikai kifejezéseket jól formált formuláknak nevezzük. Ilyen jól formált formulákkal leírhatók a valós világ objektumai Lényeges megjegyezni, hogy az implikáció és negálás segítségével a logikai ÉS és VAGY operátorok is kifejezhetők (a ∨ b ≡ ¬a b; a ∧ b ≡ ¬(¬a ∨ ¬b)), tehát a jól formált formulákban ezek is szerepelhetnek, mint a megfelelő azonosságok bal oldalán szereplő kifejezések
"rövidítései". Előnyök: - Egyszerű. - Az ítélet kalkulusban leírt problémák algoritmikusan eldönthetők. Bajok: - - Minden állítás egyedi, így nem derülnek ki a közös vonások. Például: Platón ember. Platónember Szókratész ember. Szókratészember (jobb lenne ember(Platón)) (jobb lenne ember(Szókratész)) Általános állítások csak nagyon körülményesen fogalmazhatók meg. Pl. A "Minden ember halandó" állítás úgy írható le, hogy azt minden ismert emberről különkülön ki kell mondani: Platónember Platónhalandó Szókratészember Szókratészhalandó 2.352 Predikátum kalkulus A predikátum kalkulusban egy- és többváltozós logikai relációkat vezetünk be (ezeket nevezzük predikátumoknak), illetve az axióma rendszerünket kibővítjük az egzisztenciális (∃) és univerzális (∀) kvantorokkal. A predikátumok argumentumai olyan logikai változók, amelyekhez értéket köthetünk a predikátum
kiértékelésekor. A predikátum kalkulusban megfogalmazott állítások alapján különböző kérdésekre lehet választ adni. Pl.: Válaszoljunk a 212 ábra állításhalmaza alapján a "Lojális volt-e Marcus Caesarhoz?" kérdésre Alkalmazzunk visszafelé következtetést, és tegyük föl, hogy a feltett kérdésre a válasz "nem". Ilymódon a célállapot ¬lojális(Marcus,Ceasar) alakban fogalmazható meg. A válasz generálásához használt következtetési gráf a 2.13 ábrán látható Az célállítás igazolható, ha pl. felvesszük a 9. Minden ember személy ∀x ember(x) személy(x) állítást. Innen a kérdéses predikátum a 9 szabály alkalmazásával az 1 tényre vezethető vissza, tehát Temészetes nyelvű állítás Predikátum kalkulusbeli forma 1. Marcus ember volt. ember(Marcus) 2. Marcus popeii volt. pompeii(Marcus) 3. Minden pompeii római volt. ∀x pompeii(x) római(x) 4. Ceasar császár volt. császár(Ceasar) 5.
Minden római vagy lojális volt Ceasarhoz vagy gyűlölte. ∀x római(x) (lojális(x,Ceasar) ∧ ¬gyűlöli(x,Ceasar)) ∨ (¬lojális(x,Ceasar) ∧ 6. Mindenki lojális valakihez. ∀x(∃y lojális(x,y)) 7. Csak olyan személy próbál megölni egy császárt, aki nem lojális hozzá. ∀x∀y (személy(x) ∧ császár(y) ∧ ölnipróbál(x,y)) ¬lojális(x,y) 8. Marcus megpróbálta ölnipróbál(Marcus,Ceasar) megölni Caesart. 2.12 ábra: Egy állításhalmaz és leírása predikátum kalkulusban gyűlöli(x,Ceasar)) igaz. Problémák: - A természetes nyelvű állítások gyakran nem egyértelműek (pl. az 5 állítás látszólag (∀x római(x) lojális(x,Ceasar) ∨ gyűlöli(x,Ceasar)) alakban is felírható, de ez nem elég precíz, hiszen nem fejezi ki azt a nyilvánvaló tényt (amit egyébként az 5. állítás explicit módon nem mond ki!), hogy a gyűlölet és a lojalitás egymást kizáró fogalmak). - Nem mindig triviális a predikátumok és
argumentumok tördelése (pl. ölnipróbál(Marcus) vagy próbál(ölni,Marcus)). Ezt mindig csak annak alapján lehet eldönteni, hogy milyen primitívekben akarunk gondolkozni. - Teljes axiómarendszert kell megfogalmazni, amelyből minden, számunkra lényeges állítás levezethető. (Pl Minden ember személy) - Előre általában nem lehet eldönteni, hogy egy állítást vagy annak a negáltját próbáljuk-e meg bizonyítani. Ha a bizonyítandó állítás az axiómarendszerünkben hamis, akkor az állítás bizonyítása algoritmikusan eldönthetetlen probléma. A valóságban ez nem is olyan nagy gond (ha nincs megoldás egy adott időkorláton belül, adjuk föl, illetve létezhetnek olyan problémák, amelyekre a megállási probléma valójában az esetek csak nagyon korlátozott számában jelentkezik). Bár a predikátumok relációk, így tetszőleges aritmetikai reláció vagy függvény is leírható velük, ez konkrét esetekben nagyon munkaigényes lehet (pl.
gondoljunk a <, > relációk ábrázolására a természetes számok halmazán). Ezért célszerű az eredeti predikátum kalkulus formalizmust számítható predikátumokkal és függvényekkel bővíteni. Pl.: Ha a gt(x,y) számítható predikátummal írjuk le a > relációt, akkor a predikátum egy olyan eljárásként működik, mely az x>y aritmetikai reláció értékét adja vissza. Így pl gt(2,3) hamis, gt(23,2) igaz értéket szolgáltat. Számítható függvények predikátumok argumentumaiként szerepelhetnek. gt(2+3,4) igaz, de kiértékeléséhez szükség van a "+" számítható függvény definiálására. 2.523 A rezolúció Általános következtetési (tételbizonyító) algoritmus, mely szorosan a predikátum kalkulushoz kapcsolódik. A rezolúció az indirekt bizonyítás elvén működik (azaz a bizonyítandó állítás ellentettjét feltételezve ellentmondásra igyekszik jutni). Ez ellentétes az eddigiekben látott eljárásokkal, ahol
közvetlenül a célállapot (ezesetben a bizonyítandó állítás) elérésére törekedtünk. A rezolúció a predikátum kalkulusban megfogalmazott állítások standardizált alakján dolgozik. Tételezzük föl, hogy minden római, aki ismeri Marcust, az vagy gyűlöli Ceasart, vagy azt gondolja, hogy bárki, aki gyűlöl valakit, az őrült. Ennek az állításnak a jól formált formula alakja: ∀x:(római(x) ∧ ismeri(x,Marcus)) gyűlöli(x,Ceasar) ∨ (∀y:∃z:gyűlöli(y,z) őrültnekhisz(x,y)) (1) A cél a mintaillesztés megkönnyítése, tehát az egymásba ágyazások csökkentése, és a kvantorok kiemelése (azaz a változók kvantifikáltságának egységesítése). Ezeket a követelményeket jól kielégíti a konjunktív normál forma, amely a példában szereplő kifejezésre a következő: ¬római(x) ∨ ¬ismeri(x,Marcus) ∨ gyűlöli(x,Ceasar) ∨ ¬gyűlöli(y,z) ∨ őrültnekhisz(x,y) ¬lojális(Marcus,Ceasar) 7. ∀x∀y személy(Marcus) ∧
császár(Ceasar) ∧ ölnipróbál(Marcus,Ceasar) ? 4. 8 ember(Marcus) kellene (az állításhalmaz reprezentációja nem konzisztens) 2.13 ábra: A válasz generálásához használt következtetési gráf A konjunktív normál forma VAGY kapcsolatokkal összekapcsolt predikátum csoportok ÉS 1. Alakítsd át az összes állítást klóz formára. 2. Negáld a bizonyítandó (cél) állítást, az eredményt alakítsd át klóz formára, és add az így keletkező klózokat az 1. lépésben generált klózok halmazához. 3. Válassz ki két klózt úgy, hogy tartalmazzon. Ezek a szülő klózok 4. Rezolváld őket "össze" egymással. Az eredmény (rezolvens) egy újabb klóz, mely L és ¬L kivételével mindkét szülő klóz összes elemének VAGY kapcsolatát tartalmazza. 5. Ha a rezolvens üres, ez ellentmondás, tehát az eredeti célállítás igaz, állj meg. Egyébként add a rezolvenst a klózok halmazához, és folytasd 3-tól. az
egyik L-t, a másik ¬L-t 2.14 ábra: A rezolúció algoritmusa ítélet kalkulusban kapcsolataiból áll. A példaként vizsgált kifejezés egyetlen ilyen csoportot (ún klóz-t) határoz meg Ha több ilyen csoport lenne, ezeket különálló klózokként kezelnénk, mivel a rezolúció ilyen klózokon operál. A konjunktív normál klózokká való alakítás előre meghatározott formális lépések sorozatának végrehajtásával elvégezhető, azonban ezeket most nem részletezzük. A konverzió után olyan klózhalmazt kapunk, amelynek minden eleme szükségképpen igaz kell legyen ahhoz, hogy a teljes állításhalmaz igaz legyen. Ezek után tegyük fel, hogy van két klózunk: a∨b, ¬a∨c. Mivel a és ¬a egyike biztosan hamis, a két állítás egyszerre csak akkor lehet igaz, ha b és c közül legalább egyik igaz, azaz létrehozhatunk egy újabb b∨c alakú klózt, és ezt hozzáadhatjuk a klózhalmazunkhoz. A b∨c klóz úgy keletkezett, hogy a komplementer
tagokat (ezesetben a és ¬a) elhagytuk, és a maradék tagoknak a logikai VAGY kapcsolatát képeztük. Ha ennek az eljárásnak a során üres klóz generálódna, ez az axiómarendszerünk ellentmondásosságára utal. A rezolúció lényegében ezt a tulajdonságot használja ki logikai állítasok indirekt elven való bizonyítására. Az ítélet kalkulusban alkalmazható rezolúciós algoritmust a 2.14 ábra mutatja Az algoritmus működését a következő példán szemléltetjük. Axiómák Klóz forma P (P∧Q)R (S∨T)Q P (1) ¬P∨¬Q∨R (2) ¬S∨Q (3) ¬T∨Q (4) T T Legyen a bizonyítandó állítás: R. A bizonyítás rezolúciós fáját a 2.15 ábra mutatja Predikátum kalkulus esetén a rezolúció algoritmusa alapjaiban nem különbözik, azonban annak eldöntése, hogy két predikátum egymás ellentettje-e vagy sem, már nem olyan egyszerű feladat. Pl Q és ¬Q triviálisan egymás ellentettjei, de mondjuk ember(x) és ¬ember(y) esetén ez csak akkor
dönthető el, ha ismerjük x és y aktuális kötéseit. Így ember(Platón) és ¬ember(Platón) egymás negáltjai, de ember(Platón) és ¬ember(Bodri) nyilvánvalóan nem. ¬P ∨ ¬Q ∨ R ¬R ¬P ∨ ¬Q ¬T ∨ Q P ¬Q ¬T (5) T 2.15 ábra: Az "R" állítás bizonyításának rezolúciós fája az adott axiómák alapján A predikátumok igazságértékének meghatározásához tehát szükség van a bennük szereplő változók értékének meghatározására. Ez mintaillesztéssel történik. A rezolúció predikátum kalkulus esetén az ítélet kalkulusra megadott rezolúciós eljáráson felül a következő kiegészítő lépéseket igényli: A szülő klózok kiválasztása előtt el kell végezni a mintaillesztést. - Ha az egyik szülő klózban szerepel a T1 literál, a másikban pedig a ¬T2 literál, és T1 T2-re illeszthető, akkor a rezolvensből T1 és ¬T2 elhagyandó. T1 és T2 komplementer literálok A következő példa a rezolúció
működését mutatja predikátum kalkulus esetén. Az egyszerűség kedvéért ezúttal közvetlenül az állítások klóz formáját adjuk meg (4.16 ábra) ¬ gyűlöli(Marcus,Ceasar) 5 Marcus/x2 3 ¬római(Marcus) lojális(Marcus,Ceasar) Marcus/x2 2 ¬pompeii(Marcus) lojális(Marcus,Ceasar) - Bizonyítsuk be, hogy Marcus gyűlöli Ceasart. Az állítás klóz alakja: gyűlöli(Marcus,Ceasar). A bizonyítás rezolúciós fáját a 2.17 ábra mutatja. A rezolúció a bizonyítandó állítás ellentettjéből indul ki. A szülő klózoknak csak a sorszámát tüntettük föl, illetve magát a rezolvenst. Megadtuk a szülő klózokban szereplő változók aktuális kötéseit is. 7 lojális(Marcus,Ceasar) Marcus/x4, Ceasar/y1 ¬ember(Marcus) ¬császár(Ceasar) 1 ¬ölnipróbál(Marcus,Ceasar) 4 ¬császár(Ceasar) ¬ölnipróbál(Marcus,Ceasar) ¬ölnipróbál(Marcus,Ceasar) 8 2.17 ábra: A "gyűlöli(Marcus,Ceasar)" állítás bizonyításának
rezolúciós fája A matematikai logikai eljárásokon alapuló tudásreprezentációs és problémamegoldási módszerek formális, egzakt matematikai eszközökkel kezelhető módszereket adnak természetes nyelvű állítások gépi megfogalmazására és ezek segítségével automatikus következtetést tesznek lehetővé. Minden olyan esetben jól alkalmazhatók, amikor: - A problémában szereplő mennyiségek pontosan ismertek (tehát nem kvalitatívek). - A predikátumok binárisak (tehát értékük csak igaz vagy hamis lehet). A rezolúció hatékonyan alkalmazható, de: - Tudatában kell lenni a megállási problémának (ha a módszer adott időkorláton belül nem talál megoldást, akkor fel kell függeszteni a keresést). Ha magyarázat adás szükséges (tehát reprodukálni kell az utat, amelyen keresztül a megoldást elértük), akkor a rezolúció nem alkalmazható, ugyanis a klózkonverzió miatt a kifejezések eredeti szintaktikájától túlságosan elszakad,
ami nem teszi lehetővé emberi felhasználók 1. ember(Marcus) számára egyszerűen értelmezhető 2. pompeii(Marcus) magyarázatok generálását. 3. ¬pompeii(x1) ∨ római(x1) 4. császár(Ceasar) 5. ¬római(x2) ∨ lojális(x2,Ceasar) ∨ gyűlöli(x2,Ceasar) 6. lojális(x3,S1(x3)) 7. ¬ember(x4) ∨ ¬császár(y1) ∨ ¬ölnipróbál(x4,y1) ∨ ¬lojális(x4,y1) 8. ölnipróbál(Marcus,Ceasar) 2.16 ábra: Az állítások klóz alakja 2.4 Procedurális programozás 2.41 A procedurális programozási nyelvek fejlődése A bonyolult szoftverrendszerek fejlesztésének alapjául közvetlenül vagy közvetett módon mind a mai napig valamiféle magasszintű, procedurális programnyelvet használnak. A maggasszintű programnyelvek elképesztő változatosságot mutatnak, amiből első pillantásra úgy tűnik, hogy a számítástechnikusok nem szívlelték meg az ősi bábeli példát, nem rettentek meg következményeitől, sőt még keményen dolgoztak is érte, hogy a
bábeli zűrzavar bekövetkezzen. Persze ez csak a dolog felületes megítélése. A valóságban konzekvens fejlődési folyamatnak lehetünk tanúi, melyet az egyre bonyolultabbá váló szoftverrendszerek fejlesztésének igénye kényszerített ki. Ennek a fejlődési folyamatnak a leglényegesebb lépéseit vesszük sorra ebben a pontban. A napjainkig megjelent programozási nyelveket általában négy generációba szokás csoportosítani, melyeket alapvetően a nyelv kidolgozásának időpontja határoz meg. Ez azért lehet hasznos számunkra, mert a generációk alapján leszűrhetők egy adott korszak alapvető célkitűzései és eredményei. A generációk és főbb jellegzetességeik tehát a következők: Első generációs nyelvek (1954-58): Alapvető célkitűzésük a matematikai kifejezések kezelése, és bonyolult számítások egyszerű programozásának lehetővé tétele. Magasabb szintű absztrakciós eszközöket nem tartalmaznak. Ebbe a csoportba tartozik a
Fortran I és az Algol 58 - Második generációs nyelvek (1959-1961): Megjelennek a magasabb szintű absztrakciós eszközök, így az adattípusok használata, eljárások, függvények és a blokkstruktúra. Ebbe a csoportba tartozik a Fortran II és az Algol 60. A második genarációs nyelvek közé tartozik néhány sikeres célnyelv is, mint amilyen például a COBOL (adatkezelési célra) és a Lisp (szimbolikus programozás céljára). - Harmadik genarációs nyelvek (1962-70): Alapvetően a második generációs nyelvek finomításai (így például az Algol 68 az Algol 60 továbbfejlesztett, bonyolult, és végletesen precíz utódja, a Pascal pedig az Algol 60 továbbfejlesztett, de egyszerűségre és áttekinthetőségre törekvő utódja). Bizonyos fokig kilóg a sorból a szintén Algol 60 utód Simula, mely az objektumorientált nyelvek ősatyjának tekinthető. - Genarációs rés (1970-1980): Ezrével születtek új nyelvek, közülük azonban csak kevesek maradtak
fönn az utókor számára. A legsikeresebb túlélő a C, mely egyike a legelterjedtebben használt, és legnagyobb hatást kifejtő nyelveknek. A C alapvető jellegzetessége az Algol vonallal szemben az egyszerűségre és hatékonyságra törekvés (akár egy Forma1 versenyautó: igen gyors, és fantasztikus mutatványokra képes, de borzasztó könnyű tönkretenni). Ebben az időszakban tisztul le a párhuzamos és valós idejű programozás fogalomrendszere, és ezzel együtt számos olyan nyelv születik, amely lehetővé teszi a párhuzamos programozást (pl. Concurrent Pascal, Modula). - Negyedik genarációs nyelvek (1980 után): Ide tartoznak többek között az objektumorientált nyelvek és a deklaratív nyelvek. Ezt úgy is értelmezhetjük, hogy a programnyelvek világa két táborra szakadt. A deklaratív tábor legjelentősebb tagjai az SQL és a Prolog (valamint különböző változataik). Az objektumorientált tábor legjelesebb képviselői a SmallTalk, a C++ és
újabban a Java. A fenti, lényegében időrendi csoportosítás mellett érdekes tendenciák figyelhetők meg az adatok és a rajtuk műveleteket végző algoritmusok viszonyának változásában. - Az első generációs nyelvekben a műveletek nem strukturáltak, vagyis az adatok és a rajtuk elvégzendő műveletek egységes kezelésére nincsenek nyelvi konstrukciók. A második generációs nyelvekben megjelennek az adatsruktúrák, és a strukturált, több szinten egymásba ágyazott megvalósítású absztrakt műveletek (eljárások és függvények). Az adatok és a rajtuk elvégezhető műveletek még mindig elkülönülnek. A harmadik generációs nyelvekben a nagy rendszerek csapatmunkában való létrehozását támogatandó megjelennek a moduláris programtervezés, programfejlesztés és fordítás eszközei, azaz a programok a második generációs absztrakciós eszközökön kívül procedúrákból és adatokból álló nagyobb egységekből, modulokból vagy
csomagokból állíthatók össze egyetlen nagy működő egységgé. Az adatok és a rajtuk végrehajtandó műveletek egységes kezelése a negyedik genarációs nyelvekben jelent meg, mégpedig kétféle megközelítésben: Az első lehetőség az absztrakt adattípusok használata. Az absztrakt adattípus olyan, felhasználó által definiált típus, amely lehetővé teszi a típus értékkészletének és a típus elemein végzett műveleteknek olyan egységes módon való definiálását, amely a későbbi felhasználók számára megengedi adott típusú elemek deklarálását és kezelését, de elrejti az adattípus adatszerkezetét és a rajta végezhető műveletek implementációjának részleteit. Az absztrakt adattípusok használatát az Ada támogatja a csomagokon (package) keresztül. - A másik lehetőség az objektumorientált programozás, amelyről mindaz elmondható, mint az absztrakt adattípusokról általában, ezen kívül azonban még jónéhány egyéb
dolog is. Ezekkel a kérdésekkel a következő fejezetben meglehetősen részletesen foglalkozunk. - 2.42 Objektumorientált programozás Az objektumorientált program a felfogásunk szerint együttműködő objektumok (object) összessége. A világról alkotott képünk objektumokból épül föl. Az objektumokhoz tulajdonságokat rendelünk, és megjegyezzük, hogy ‘mire valók’, azaz a segítségükkel vagy éppen rajtuk, milyen műveleteket hajthatunk végre. Mikor a hétköznapi életben egy új problémát akarunk megoldani, általában nem egyedi objektumokból, hanem általános objetumféleségekből indulunk ki. Így aztán már kisgyerek korunkban sem okoz gondot olyan megállapítások megtétele, hogy a szél hideg és fúj, a farkas pedig szőrös és harap. Lényegében ugyanezeken az elveken alapul az objektumorientált programozási paradigma is. Népszerűségének és hatékonyságának egyik titka minden bizonnyal a való világgal, és a hétköznapi
gondolkodásunkkal való magától értetődő kapcsolata. A program alapépítőkövei az objektumok. Ezek olyan, a környezetüktől jól elkülöníthető, viszonylag független összetevők, amelyeknek saját viselkedésük, működésük és lehetőleg rejtett, belső állapotuk van. Az objektumokra hatnak a környezetükben lévő egyéb objektumok, és ennek hatására az objektumok belső állapota megváltozhat. Minden objektum valamilyen osztályba (class) tartozik. Az osztályok megfelelnek az absztrakt adattípusoknak, így minden objektumnak van osztálya, melynek az adott objektum a példánya, vagy más szóval egyede (instance). Az osztályok definiálják az egyes objektumok állapotát leíró adatszerkezetet és a rajtuk végezhető műveleteket, az úgynevezett módszereket (method). Az egyes egyedek csak az állapotukat meghatározó adatszerkezet tényleges értékeiben különböznek egymástól, a módszerekkel definiált viselkedésük közös. Az egyes
objektumok aktivizálhatják egymás módszereit, melyet módszerhívásnak (method invocation) vagy üzenetküldésnek (message passing) nevezünk. Ilyen módon az egyes objektumok hatással vannak egymásra, szolgáltatásokat kérnek másoktól és szolgáltatásokat nyújtanak a környezetük számára. Az egyes osztályokat az öröklődés hierarchiába rendezi. Az öröklődés az az eljárás, amelynek segítségével egy osztály felhasználhatja a hierarchiában felette álló osztályokban definiált állapotot (adatszerkezeteket) és viselkedést (módszereket). Így a közös elemeket elegendő egyszer, a hierarchia megfelelő szintjén definiálni. Az olyan programozási nyelvet, amely csak az osztály-egyed koncepciót támogatja, az osztályok közötti hierarchikus öröklődést azonban nem, objektumalapúnak (object based) nevezzük. A többszintű öröklődést is támogató nyelvek objektumorientált (object oriented) nyelvek. Az objektumalapú nyelvek elemei
már korábbi programozási nyelvekben is megjelentek. Ezeket többnyire absztrakt adattípusoknak nevezték. Kezdetben mi is ezzel foglalkozunk 2.421 Absztrakt adattípusok és objektumok Történetileg az objektumorientált programozás alapja a minden magasszintű programnyelvben megjelenő absztrakt adattípus, illetve az ehhez kapcsolódó objektum fogalma. A maggasszintű programnyelvekben adatnak tekintünk mindent, amin egy program utasításai műveletet végezhetnek. Az adatoknak típusa van, mely a programozó számára minden adatelemhez három alavető tulajdonságot rendel: - Értékkészlet: az adatelem lehetséges értékeit meghatározó halmaz. Műveletek: az értékkészlet elemein végrehajtható tevékenységek. Az értékkészlet a műveletekre nézve nem szükségképpen zárt. - Jelölés: Az adott típusú adatokra való hivatkozás szintaxisa. Az adattípus a fordítóprogram számára is lényeges információkat hordoz, nevezetesen: Egy adott típus
elemeihez adott méretű és adott szerkezetű hely tartozik a memóriában, ez teszi lehetővé a fordítónak az automatikus helyfoglalást. - A műveletek típusokhoz köthetők, így ellenőrizhető az adatok felhasználásának helyessége. A különböző típusú adatokon elvégezhető műveletek helyességének ellenőrzése szempontjából az egyes nyelvek meglehetősen széles skálán mozognak. A két véglet a szigorúan típusos nyelv (ahol két eltérő típusú adat között nem végezhető művelet, csak explicit típuskonverzió után), illetve az automatikus típuskonverziót végző nyelvek (ahol az ésszerűség határain belül tetszőleges típusú adatok között végezhetők műveletek, és a típusátalakítást a fordítóprogram automatikusan elvégzi). Korszerű nyelvekben minden adatnak van típusa, és ezt a programozónak deklarálnia kell ("ki kell jelentenie") az adatelem első felhasználása előtt. - A jó adatabsztrakciós eszköz
biztosítja az absztrakt adattípus mindhárom elemének egységes kezelését, továbbá képes egy alkalmas felület megmutatásával elrejteni a típus implementációjának részleteit a későbbi felhasználók szeme elől. Ezt nevezik információrejtésnek. Az absztrakt adattípus csak egy általános minta, ezért rajta közvetlenül nem hajthatók végre műveletek. Az objektum olyan adatelem, melyhez típust, tárolóhelyet és nevet rendelünk, és ettől kezdve rajta műveleteket hajthatunk végre. Az adott objektumra a nevével hivatkozhatunk, mely az objektum jellemzőit (attribútumait) is hordozza. 2.18 ábra: Egyszerű objektumok elemei Jellemzők (attribútumok) megadása: - Deklaráció 9: minden objektumról (statikus típushozzárendelés esetén) még a használata előtt ki kell jelenteni, hogy milyen típusú, összetett típus esetén azt is, hogy milyen típusú elemekből áll. - Dinamikus típushozzárendelés: a változók típusát nem kell előre
deklarálni, a változó első előfordulásakor, (mely tipikusan értékadás), automatikusan rendelődik hozzá típus az aktuális programkörnyezetnek megfelelően. 9 Kijelentés, kinyilatkoztatás. 2.422 Az objektumok jellemzői Bár a 2.42 pont első mondatában kijelentettük, hogy az objektumorientált program objektumok összessége, és a 2.421 pontban a hagyományos programnyelvek objektumfelfogását is tisztáztuk, azt valójában még nem részleteztük, mit is jelent az objektum fogalma objektumorientált megközelítésben. A 242 pontban az objektumokról röviden elmondtuk, hogy egy osztály egyedeként jönnek létre (és később meg is szűnhetnek), egyedváltozóik vannak, melyek értéke meghatározza az objektum mindenkori állapotát; továbbá hogy az objektumok képesek meghatározott műveletek végrehajtására, azaz meghatározott viselkedést produkálnak. Mindezek alapján az objektum fogalma a következőképpen definiálható: Az objektum belső
állapottal, saját viselkedéssel, azonossággal és élettartammal rendelkező egység. A hasonló viselkedésű és struktúrájú objektumok felépítését közös osztályuk definiálja. A fenti definícióból következően az objektum (object) és az egyed (instance) fogalmak egymás szinonímái. A továbbiakban részletesen megvizsgáljuk az objektumoknak a fenti definícióban megjelenő tulajdonságait. 2.4221 Belső állapot A mindennapi életünkben megjelenő objektumok belső állapottal rendelkeznek, mely alapvetően befolyásolja pillanatnyi viselkedésüket. Természetesnek vesszük például, hogy egy lámpa nem kezd világítani, amíg meg nem nyomjuk a kapcsolóját, illetve hogy ugyanez a lámpa kialszik, ha a kapcsolót még egyszer megnyomjuk. Mindez annak a következménye, hogy a lámpa belső állapottal rendelkezik, melyet kapcsolójának állása tárol, és amely a lámpához kötődő egyes tevékenységek végrehajtása között folyamatosan megmarad.
Már a fenti, végletesen egyszerű példa alapján is leszűrhető, hogy az objetumok belső állapota két alapvető elemet tartalmaz: - az objektumok tulajdonságai (esetünkben az a tény, hogy a lámpának van kapcsolója); - az egyes objektumtulajdonságok értékei (esetünkben a kapcsoló ki vagy bekapcsolt volta). Ojektumorientált programnyelvekben az objektumok tulajdonságai az egyedváltozókban öltenek testet, melyek típusát és szerkezetét az osztályuk határozza meg, értéküket azonban maga az objektum tárolja. Így az azonos osztályba tartozó, de különböző objektumok egymástól teljesen független életet élnek. Az objektumok egyedváltozói lehetnek: - nyilvánosak: ilyenkor bárki, aki az objektumhoz hozzáfér, közvetlenül, értékadással megváltoztathatja az egyedváltozók értékét, ezzel az objektum állapotát; - belsők: ilyenkor az egyedváltozók értéke csak korlátozásokkal (tipikusan csak módszereken keresztül) változtatható
meg. Ez a megoldás strukturáltabbá teszi a rendszert, ezért ha osztályokat fejlesztünk, jobban szeretjük majd 10. Az egyedváltozók láthatósági viszonyai konkrét objektumorientált nyelvekben ennél jóval árnyaltabbak. A kérdésre később még visszatérünk 2.4222 Viselkedés Az objektum viselkedése az a mód, ahogyan állapotváltozásain valamint elfogadott és elküldött üzenetein keresztül a környezetének eseményeire reagál és azokra hatással van. 10 A sors iróniája, hogy mint felhasználók, nem biztos, hogy rajongani fogunk érte, mert ez a módszer bonyolítja az egyedváltozókhoz való hozzáférés szintaxisát. Az objektumok által végrehajtható tevékenységeket módszereik határozzák meg, amelyek leginkább az objektum egyedváltozóin végeznek műveleteket, tehát az objektumok szoros egységbe foglalják az összetartozó algoritmus- és adatelemeket. Objektumorientált nyelvekben a módszereken keresztül végrehajtható
műveleteknek a következő általános kategóriái figyelhetők meg: Az objektum állapotát módosító művelet. Az egyedváltozók értékeit írja Az objektum állapotát lekérdező művelet. Az egyedváltozók értékeit olvassa Olyan művelet, mely az egyedváltozók értékeit olvassa és írja meghatározott sorrend szerint. Konstruktor: Új egyed létrehozására, és kezdeti állapotának beállítására szolgáló művelet. Destruktor: Egyed megszüntetésére szolgáló művelet. Csak azokban a nyelvekben létezik, amelyek nem alkalmaznak automatikus szemétgyűjtést (pl. C++) Egyes nyelvekben (pl. Smalltalk, Java) a műveleteket csak módszerekként lehet létrehozni, mert a nyelv nem engedi meg osztályokon kívüli eljárások és függvények definiálását. Más nyelvek (pl C++, CLOS, Ada) azonban megengedik, hogy különböző műveleteket szabad alprogramok gyanánt definiáljunk. Az összetartozó szabad elemeket általában csomagokba szervezik, melyeket
segédprogramok (class utility) néven szoktak emlegetni. Módosító: Kiválasztó: Iterátor: Mivel az objektumok belső állapottal rendelkeznek, és a módszerek végrehajtása során ez a belső állapot megváltozhat, az objektumok mindenkori állapotát nem csak a rajtuk végrehajtott módszerek, hanem a végrehajtás sorrendje is befolyásolja. Ez más szóval azt jelenti, hogy az objektumok viselkedése véges automataként írható le. Ebből a szempontból kétféle típusú objektum különböztethető meg. Az aktív objektumok saját vezérlési szállal rendelkeznek, így képesek saját belső állapotuk megváltoztatására, míg a passzív objektumoknak nincsen saját vezérlési száluk, és állapotuk csak külső kérés eredményeként változhat meg. 2.4223 Azonosság Egy objektum azonossága az a tulajdonsága, amelynek alapján bármely más objektumtól megkülönböztethető. Mivel az objektumok azonossága minden művelet elvégzésekor fontos (nyilván nem
mindegy, melyik objektum hajtja végre az üzeneteket), az előző pontban felsorolt alapvető műveletek körét most két továbbival kell bővítenünk. Ezek az értékadás (azaz, amikor egy objektumot egy másik objektumnak adunk értékül) és az egyenlőségvizsgálat. Ha felidézzük a 2.421 pontban leírt klasszikus objektumfogalmat, objektumok azonossága háromféle módon kezelhető: Név szerint Referencia alapján Két objektum neve megegyezik. Két objektumot akkor tekintünk azonosnak, ha az értéküket tároló tárhelyre mutató referencia megegyezik. Érték szerint Két objektumot akkor tekintünk azonosnak, ha értékük megegyezik. A név szerinti azonosság triviálisnak tűnik, de valójában nagyon ritkán használatos. Ennek oka, hogy a magasszintű nyelvekben egy objektum neve az előfordulási környezettől függően vagy az értékre mutató referenciát (balérték) vagy magát az értéket jelöli (jobbérték). Valódi név szerinti azonosság csak
olyan környezetben fordulhat elő, amely nem ismeri a mutató fogalmát, és az objektumokat nem cím, hanem név szerint kezeli (pl. Oracle Forms) A név szerinti hivatkozás tehát általában nem használható objektumok egyenlőségének vizsgálatára, és értékadásra sem. Objektumok egyenlőségvizsgálata és értékadása történhet referencia és érték alapján. A referencia alapján végzett összehasonlítás egyszerű (két mutató egyenlőségét kell vizsgálnunk), és egyértelmű (ugyanis nyilvánvaló, hogy egy adott kezdőcímen egyszerre két vagy több objektum nem helyezkedhet el). A referencia alapján végzett értékadás azt jelenti, hogy egy adott objektumra hivatkozó referenciát egy másik változóban tárolt érték formájában megkettőzzük. Ezt nevezik strukturális megosztásnak (structural sharing), melynek során az objektum azonossága (vagyis az objektum tárterülete és annak tartalma) nem kettőződik meg. Emiatt a referencia
alapján végzett értékadást szokás sekély másolásnak (shallow copy) is nevezni. Ennek az a következménye, hogy az adott objektumon minden olyan változón keresztül végezhetők műveletek, amely az adott objektumra hivatkozó referenciát tárolja, és az objektumon végrehajtott műveletek hatása minden ilyen változón keresztül látszani fog. A referencia alapján végzett másolás módja általános, azaz nem kell hozzá semmilyen járulékos támogatást (pl. módszert) írni az egyes objektumok osztályaiban Az érték alapján végzett összehasonlítás objektumfüggő, ezért azokra az osztályokra, amelyekre érték szerinti összehasonlítást kell végeznünk, ezt külön módszer formájában meg kell írni. A bonyodalom abból származik, hogy az osztályok egyedváltozó struktúrája nem egyetlen változóból, hanem változók egy rekordjából áll, így az összehasonlításhoz az egyes mezők értékeit kell összevetni. A helyzetet tovább
bonyolítja, hogy egyes egyedváltozók objektumreferenciákat is tartalmazhazhatnak, így az érték szerinti egyenlőségvizsgálat objektumok egész láncolatának a vizsgálatát igényelheti. Az érték alapján végzett értékadás kicsit furcsán hangzik, és általában nem is így emlegetik, hanem mély másolásnak (deep copy) hívják. Ilyenkor az értékadáskor maga az objektum kettőződik, azaz a tartalma átmásolódik egy másik, azonos szerkezetű és tartalmú memóriatartományba. Az objektum és másolata ettől kezdve teljesen független életet élnek, tehát az objektum azonossága megkettőződik. A továbbiakban a két objektum lehet érték szerint azonos, referencia alapján azonban soha nem lesznek azok. 2.4224 Élettartam Objektumok létrehozása speciális módszerek, ún. konstruktorok és egy erre a célra szolgáló kulcsszó (általában new) segítségével történik. Egy osztály konstruktorának neve általában megegyezik az osztály nevével.
A módszerek többértelműségéből következően egy osztálynak több konstruktora is létezhet különböző paraméterekkel. A következőkben azt vizsgáljuk, mi történik egy egyed létrehozásakor. Az osztály konstruktorának aktivizálásakor a nyelvi környezet futtatórendszere megfelelő méretű munkaterületet foglal a dinamikus memóriában (halom, heap) az új objektum számára, és ennek a memóriaterületnek a kezdőcímét visszaadja, mint az új objektum azonosságát. Az egyedváltozó struktúra inicializálása a konstruktor feladata, melynek törzse csak a munkaterület lefoglalása után hajtódik végre. A feladatukat betöltött, feleslegessé vált objektumok megszüntetésével kapcsolatban az egyes nyelvek kétféle utat követnek: Explicit fölszabadítás: Egy objektum csak akkor szűnik meg, ha erre a programozó explicit módon (delete vagy free) utasítást ad. Ezt a módszert alkalmazza az Ada és a C++ Előnye a hatékonyság, hátránya, hogy
az objektumok fölszabadítását a programozónak kell kézben tartania, és ha valamit elszúr ezzel kapcsolatban, az legtöbbször végzetes: a föl nem szabadított objektumokkal betelik a memória, a szükségtelenül fölszabadítottakra pedig még vannak hivatkozások, amelyek ettől kezdve egészen furcsa helyekre írnak és onnan olvasnak majd, ami a programok legváltozatosabb módon (de legtöbbször memóriavédelmi hibával) való elszállását eredményezi. - Automatikus szemétgyűjtés: Bármely objektum által lefoglalt tárhely automatikusan fölszabadul (ezáltal az objektum megszűnik), ha az objektumra már nem létezik hivatkozás. Erről automatikus szemétgyűjtő algoritmusok gondoskodnak, melyek különböző kritériumok hatására (pl. a dinamikus memória betelt, vagy eltelt egy adott idő), beindulnak, és elvégzik a szükséges műveleteket. Ezt a módszert támogatja a Java, a CLOS és a Smalltalk Az objektumok bizonyos esetekben túlélhetik az őket
létrehozó programot. Ebből a szempontból kétféle típusú objektumot különböztethetünk meg: - Tranziens: Perzisztens A tranziens objektumok a program futásának befejeződésével automatikusan megszűnnek, munkaterületük fölszámolódik, és a program legközelebbi indításakor már más példányok keletkeznek. A perzisztens objektumokat valamilyen nagykapacitású, nemfelejtő tárban (tipikusan lemezen) tároljuk, így ezek állapota és egyéb jellemzői nem szűnnek meg a program futásának befejeződésével, és azonosságuk az egyes futások között fönnmarad. A perzisztens objektumok szolgálnak az objektumorientált adatbáziskezelők alapjául. 2.423 Az objektumok közötti kapcsolatok Egy objektum önmagában haszontalan, hiszen az objektumorientált programozás legfőbb ereje éppen a komplex problémák hierarchikus dekompozíciójában rejlik, amelynek eredményeként a feladatot egymással együttműködő objektumok halmaza oldja meg. Ennek
során az egyes objektumok felhasználják más objektumok szolgáltatásait, és végrehajtják más objektumok kéréseit. Az objektumok közötti kapcsolatnak két formája ismeretes, mely egyben az objektumhierarchia jellemzőit is meghatározza. Ezek a kapcsolat (link) vagy más szóval alárendeltségi viszony, illetve az összetétel (aggregation), más szóval szülő gyerek kapcsolat. A következőkben ezek jellegzetességeit részletesen elemezzük. 2.4231 Kapcsolatok A kapcsolat az objektumok között fönnálló fizikai vagy koncepcionális összeköttetés. A kapcsolatokon keresztül veheti igénybe egy objektum egy másik objektum szolgáltatásait, illetve a kapcsolatokon keresztül járható be az objektumhierarchia. A kapcsolatokon keresztül az objektumok üzeneteket küldhetnek egymásnak (módszerhívások vagy eredmények). Egy kapcsolatban az objektumok háromféle szerepet játszhatnak: Műveleteket végez más objektumokon, de rajta nem végeznek műveletet
mások. Az aktív objektum szinonímája Végrehajtó objektumok csak üzeneteket küldenek, de nem fogadnak, így a hozzájuk tartozó kapcsolatok az objektumból kifelé irányulnak. Kiszolgáló (server) Más objektumokon sohasem hajt végre műveleteket, csak külső kéréseket hajt végre. A passzív objektum szinonímája A kiszolgáló objektumok csak üzeneteket fogadnak, de nem küldenek, így a hozzájuk tartozó kapcsolatok az objektumba befelé irányulnak. Ügynök (agent) Műveleteket végez más objektumokon, és külső kéréseket is végrehajt. Az ügynökobjektumokhoz tartozó kapcsolatok kétirányúak. Hogy egy A és egy B objektum között kapcsolatot létesíthessünk, teljesülnie kell bizonyos láthatósági feltételeknek. Ha A üzenetet akar küldeni B-nek, a B objektum az A számára látható kell legyen Ezt a következő módszerek egyikével lehet elérni: Végrehajtó (actor) - B globális objektum A-ra nézve. - B az A-n végrehajtott művelet
paramétereként ismert A számára. - B része A-nak. - B az A által végrehajtott művelet lokális változója. A megfelelő módszer kiválasztása tervezési döntés eredménye. Amikor két objektum egy kapcsolaton keresztül üzenetet küld egymásnak, fölmerülhet a kommunikáció és szinkronizáció problémája. Mindez azon múlik, hogy a két objektum külön vezérlési szálon (tehát végeredményben külön folyamatként vagy szálként) hajtódik-e végre, vagy egyetlen, tervezési szinten szekvenciális program részeként. Ha a végrehajtási környezet szekvenciális, akkor a szinkronizáció mindössze magát a módszerhívást jelenti, ami alig bonyolultabb, mint egy egyszerű eljáráshívás. Ha aktív objektumokat használunk, melyek külön vezérlési szállal rendelkeznek, ezeket a 2.2 pontban leírt elvek alapján eleve úgy kell megírnunk, hogy rájuk a kölcsönös kizárás, kommunikáció és szinkronizáció feltételei megvalósuljanak.
További problémát jelenthet, ha több konkurens módon futó aktív objektum ugyanarra a passzív objektumra hivatkozó kapcsolattal rendelkezik, és a kapcsolaton keresztül végre akarja hajtani a passzív objektum egy módszerét. Ilyen esetben a passzív objektum háromféle módon viselkedhet: Szekvenciális A passzív objektum csak akkor működik helyesen, ha egyidőben csak egy kérést kell kiszolgálnia. Őrzött A passzív objektum helyesen működik több párhuzamos kérés kiszolgálásakor is, de ehhez a kérést küldő aktív objektum közreműködése szükséges (vagyis a kölcsönös kizárás és a szinkronizáció megoldása az aktív objektum feladata). Szinkron A passzív objektum garantálja a kölcsönös kizárást és a szinkronizációt, így több párhuzamos kérés kiszolgálásakor is helyesen működik, és ebben a kérést küldő aktív objektumnak nem kell szerepet vállalnia. Ez tulajdonképpen megegyezik a monitor koncepció
sajátosságaival. 2.4232 Összetétel Míg a kapcsolatok ügyfél/kiszolgáló viszonyt fejeznek ki, ahol az egyik objektum kapni akar, a másik pedig adni, az összetétel rész/egész kapcsolatot ír le, ahol az egyik objektum a másiknak alkatrésze, eleme, és a nagyobb egységek a kisebbekből épülnek föl. A rész-egész hierarchia mindig fa struktúrájú, és lehetővé teszi az objektumstruktúrával modellezett rendszer elemeinek bejárását az egésztől a részek irányába vagy fordítva. Az összetett objektumokat tárolónak (container) is szokták nevezni. Ez utóbbi elnevezés azonban nem minden összetétel esetén helyénvaló, mert nem biztos, hogy egy összetétel részeként megjelenő objektum fizikailag is része az egésznek. Ha mondjuk egy autót akarunk modellezni, a kerekek és a motor fizikailag is részei az autónak, ezzel szemben ha például bélyeggyűjtőket és a bélyegeiket akarjuk jellemezni, a bélyegek ugyan mind a gyűjtőkhöz tartoznak,
de azért a gyűjtők nyilvánvalóan nem a bélyegeikből állnak. A kapcsolatok és az összetétel összehasonlítása alapján elmondható, hogy az összetétel valamivel jobban támogatja a hierarchikus dekompozíciót, ahol a dekompozíció egy szintjén állva nem kell az alsóbb szintekkel foglalkoznunk, míg a kapcsolatok az objektumok lazább, így rugalmasabb szervezését biztosítják. 2.424 Az osztályok jellemzői Az elemzés és tervezés szemszögéből az objektumosztály közös tulajdonságokkal és viselkedéssel rendelkező objektumok halmaza, így minden objektum egy adott osztályba tartozó példány. Az implementáció szemszögéből az objektumosztály adatabsztrakciós eszköz - végső soron egy minta -, melynek segítségével tetszőleges számú, hasonló viselkedésű, és tulajdonságú objektumpéldány generálható. Az osztály és objektum közötti legalapvetőbb különbség tehát, hogy az osztály egy koncepció, az objektumok pedig ennek a
koncepciónak a megvalósításai. A helyzet olyasmi, mint amikor az ember bemegy a McDonalds-be, és rábök a pult fölött látható hamburgerképre, és közli, hogy ő azt kéri. Igen meg lennénk lepve, ha erre a kiszolgáló leakasztaná nekünk a falról a képet, hogy azt megetesse velünk. E helyett olyan, valódi hamburgert kapunk, amely - jó esetben - megszólalásig hasonlít a képen láthatóra, és ráadásul még ehető is. Ilyenkor a kép számunkra olyan felület, mely hordozza a valódi hamburgerrel kapcsolatos, számunkra lényeges információkat. Az ‘implementáció’, azaz az étel elkészítése nem a mi dolgunk, feltéve, hogy nem mi vagyunk a szakácsok. A kép alapján a hamburger minden vendég számára könnyedén reprodukálható, elvileg tetszőleges számban. Minden vendég külön hamburgert kap, amelyek azonban mégis hasonlóak. Ezt a kis példát azért érdemes megszívlelni, mert első hallásra bármily furcsa, az objektumorientált
programok írása közben az ember olykor véletlenül, vagy egyszerűen csak a tudatlanságából kifolyólag, dühödt kísérleteket tesz a hamburger képének megevésére - azaz osztályokon olyan műveletek végrehajtására, amelyek az egyedekhez kötődnek. Az objektumorientált programozás úgy is fölfogható, hogy az objektumosztályok szerződések (contract), vagyis ígéretek arra vonatkozóan, hogyan fognak viselkedni az adott osztályba tartozó objektumok a külvilág (azaz a rendszerben működő többi objektum) felé. Ily módon az objektumosztály két alapvető részre különül el, ezek a felület és az implementáció. 2.4241 Felület Az objektumosztály szerződése lényegében a módszereinek, és a más objektumok számára hozzáférhető egyedváltozóinak a specifikációja. A szerződésben általában változók és konstansok szerepelnek, valamint módszerek lenyomatai (fejlécei), azaz nevük, paramétereik esetleg visszaadott értékük. Egy
osztály felülete más osztályoktól örökölt elemeket is tartalmazhat. Láthatóságuk alapján az osztályok felületének háromfajta elemét (egyedváltozóját vagy módszerét) különböztethetjük meg: Nyilvános (public) Bárki, aki az osztály egyedéhez hozzáfér, látja a nyilvános elemeket is. Védett (protected) A védett elemeket csak az adott osztályba tartozó objektumok, az adott osztály alosztályaiba tartozó objektumok, illetve az osztállyal deklaráltan együvé tartozó objektumok láthatják (pl. C++: friend) Saját (private) A saját elemek csak az adott osztályon belül, láthatók. 11 Az objektumosztály felülete absztrakt adattípusként viselkedik: megadja az értékkészletet, megadja a műveleteket és a jelölést, és a fordítóprogram számára lehetővé teszi annak meghatározását, hogy egy objektum létrehozásakor mekkora helyet kell lefoglalni a memóriában. A felület egyedváltozó elemei az osztály minden egyedére nézve
azonosak, ezek értékei azonban osztályonként különbözőek. Az éttermi példánknál maradva a hamburger egyedi tulajdonsága lehetne, hogy van-e benne mustár, vagy a szalvéta, amibe becsomagolják, azonban szükségtelen például az árát minden hamburgerre rányomni, ezt nyilvánvalóan elég a képe mellé egyszer kiírni. Azokat a változókat, amelyek egy adott osztály minden egyedére nézve közösek, és csak egyetlen példányban léteznek, osztályváltozóknak nevezzük. Az osztályváltozók lényegében a hagyományos programnyelvekben alkalmazott globális változók megfelelői egy osztály egyedein belül. Az osztályváltozók megjelenésével szükségképpen megjelennek olyan módszerek is, melyek magán az osztályon (pontosabban az osztályváltozókon) végeznek műveleteket (a példánkban ilyen lehetne a hamburger árának megváltoztatása). 2.4242 Implementáció A szerződésben vállalt (azaz a felületen keresztül elérhető és
végrehajtható) elemek konkrét megvalósítása. Az implementáció legfontosabb eleme a módszerek komplett definíciója beleértve a fejlécet, törzset és az esetleges segédfüggvényeket (eljárásokat). 2.425 Osztályok közötti kapcsolatok Akárcsak az objektumok, az osztályok sem egymástól elszigetelt egységek. Ez alól a szabály alól kivétel a C++ friend konstrukció, mely lehetővé teszi olyan osztályok megadását, amelyek láthatják egy adott osztály saját elemeit. Innen ered a C++ körökben közszájon forgó mondás: "Friends can touch each other’s private parts." A friend koncepció - bár a gyakorlatban olykor jól jöhet - cseppet sem elegáns. 11 Az osztályok közötti kapcsolatok három alapvető formáját különböztethetjük meg: Általánosítás/specializálás Az ilyen jellegű kapcsolatotot szokás is a (az egy) nevezni. 12 relációnak is Példák: Morzsa (az egy) kutya. (Morzsa a kutyák osztályába tartozó egyed.)
A kutya (az egy) emlős (A kutya az emlősök osztályába tartozó alosztály13.) Rész/egész Az ilyen jellegű kapcsolatot szokás part of (része) relációnak is nevezni. Példa: Az orr része a kutyának Társítás Laza, szemantikai alapon szerveződő kapcsolat két osztály között, melyek amúgy látszólag nincsenek összeköttetésben. Társítás létesíthető például a kutya és a postás között. Az egyik állat, a másik ember A postásnak ugyan semmi dolga a kutyával, a kutya azonban szereti a postást (harapdálni). Ezeknek a kölcsönhatásoknak a leírására az objektumorientált nyelvek a társítás (association), öröklődés (inheritance), összetétel (aggregation), használat (using), sablonok (templates) és a metaosztály (metaclass) eszközöket kínálják. Bár az egyes objektumorientált nyelvek a felsorolt eszközöket általában csak részben tartalmazzák, a továbbiakban ezen egymáshoz rendelési eszközök tulajdonságait elvi szinten
részletesebben szemügyre vesszük. 2.4251 Társítás Az társítás szemantikus kapcsolatokon alapuló kétirányú összeköttetés, azaz általában nem kötjük meg a társított objektumok közötti navigáció14 irányát. Ha egy kutya megharap egy postást, a postást ismerve jogunk van tudni, melyik kutya volt az, és a kutyát ismerve tudhatjuk, melyik postás volt az áldozat. A társítás olyan összeköttetés, melyben lényeges a társított objektumok számossága. Így a társítás lehet: Egy-egy Egy adott objektumot pontosan egy másikkal társítunk. (Pl férj-feleség) Egy több Egy adott objektumot több másikkal társítunk. (Pl török szultán és háreme) Több-több Bármely objektum több másikkal társítható. (Az előbbi két példánk alapján könnyedén sikerült erre is példát konstruálnunk, de ennek közlését a cenzúra megtiltotta.) 2.4252 Öröklődés Az öröklődés osztályok közötti reláció, melyben egy adott osztály
osztozik egy vagy több másik osztály szerkezetén (egyedváltozó struktúra) és viselkedésén (módszerek). Ha az A osztály örököl a B osztálytól, A alosztálya (leszármazottja) B-nek, illetve B szuperosztálya (szülője) A-nak. 12 Az is a reláció angolul leírva jól formált mondatokat eredményez, a fordítása (az egy), azonban magyarul nem csak erőltetett, hanem kifejezetten rosszul is hangzik. Ezen a nehézségen úgy próbáltunk segíteni, hogy (mivel nyelvileg nincs rájuk szükség, a példánk szempontjából azonban lényegesek) az az egy szavakat zárójelek közé tettük. 13 Itt természetesen az osztályra mint objetktumorientált programozási fogalomra, és nem mint állatrendszertani kategóriára gondolunk. Navigációnak nevezik azt a műveletet, amelynek során egy objektumhalmaz bejárható, vagyis egyik objektum ismeretében eljuthatunk más, valamilyen módon hozzá kapcsolódó objektumokhoz. 14 Az öröklés során az alosztálynak
joga van szuperosztályának viselkedését bővíteni illetve szűkíteni is 15. Valós programozási helyzetekben gyakori, hogy egy alosztály mindkettőt megteszi Az osztályok és alosztályaik (amelyeknek szintén lehetnek további alosztályaik) öröklési hierarchiát (osztályhiererchiát) alkotnak. Az öröklési hierarchián belül elvileg minden osztálynak lehetnek egyedei, de a legvalószínűbb mégis az, hogy a legalacsonyabb szinten elhelyezkedő osztályok lesznek azok, amelyekből végül egyedeket generálunk. Ez a sajátosság az objektumorientált programtervezés hierarchikus dekompozíción, és többszintű absztrakción alapuló megközlítésmódjából fakad. Ezen az alapon kétfajta osztályt különböztethetünk meg: absztrakt osztályt (amelynek nincsenek egyedei) és valós osztályt (amelynek vannak egyedei). Az absztrakt osztályokat eleve úgy hozzuk létre, hogy viselkedésüket az alosztályok majd bővítik. Ez általában azt is jelenti, hogy a
módszereiket nem implementáljuk, csak a felületet definiáljuk, és az implementáció az absztrakt osztályra hivatkozó alosztályokra marad. Az osztályhierarchia csúcsán elhelyezkedő osztályt alaposztálynak (base class) nevezik. Egy alkalmazáson belül számos alaposztály lehet. Másrészt egyes nyelvek (pl Smalltalk, Java) az alaposztályt (többnyire Object néven) nyelvi szinten, előre definiált osztályként kezelik. Ezek után bármely osztály szolgáltatásaira hivatkozhatnak osztályok (öröklés) és egyedek (példánygenerálás). Az egyedek létrehozásakor természetesen nem kell megismételnünk mindazt, amit az osztálydefinícióban már elmondtunk. Elég csak magát az osztályt megneveznünk, és esetleg néhány paramétert megadnunk. Az egyed tehát örökli az osztálytól az egyedváltozó struktúra szerkezetét és a módszereket. Amikor egy B osztályt definiálunk, hivatkozhatunk egy C szülőosztályra. Ekkor B örökli C egyedváltozó
struktúráját és módszereit. B ezeken kívül új módszereket és egyedváltozó struktúra elemeket is definiálhat. Ha B olyan néven definiál egyedváltozót, vagy olyan lenyomattal definiál módszert, ami már korábban C-ben létezett, az újonnan definiált elemek elfedik a régieket. De vajon mi történik olyankor, ha egy A osztályból származtatott több alosztály is definiál ugyanolyan m néven módszereket? Egyrészt a több különböző alosztályban definiált azonos nevű módszerek törzse, így az általuk produkált viselkedés is különböző, másrészt az összes alosztályba tartozó egyedek egyben A egyedeinek is tekinthetők, hiszen leszármazottjai A-nak is. Képzeljük el azt a gyakorlatban meglehetősen sűrűn előforduló esetet, hogy az A alosztályaiból generált különböző egyedeket a közös A típus alapján akarjuk kezelni. Ilyenkor azt várjuk, és valójában az is történik, hogy ha végrehajtjuk egy egyed m módszerét, akkor
mindig az adott objektum közvetlen osztályára érvényes módszer hajtódik végre anélkül, hogy tudnánk az objektum pontos típusát (osztályát). Az objektumorientált rendszereknek ezt a tulajdonságát nevezik polimorfizmusnak (sokféleség). A hagyományos, típusorientált adatabsztrakcióra épülő nyelvekben (pl. Ada) a fent leírt működés csak úgy valósítható meg, hogy az aktuális objektum típusának kikeresése után egy case struktúrában típus szerint szét kell ágaztatni a vezérlést, és a művelet végrehajtásakor explicit módon meg kell határoznunk az objektum típusát. Az előbbi gondolatmenetünket kicsit más szemszögből vizsgálva a kérdés úgy is fölvethető, lehet-e egyetlen osztálynak több, azonos nevű módszere. A válasz általában az, hogy igen, feltéve, hogy a módszerek lenyomata (azaz egyes paramétereik vagy visszaadott értékük típusa) különböző. Ilyenkor az adott osztályhoz tartozó objektumok kezelésekor a
módszer végrehajtásának pillanatában, az aktuális paraméterek típusa alapján dől el, hogy pontosan melyik módszer fog végrehajtódni a sok közül. Ezt a tulajdonságot többértelműségnek (overloading) nevezzük A polimorfizmus és a többértelműség egyaránt azt eredményezi, hogy bizonyos lényeges döntések nem születhetnek meg fordítás közben, csak futásidőben. Mindez lényegében egyenértékű a késői típushozzárendelés (late binding vagy dinamic binding) elvével. A késői típushozzárendelés minden A szűkítés a gyakorlatban egy létező módszer olyan módon való felüldefiniálásával történik, mely a szülő osztály azonos nevű módszerénél kevesebbet (vagy éppen semmit sem) csinál. 15 előnye és rugalmassága ellenére költségesebb, mint az egyszerű eljáráshívás, hiszen futásidejű keresést igényel. Hogy mindez mégsem rontja jelentősen az objektumorientált nyelvek hatékonyságát, annak két oka is van.
Egyrészt a módszerek többsége nem többértelmű, és nem is polimorf Másrészt az objektumorientált nyelvekben a többértelmű és polimorf módszerek kikeresésére speciális, indexelt keresőtáblákat és gyorsítótárakat (cache) alkalmaznak, melyekkel a hívás többletideje 20-30% alá csökkenthető, és az ilyen arányú teljesítménycsökkenés már nem tűnik nagy áldozatnak cserébe mindazért, amit a polimorfizmus és többértelműség nyújt. Attól függően, hogy egy osztály definíciójakor hány másik osztályt nevezünk meg ősként, az öröklődésnek két fajtáját különböztethetjük meg: - Egyszeres öröklődés (rész-egész hierarchia): Bármely osztály csak egyetlen másik osztályt nevezhet meg őseként, és ettől örökölhet tulajdonságokat. Ez természetesen nem jelenti azt, hogy csak egyetlen osztálytól örökölhetők tulajdonságok, hiszen maga az öröklési lánc ettől még lehet többszintű. Az egyszeres öröklődés
esetén az osztályhierarchia mindig fa struktúrájú Ezzel az öröklési mechanizmussal kiválóan leírhatók olyan rendszerek, melyek komponensekből és részkomponensekből állnak. Nyilvánvaló ugyanis, hogy ha egy alkatelem egy adott részrendszer alrendszere, akkor közvetlenül nem tartozik egyetlen más részrendszerbe sem. Ilyen öröklődési hierarchiát valósít meg a Java és számos 4GL fejlesztő környezet (pl. Oracle Forms) - Többszörös öröklődés (osztály-egyed hierarchia): Egy osztály akárhány más osztálytól közvetlenül örökölhet tulajdonságokat és módszereket. Az osztályhiererchia hurokmentes gráf struktúrájú. Ezt a módszert támogatja a C++ A többszörös öröklődés látszólag rugalmasabb az egyszeres öröklődésnél, használata azonban az osztályhierarchia sokkal nagyobb strukturális bonyolultsága miatt számos furcsasághoz vezethet. Ilyen például az osztályhierarchia hurokmentességének problémája (az előző
esetben ez fel sem merül, hiszen a fák mindig hurokmentesek). További probléma az egy adott osztálytól több úton való öröklés. Tegyük föl például hogy B és C osztályok egyaránt az A osztály leszármazottai. A tartalmaz egy x nevű, t1 típusú egyedváltozót, melyet B felüldefiniál egy t2 típusú x-szel, C azonban egyszerűen csak örökli. Ha a D osztály ősként B-t és C-t is megnevezi, kérdés hogy a D által látott x, vajon t1 vagy t2 típusú-e. A megoldás kvalifikált hivatkozás lehet (tehát B.x ill Cx), ez azonban szükségessé teszi D használatakor az ősei szerkezetének ismeretét is, ami nem túl szívderítő. 2.4253 Összetétel Éppúgy, ahogy az objektumoknál, az összetétel az osztályok esetében is rész/egész hierarchiát fejez ki, mely lehet fizikai tartalmazás vagy szülő/gyerek kapcsolat. A tartalmazás módja lehet: Érték szerinti Egy adott osztály egyedei fizikailag tartalmazzák az alosztályok egyedeinek
munkaterületeit. Ilyenkor a munkaterületek fizikailag egymásba ágyazottak, és az osztály és alosztályainak egyedei nem jöhetnek létre és nem szűnhetnek meg egymástól függetlenül, azaz közös életet élnek. Referencia szerinti Az osztály egyedei ebben az esetben csak egy referenciát tartalmaznak a megfelelő alosztályok elemeire. Ilyenkor az osztály és alosztályainak egyedei fizikaikag elkülönülnek, egymástól függetlenül keletkeznek és szűnnek meg, tehát külön életet élnek. Az összetétel irányított reláció, ugyanis a rész nyilvánvalóan eleme az egésznek, de az egész természetesen nem eleme a résznek (matematikusok és filozófusok kéretnek a fülüket befogni). Figyeljünk rá, hogy össze ne keverjük az osztályok összetételét a többszintű örökléssel! A két eset a megoldandó probléma elemzése során könnyen megkülönböztethető. Ha nem tudjuk minden kétséget kizáróan belátni, hogy két osztály között az egy
típusú reláció áll fönn, akkor ez a kapcsolat nem öröklődéssel, hanem valamely más módon írható le. 2.4254 Sablonok Amikor egy osztályt hagyományos módon definiálunk, meg kell adjuk az egyedváltozók típusát, majd a módszereket úgy kell megírni, hogy az adott típusú egyedváltozókon végezzenek műveleteket. Előfordulhat azonban, hogy úgy akarunk egy osztályt létrehozni, hogy előre tudjuk, hogy az adott osztály funkcionalitását többféle típusú adatra is biztosítani akarjuk. A példa kedvéért tegyük föl, hogy t1 és t2 típusú elemekből akarunk várakozási sorokat felépíteni és kezelni. A sorokon nyilvánvalóan ugyanolyan műveleteket akarunk végrehajtani (pl elem betétele az utolsó után, első elem kivétele, üresség tesztelése), melyek algoritmusa is azonos függetlenül attól, hogy a várakozó elemek típusa t1 vagy t2. Mivel a típusdeklaráció a programszövegnek statikus része, a kétféle típusú elemen végzett
azonos műveletek tisztességesen viselkedő programnyelvekben első közelítésben csak úgy valósíthatók meg, ha minden újabb adattípusra vonatkozóan az adatstruktúra mellet az elvégezendő műveleteket is újradefiniáljuk, melyek azonban csak a változók típusdeklarációiban különböznek 16. Mindez nem csak felesleges munkának tűnik, hanem hibalehetőségeket is takar (pl. a szövegrészeket átmásoljuk, de a típusnevekre való hivatkozásokat csak részben sikerül lecserélni), ráadásul a program karbantartása ettől kezdve jóval nehézkesebbé válik (a műveleteken végrahajtott változtatásokat ugyanis annyi helyen kell elvégeznünk, ahány különböző típust akarunk kezelni). Ezt a problémát oldják meg a sablonok (template 17). Ezek olyan osztályok, melyek definíciójában paraméterként típusnevek (osztályok nevei) is szerepelhetnek. A sablonokból közvetlenül nem hozhatók létre egyedek, ehhez a sablonparaméterek helyén az aktuális
típusnevek (osztálynevek) megadásával először megfelelő osztályokat kell létrehoznunk a sablonokból. A fenti példánkat ezzel a módszerrel úgy oldhatnánk meg, hogy létrehozunk egy S(T) sablont, melyben megvalósítjuk a várakozási sor általános viselkedését. A sablonban a változó deklarációkban és a módszerdefiníciókban a megfelelő helyeken nem konkrét típusok, hanem a típusparaméter neve, azaz T szerepel. Ezután a T helyén konkrét típusneveket megadva létrehozhatunk annyi osztályt, ahányféle típusú elemet kezelni akarunk, például: S<t1> qt1; S<t2> qt2; Mostantól a qt1 osztály segítségével létrehozhatunk olyan objektumokat, melyek t1 típusú elemek sorainak kezelésére képesek, és a qt2 osztály segítségével olyan objektumokat, amelyek t2 típusú elemek sorainak kezelésére alkalmasak. Bár a sablonok első pillantásra érdekes konstrukciónak tűnnek, használatuk többnyire elkerülhető az öröklődés és
polimorfizmus eszközeivel, ráadásul az öröklődés tágabb fogalom. Ezért, míg a C++ ismeri a template (sablon) fogalmát, a Java ezt az elvet a nyelv egyszerűsítésének érdekében, a kis veszteség, kis bánat elvének alapján elvetette. 2.4255 Metaosztály A metaosztály olyan osztály, melynek egyedei osztályok. Bizonyos szempontból a sablonok is egyfajta metaosztálynak tekinthetők, ezek lehetőségei azonban korlátozottak. A legtöbb objektumorientált nyelv nem támogatja a metaosztályokat, vagy ha mégis, visszafogott formában teszi. A metaosztályok gyakori megnyilvánulásai az osztályváltozók, melyek egy adott osztályhoz tartozó objektumok globális változóinak tekinthetők. Az osztályváltozókat már jellemeztük a 2424 pontban, most inkább csak arra az eddig nem részletezett filozófiai ellentmondásra szeretnénk utalni, hogy bár Megjegyzendő, hogy vannak nyelvek, amelyek ebből a szempontból nem tisztességesek. Ilyen például a C,
amelyben a void* típusú mutatón keresztül bármilyen típusú elem elérhető, majd a cast műveletek segítségével a kívánt típusúként értelmezhető. 16 17 A C++ sablon helyett a generikus osztály (generic class) vagy paraméterezett osztály (parameterized class) elnevezések is használatosak. az osztály egy minta, melyen közvetlenül műveletek nem végezhetők, és pusztán arra szolgál, hogy segítségével objektumokat generáljunk, ezt az elvet az osztályváltozók megjelenése - amelyekre egyébként a gyakorlatban nagy szükségünk van - megsérti. Ez az elvi rés betömhető, ha már magukat az osztályokat is egy magasabbszintű metaosztály elemeinek tekintjük. (Vigyázat, nem öröklésről van szó!) A metaosztályokat jelenleg csak a CLOS (Common Lisp Object System) kezeli tisztességesen. A metaosztályok segítségével az objektumorientált környezetnek számos eleme megváltoztatható, így például az objektumok létrehozására és
törlésére szolgáló default konstruktor és destruktor módszerek és még sok egyéb. A CLOS a metaosztályok létrehozásának és kezelésének módját Common Lisp Metaobject Protocol néven definiálja, melynek bonyolultsága már csak abból is sejthető, hogy bár csupán a Common Lisp egy nyelvi alrendszeréről van szó, külön nevet kapott, és csak az igen vájtfülű, nagyon bonyolult problémákon dolgozó programozók jutnak el odáig, hogy ezt az alrendszert használják is. 2.43 Java Ismeretes, hogy a Java a C++ nyelvből született, sok helyen egyszerűsítve, esetenként bővítve azt. 2.431 Egy példaprogram Tapasztalataink szerint egy programozási nyelvet a száraz ismertetés helyett példákat követve könnyebb megérteni. Persze nem könnyű az ismerkedés elejére érthető, rövid, de nem túlzottan egyszerű példát találni. A bevezető példa a Internet hálózat világába visz el bennünket. A drótposta (email) továbbítására itt az ún.
SMTP (Simple Mail Transfer Protocol) protokoll használatos A levéltovábbító programok (Message Transfer Agent, általában a sendmail program) a 25-ös TCP kapun várakoznak arra, hogy valaki felépítse velük a kapcsolatot. Ha a kapcsolat létrejött, egy nagyon egyszerű, karakterorientált párbeszédbe kezdenek a hívóval. A párbeszéd ritkán használt, de néha nagyon hasznos parancsa, a VRFY (Verify) segítségével megkérdezhetjük egy SMTP kiszolgálótól, hogy egy adott nevű felhasználónak van-e a gépen postaládája. A mi programunk pontosan ezt csinálja. A java SMTPclient <host> <user> parancs hatására a host számítógép SMTP szerverétől a user felhasználó felől érdeklődik, a kapott választ pedig kiírja a konzolra. Lássunk egy példa párbeszédet a két gép között! Dőlt betűkkel a megszólított SMTP kiszolgáló (a hapci.mmtbmehu gépen), normális betűkkel az ügyfél, a mi programunk (a tudormmtbmehu gépen) üzenetei
láthatók. A példa egy sikeres és egy sikertelen lekérdezést is tartalmaz 220 hapci.mmtbmehu 565c8/BMEIDA-144 Sendmail is ready at Wed, 14 Feb 1996 17:31:03 +0100 HELO tudor.mmtbmehu 250 Hello tudor.mmtbmehu, pleased to meet you VRFY kiss 250 Kiss Istvan <kiss> VRFY duke 550 duke. User unknown QUIT 221 tudor.mmtbmehu closing connection Látható, hogy a kiszolgáló minden üzenetét egy számmal is kódolja. A program szerkezete A programok import utasításokkal kezdődhetnek, felsorolva a programban felhasznált könyvtárak nevét. Ez nem kötelező, az egyes könyvtári elemekre való hivatkozásnál is megadhatjuk a könyvtár nevét. C programozók figyelem: ez nem #include parancs, nem a fordítóprogramnak szóló üzenet a forrásszöveg beolvasására. Az import a kapcsolatszerkesztőnek, betöltőnek szól, már lefordított kódú könyvtárakra hivatkozik. Importálásnál csomagokból (package) osztályokat (class) importálunk, ezzel a programunkban
felhasználhatóvá tesszük őket. A csillag az adott csomag összes osztályát jelenti import java.net*; import java.io*; import java.lang*; import java.util*; Ezután rögtön egy osztálydeklarációnak kell következni, a Java programban minden változó, minden utasítás csak osztályok törzsében szerepelhet. Eltűntek a C globális változói, globális függvényei Aki most lát először C++-ból átvett osztálydeklarációt, ne essen kétségbe, egyelőre fogadja el, hogy még a legegyszerűbb Java program is a class <osztálynév>{ . } mintát követi. Az osztálynév-ben, mint a Java minden azonosítójában, a kis- és nagybetűk különbözőek. A mi konkrét osztályunk definíciója tehát valahogy így kezdődhetne: class SMTPclient { Változók és értékek Az osztályok belsejében változókat deklarálhatunk, a deklaráció szintaxisa egy-két módosító alapszó (static vagy final) kivételével megfelel a C-nek: típus változónév [ =
kezdeti érték ] A nyelv tartalmaz néhány beépített, egyszerű adattípust, amelynek neve és értékkészlete a következő táblázatban található: egész típusok byte 8 bites kettes komplemens short 16 bites kettes komplemens int 32 bites kettes komplemens long 64 bites kettes komplemens float 32 bit IEEE 754 lebegőpontos Látható, hogy csak valós típusok előjeles egész típusok double 64 bit IEEE 754 lebegőpontos vannak, bár ez nem char 16 bites Unicode karakter kód jelenti azt, hogy nem karakter típus lehet rájuk például a logikai boolean true vagy false szokásos bitműveleteket alkalmazni. Eltérés a C-től az is, hogy itt az egyes beépített típusok mérete, helyfoglalása nyelvi szinten definiált, és nem függ a programot futtató gép architektúrájától. A karakterek tárolásánál a nemzetközileg elfogadott, 16 bites Unicode kódkészletet használják. Az osztályunk első sora így: static final int SMTPport = 25; A Javában
nincs #define, sem a C++ const-ja, ennek a legközelebbi megfelelője a final kulcsszó. A fordítóprogram gondoskodik róla, hogy az így deklarált változó értéke ne változhasson meg. Látható, hogy a változó deklarációjával együtt kezdeti értéket is adhatunk neki, ez persze final változó esetén kötelező. A static módosító egyelőre ne zavarjon bennünket, pontos jelentésére majd az osztályok ismertetésénél térünk ki. Elöljáróban csak annyit, hogy a static kulcsszóval bevezetett változódeklarációk osztályváltozók, és ezek közelítik meg leginkább a szokásos nyelvek globális változóit. A beépített numerikus típusokon a C-ből ismert műveletek értelmezettek. A numerikus típusú értékekkel a szokásos aritmetikai műveletek használhatók. Az egész típusú értékekre használható bit műveletek kiegészültek a >>> operátorral, amely jobbra léptetésnél 0-t és nem az előjelet lépteti be a legnagyobb
helyiértékű bitre. Ezek az operátorok értékadással is kombinálhatók, pl: +=, /=, A logikai értékek a C-vel ellentétben nem egész típusúak. Azonos típusú értékek összehasonlítására használt operátorok logikai értéket állítanak elő, ilyen értékekre használhatók az ismert logikai műveletek is. A következő táblázat tartalmazza a Java nyelv összes operátorát precedenciájuk csökkenő sorrendjében (némelyikről egyelőre még nem beszéltünk): ++ -! ~ instanceof (pre- vagy poszt-) inkremens és dekremens, logikai és bitenkénti negálás, típus ellenőrzés * / % szorzás, osztás, moduló + - összeadás, kivonás << >> >>> bitenkénti léptetések < > >= <= összehasonlítások == != egyenlő, nem egyenlő & bitenkénti AND ^ bitenkénti XOR | bitenkénti OR && logikai AND || logikai OR ? : feltételes kifejezés = += -= *= /= %= ^= &= |= <<= >>=
>>>= különböző értékadások Az egyszerű, beépített típusokon túl a Java csak kétfajta összetett adattípust tartalmaz, a programozók által definiálható osztályokat (class) illetve a tömböket (array), amelyek egyébként teljes értékű osztályok. Nincs struct, union, de nincs typedef sem, sőt mutatók (pointer) sincsenek! Ezek alapján példaprogramunk további sorai: static Socket smtpconn; static DataInputStream instream; static DataOutputStream outstream; A fenti 3 sor a szabványos Java könyvtárakban definiált osztályok - Socket, DataInputStream, DataOutputStream -egy-egy példányának, egyedének (objektum) foglal helyet, pontosabban ezek egyelőre csak üres hivatkozások, az objektumok még nem jöttek létre. Megjegyzések a programban // Program: drótposta címek ellenőrzése SMTP kiszolgáló // segítségével // Használata: // java SMTPclient <host> <user> A Java örökölte a C-ből a /* . */ stílusú megjegyzés
szintaxist, a C++-ból a //-rel kezdődő egysoros megjegyzéseket, végül a /* . */ alakú megjegyzések a kiegészítő dokumentációs rendszerrel (javadoc) együtt használhatók. A főprogram Mivel a Javában minden utasítás csak osztályok definíciójában szerepelhet, minden Java alkalmazásnak, pontosabban az egyik osztályának tartalmaznia kell egy main nevű, a következőkben megadott fejlécű függvényt: public static void main (String args[]) { A public a módszer láthatóságáról nyilatkozik, jelentése: bárhonnan meghívható. A static jelentéséről egyelőre annyit, hogy az eljárás csak osztályváltozót használ, void a módszer visszatérési értékének típusa, azaz nem ad vissza értéket. A Java alkalmazás futtatását a virtuális gép a main eljárás végrehajtásával kezdi A main eljárás paramétere egy szövegtömb, amelynek egyes elemei a parancssorban megadott argumentumokat tartalmazzák. A szövegek tárolására, kezelésére
szolgáló String egy előre definiált osztály, nem pedig karaktertömb, mint a C-ben. A tömbök indexértékei itt is nullától kezdődnek, viszont itt az args[0] nem a program nevét adja vissza, mint a C-ben, hanem ténylegesen az első argumentumot. Akinek hiányzik a megszokott argc argumentumok száma paraméter, ne aggódjon, a Java tömbök méretét le lehet kérdezni futás közben. Az egyes eljárásokban természetesen használhatunk lokális változókat, itt a String osztály egyedére lesz szükségünk, amelyet a deklarációjával egyidejűleg a new parancs segítségével létre is hozzuk: String res = new String(); Lokális változókat a módszerek törzsében tetszőleges helyen deklarálhatunk, sőt az egyes programblokkok - { . } - saját, máshonnan nem látható lokális változókkal rendelkezhetnek Vezérlési szerkezetek Kezdjük a programunkat azzal, hogy megvizsgáljuk, megfelelő számú argumentummal hívták-e meg! Ha a programnak nem 2
argumentuma volt (az SMTP kiszolgáló és a keresett felhasználó neve ), tájékoztató üzenetet írunk ki, és befejezzük a program működését: if (args.length != 2) { System.outprintln("Usage:"); System.outprintln(" java SMTPclient <host> <user>"); System.exit(1); } A System könyvtár a Java programok futtatásához szükséges objektumokat, módszereket tartalmazza. Ilyen a programok szabványos kimenetét (standard output, pl. konzol periféria) megvalósító out objektum. A kiíró eljárás a print illetve println, amelynek csak egyetlen paramétere van, de elég "intelligens" ahhoz, hogy karakteres formában tetszőleges típusú értéket (igen, még a felhasználó által definiált objektumokét is) "megjelenítsen". A programunkban csak szövegek kiíratására fogjuk használni. A println módszer a paraméterének kiírása után még egy új sort is kiír A programból vagy a main módszer befejezésével,
vagy a System könyvtár exit módszerének meghívásával lehet kilépni. A Java nyelv átvette a C vezérlési szerkezeteit. Az elágazásokhoz az if-else és switch; ismétlésekre, ciklusszervezésre a for, while és do-while utasítások használhatók. Eltérés, hogy az összes feltételnek logikai értéknek kell lennie, int nem használható, ezért a fordítóprogram kiszűri a C programozók "kedvenc" hibáját, amikor az if utasítás feltételrészében értékadást ("=") írunk egyenlőség-vizsgálat ("==") helyett. A nyelvből kimaradt az ugró utasítás (goto), de megmaradtak a címkék (label). A címkéket ciklusutasítások megjelölésére használhatjuk, így a ciklus törzsében kiadott break és continue utasítások nem csak a legbelső, hanem bármelyik, címkével ellátott beágyazó ciklusra hivatkozhatnak. Kivételkezelés Van még egy utasításcsoport, amely a programvégrehajtás sorrendjét befolyásolhatja, a
try-catch-throw csoport, amely nagyon hasonló formában a C++ nyelvben is megtalálható. A tisztességesen megírt C programok legnagyobb problémáját az egyes eljárások végrehajtása közben esetleg előbukkanó hibák, ún. kivételek (exception) lekezelése jelenti Kisebb gondossággal megírt programoknál a programozó hajlamos elfeledkezni arról, hogy (legtöbbször a függvény visszatérési értékének tesztelésével) ellenőrizze, sikerült-e a végrehajtott művelet. Ha mégis megteszi, leggyakrabban egyszerűen befejezi a programot, ugyanis olyan bonyolult a hiba előbukkanását és okát "felfelé" terjeszteni, és a megfelelő helyen lekezelni azt. Ezen segíthetnek a fenti utasítások. Most csak felületesen ismerkedhetünk meg a kivételkezelés rejtelmeivel, majd később részletesen visszatérünk a témához. try { Egy try blokkba zárt utasításcsoporton belül bárhol előforduló kivétel hatására a program normális futása abbamarad,
és a vezérlés automatikusan a catch blokkra kerül. Itt a catch paramétereként megkapjuk a hiba okát, általában egy Exception típusú objektumot, aztán kezdjünk vele, amit tudunk. A példaprogramunk egyszerűen kiírja a hiba okát. A programozó maga is definiálhat ilyen kivételeket, amiket (no meg az egyes catch blokkokban lekezelhetetlen kivételeket) a throw utasítással felfele passzolhatunk, amíg valaki, legrosszabb esetben a virtuális gép, "lekezeli" őket. A program törzse A programunk szerencsére nagyon egyszerű: kiépítjük az SMTP kapcsolatot az első argumentumként megadott géppel: openSMTPconnection(args[0]); Elküldünk egy HELO üzenetet, hozzáfűzve a saját gépünk nevét (részletekről majd máskor), a visszakapott választ (res tartalma) nem használjuk. res = sendMessage("HELO " + java.netInetAddressgetLocalHost()getHostName()); Elküldünk egy VRFY üzenetet a második argumentumként megkapott felhasználó
nevével. A visszakapott választ kiírjuk. res = sendMessage("VRFY " + args[1]); System.outprintln(res); Elküldjük a búcsúzó QUIT üzenetet, és lezárjuk a kapcsolatot. res = sendMessage("QUIT"); closeSMTPconnection(); } Bármi hiba történt, kiírjuk a program kimenetre. catch(Exception e) { System.outprintln("Error: " + etoString()); } }; Egész egyszerű, nemde? Persze csaltunk. Azért ilyen egyszerű minden, mert "magasszintű", a feladathoz alkalmazkodó eljárásokat használtunk. Ezeket még definiálni kell, mert sajnos nem szerepelnek a Java könyvtárakban. Módszerek Emlékezzünk arra, hogy egy osztály - SMTPclient - belsejében vagyunk! Az osztályok belsejében definiált függvényeket az objektumorientált terminológia szerint módszereknek (method) hívják. Definíciójuk nem különbözik a C-ben megszokottaktól, persze a mi programunkban a main-hez hasonlóan itt is meg kell adni a static módosítót, mert ezek
a módszerek is használják a fent definiált osztályváltozókat. static void openSMTPconnection (String host) throws IOException { Persze a korábban megismert kivételkezelés belezavar a képbe: a Java nyelv megköveteli, hogy a módszerekben előforduló kivételeket vagy le kell kezelni, vagy deklarálni kell a tényt, hogy lekezeletlenül továbbadjuk. A módszer fejében a throws kulcsszó erre szolgál (Megjegyzés: a lekezeletlen kivételek deklarálásának szabálya alól is vannak "kivételek", de erről is csak később.) Hálózati kommunikáció A programhoz szükséges hálózati kommunikációt szerencsére a Java net könyvtára segítségével könnyű megoldani. A részletek mellőzésével: a könyvtár definiál egy Socket osztályt, amely egyszerű TCP kapcsolat kiépítésére, és azon adatok átvitelére szolgál. Először létrehozunk egy új Socket-et, egyben megnyitva az összeköttetést a paraméterként megkapott gép 25-ös (SMTP
kiszolgáló) kapujával (port). smtpconn = new Socket(host, SMTPport); Az átvitelre két - input és output - stream objektum szolgál, amelyeket a get.Stream függvénnyel kibányászunk a kapcsolatból. instream = new DataInputStream(smtpconn.getInputStream()); outstream = new DataOutputStream(smtpconn.getOutputStream()); Ezen stream-ek segítségével már közvetlenül olvashatunk vagy írhatunk a kiépült kapcsolaton. Itt az első beérkező sort - az SMTP kiszolgáló bejelentkező üzenetét - egyszerűen eldobjuk. String dummy = instream.readLine(); }; A kapcsolat lezárása egyszerű: static void closeSMTPconnection () throws IOException { smtpconn.close(); }; Egy SMTP üzenet kiküldéséhez egy kicsit ügyeskednünk kell: static String sendMessage(String msg) throws IOException { Először is a kiküldendő üzenetet - a String típusú msg paramétert - Byte-okká alakítva kell elküldeni. Ne feledjük el, hogy a Java a karakterek tárolására - és egy szöveg
is ezekből áll - 16 bites Unicode-ot használ, a szegény SMTP kiszolgáló alaposan meg lenne lepve, ha ezt kapná. A Byte-sorozat végére még odabiggyesztjük a protokoll által megkívánt CR és LF karaktereket. Legalább látunk arra is példát, hogyan lehet - úgy, mint a C-ben - karakter konstansokat megadni. outstream.writeBytes(msg); outstream.write( 15); outstreamwrite( 12); A flush utasítás kiüríti az átviteli puffert, átküldi az összegyűlt byte-okat az összeköttetésen. outstream.flush(); Az SMTP szerver egy ilyen üzenetet nem hagy válasz nélkül, az egysoros választ beolvassuk és visszaadjuk a módszert meghívó programnak. Az utasítás arra is példa, hogy változót (res) nem csak a módszerek elején, hanem bárhol deklarálhatunk, ahol Java utasítás állhat. String result = instream.readLine(); return result; }; } Fordítás, futtatás Ha valaki nem sajnálja a fáradságot, és begépeli a fenti programot, mondjuk SMTPclient.java
néven, lefordítani a javac SMTPclient.java paranccsal lehet. Ha nem követtünk el gépelési hibát, és a Java Fejlesztői Környezetet (JDK) is helyesen telepítettük, akkor a fordító létrehoz egy SMTPclient.class állományt, ami a lefordított bytekódot tartalmazza. Az állomány neve nem a forrás nevéből származik, hanem az állományban definiált osztály nevét hordozza. Ha több osztályt definiálnánk, a fordító több különálló állományt hozna létre. A programunkat tesztelhetjük például a java SMTPclient hapci.mmtbmehu kiss paranccsal, és kis szerencsével a válasz: 250 Kiss Istvan <kiss> lesz, akinek a neve minden bizonnyal ismerősen cseng. 2.432 Objektumorientált programozás Java módra Az objektumorientáltság manapság az egyik legdivatosabb programozási paradigma (alapelv). Természetesen a Java is követi a divatot. Bár a nyelv őse a C++ volt, a Javában sokkal tisztábban érvényesülnek az objektumorientált programozás
elvei, talán azért, mert nem kényszerül rá, hogy a C nyelvből származó programozási elveket, szerkezeteket koloncként magával hordozza. 2.433 Absztrakt adattípusok létrehozása A Java nyelvben csaknem minden nyelvi egység objektum. Mint korábban láthattuk, egy programban az import utasításokon kívül minden utasítás osztályok belsejében szerepelhet csupán, nincsenek globális változók, globális eljárások. A nyelv ugyan tartalmaz néhány egyszerű adattípust, de ezeken felül minden egyéb adatszerkezet valamilyen osztályba tartozó objektum vagy tömb. Mint azt hamarosan látni fogjuk, a tömb ugyancsak egyfajta speciális osztály. Mellesleg a nyelv tartalmaz a beépített típusokat becsomagoló osztályokat (wrapper class) is. Ezek a Boolean, Character, Double, Float, Integer. Objektumok Az objektumok olyan programösszetevők, amelyek szoros egységbe foglalják az állapotukat leíró belső adatszerkezetüket és a rajtuk értelmezhető
műveleteket. Ebben az értelemben az egyes objektumok nagyon hasonlítanak egy adott típusba tartozó értékekhez. Egy int típusú érték is elrejti előlünk a belső reprezentációját, és csak az előre definiált műveletek végezhetők rajta. A Javában minden egyes objektumnak meghatározott típusa kell legyen, azaz valamelyik osztályba kell tartoznia. Az objektumokra változókon keresztül hivatkozhatunk, pl a Person somebody; azt jelenti, hogy a somebody név mindig egy Person típusú objektumra hivatkozik. Viszont a C++ nyelvvel ellentétben a fenti deklaráció nem jelenti egy új objektum létrejöttét, a változó egyelőre nem hivatkozik semmire, azaz "értéke" null. A Person somebody = new Person("Kovács", "János", "15502280234"); utasítás nem csak definiál egy új változót, de a new paranccsal létre is hoz egy új objektumot, amelyre a változó hivatkozik. Nagyon fontos megértenünk, hogy a Javában minden
változó értéke referencia csupán. Ezt szem előtt tartva érthető, hogy az értékadás (=) operátor nem készít másolatot, a bal oldali változó ugyanarra az objektumra hivatkozik majd, mint a jobb oldal. Ha valakinek mégis másolatra van szüksége, többnyire használhatja a csaknem minden osztályban megtalálható clone módszert. Hasonlóképpen az egyenlőség vizsgálatának C-ből ismert operátora (==) is a referenciák egyenlőségét ellenőrzi, az objektumok strukturális ellenőrzésére az equal módszer való, már amennyiben az adott osztályban létezik ilyen. Ha minden objektumra amúgy is csak referenciákon keresztül hivatkozhatunk, már nem is tűnik túl meglepőnek, hogy a Java külön mutató típust nem tartalmaz. Ez nagyon sok programozási hibától, biztonsági problémától megkímél bennünket. A referenciák viszont szentek és sérthetetlenek, semmiféle művelet, konverzió nem végezhető rajtuk. Az egyes referenciák által hivatkozott
objektumok típusát az instanceof logikai értéket adó operátorral tudjuk lekérdezni, például if (x instanceof Person) System.outprintln("x egy személy!"); Azt már láttuk, hogy új objektumokat a new utasítással lehet létrehozni. Az objektumokat megszüntetni, törölni viszont nem kell. A programokat futtató Java virtuális gép tartalmaz egy szemétgyűjtő (garbage collection, gc) algoritmust, amely általában a háttérben, a CPU szabadidejében, a programmal aszinkron módon futva összegyűjti a már nem hivatkozott objektumokat és azok tárterületét felszabadítja, hogy a new újra felhasználhassa majd. A szemétgyűjtés teljesen rejtetten, automatikusan történik, a programozóknak általában nem is kell törődniük a szemétgyűjtéssel. A szemétgyűjtéssel kapcsolatos általános problémákkal a 2.1210 pontban foglalkoztunk Ha már vannak objektumaink, nézzük meg, hogyan tudunk hatni rájuk. Az egyes objektumok módszereit az
obj.method(parameter, parameter, ) szintaxissal hívhatjuk meg. Az objektumorientált terminológia ezt gyakran üzenetküldésnek (message passing) nevezi, pl. a somebody.changePID("15502280234"); utasítás "megkéri" a somebody objektumot, hogy hajtsa végre saját magán a changePID műveletet a "15502280234" paraméterrel. Az ilyen módszerhívások vezethetnek az objektum belső állapotának, azaz belső változói értékének megváltozásához is, de gyakran csak egy belső változó értékének lekérdezése a cél. Bár a tiszta objektumorientált elvek szerint egy objektum belső állapotváltozóinak értékéhez csak módszerein keresztül lehetne hozzáférni, a Java - a C++-hoz hasonlóan - megengedi a programozónak, hogy bizonyos változókat közvetlenül is elérhetővé tegyen. Pl ha az age változó közvetlenül látható, a somebody.age = 46; utasítás közvetlenül megváltoztathatja a példaszemélyünk életkorát.
Osztályok definíciója A Java programozók osztályokat a class ClassName { /* az osztály törzse / } programszerkezettel definiálhatnak, ahol a ClassName az újonnan definiált osztály neve lesz, az objektumok belső állapotát és viselkedését pedig a kapcsos zárójelek közötti programrészlet írja le. A class alapszó előtt, illetve az osztály neve után opcionálisan állhatnak még különböző módosítók, de ezekről majd később. Egyedváltozók Az osztály törzsében szerepelnek azok a változó-deklarációk, amelyek az egyes egyedek belső adatszerkezetét valósítják meg. Ezeket gyakran egyedváltozóknak (instance variable, member variable) nevezzük, jelezve, hogy az osztály minden egyede ezekből a változókból saját készlettel rendelkezik. A változó-deklarációk megfelelnek a C-ben megszokottaknak: class Person { String name, surname, pid; byte age; Person mother, father; . A képzeletbeli Person típusunkba tartozó egyedek három
szövegváltozóban tárolhatják a személy vezeték- és keresztnevét, személyi számát (ez persze lehetne akár egy long egész szám is), egy byteban az életkorát és két Person típusú változóban az apjának és anyjának adatait. Az utolsó 2 változó példa arra, hogy egy egyed belső állapota hivatkozhat más - akár az egyeddel megegyező típusú objektumokra is. Módszerek A típus által értelmezett műveleteket a módszerek testesítik meg, ezek definiálásának módja megegyezik a C függvényekével. void changePID (String newPID) throws WrongPIDException { if (correctPID(newPID)) pid = newPID else throw new WrongPIDException(); } Mint látjuk, az egyes módszereknek lehet visszatérési értéke - bár a példánkban nincsen, ezt jelzi az ilyenkor kötelező void -, illetve adott típusú és számú bemenő paramétere. A módszer nevét, visszatérési értékének és paramétereinek számát, típusát a módszer lenyomatának (signature) nevezzük. A
módszerek meghívásánál a lenyomatban definiált formális paraméterek aktuális értéket kapnak. A Jávában minden paraméterátadás érték szerint történik, azaz az aktuális paraméter helyén szereplő értékről másolat készül, a módszer ezt a másolatot látja, használja. Persze, ha a másolat módosul, az az eredeti értéket nem érinti. Kicsit becsapós a "mindig érték szerinti paraméterátadás" szabálya Ha a paraméter nem valamelyik beépített, egyszerű típus eleme, úgy az átadásnál a referenciáról készül másolat, azaz a C++ fogalmai szerint ilyenkor referencia szerinti átadás történik, tehát a hivatkozott objektumon a módszer belsejében végrehajtott módosítások magán a hivatkozott objektumon hajtódnak végre (és nem egy róla készült belső másolaton). A visszatérési érték sem feltétlenül egy egyszerű típusba tartozó érték, hanem lehet objektum referencia is. Az osztály belsejében azonos névvel, de
egyébként különböző lenyomattal több módszert definiálhatunk (többértelműség, overloading), ilyenkor a fordító a módszer hívásánál a paraméterek számából és típusából dönti el, hogy melyik módszert akarjuk meghívni, de a kiválasztásnál a módszer visszatérési értékének típusát a fordító nem veszi figyelembe. A módszerek törzsében a korábban már megismert utasításokat írhatjuk. A kifejezésekben, értékadások bal oldalán használhatjuk az eljárás paramétereit, lokális változóit, de az objektum egyedváltozóinak értékét is. Ha a saját objektumra akarunk hivatkozni, használhatjuk a this szimbólumot. Pl a pid = newPID azonos a thispid = newPID utasítással A módszerek fejében használhatjuk a native módosítót, ez a fordítóprogramnak azt jelenti, hogy a módszer törzsét nem Javában írtuk meg, hanem az aktuális platformtól függő módon, pl. C-ben Az ilyen módszerek paraméterátadási módja, esetleg az
elnevezési konvenciói az aktuális platformtól függhetnek. Természetesen ilyenkor az osztálydefinícióban a módszernek csak a lenyomatát kell megadni. Az ilyen módszereket használó programok sajnos elvesztik a Java programok architektúrafüggetlenségét, de egyébként a native módszerek mind a láthatóság, mind az öröklődés szempontjából a többi módszerrel teljesen azonos módon viselkednek. Konstruktorok Az osztályban definiált módszerek közül kitűnnek az ún. konstruktorok (constructor), amelyek szerepe az objektumok létrehozása, belső állapotuk kezdeti értékének beállítása. Minden konstruktor neve megegyezik az osztálya nevével, visszatérési értéke pedig nincs. Természetesen a konstruktor is lehet többértelmű módszer: Person () { /* üres / }; Person (String name, String surname, String PID) { this(); // az üres konstruktor egyenlõre nem csinál semmit this.name = name; this.surname = surname; this.PID = PID; . }; A
konstruktorokat a new utasítás kiadása hívja meg, a konstruktorok közül a new-nál megadott paraméterek száma és típusa szerint választunk. Kitüntetett szerepű a paraméter nélküli, ún alap (default) konstruktor, ha mi nem definiáltunk ilyet, a Java fordító automatikusan készít egyet az osztályhoz. Az így generált konstruktor a helyfoglaláson kívül nem csinál semmit A szemétgyűjtés miatt a C++-ból ismert destruktorok itt nem léteznek, viszont ha egy objektum végleges megszűntetése előtt valami rendrakó tevékenység végrehajtására lenne szükség, akkor ezt írjuk egy void finalize() lenyomatú módszer törzsébe, a Java virtuális gép biztosan végrehajtja, mielőtt az objektum területét a szemétgyűjtő felszabadítaná, azonban az aszinkron szemétgyűjtés miatt azt soha nem tudhatjuk biztosan, hogy ez mikor következik be. Osztályváltozók és -módszerek Míg az egyedváltozókból minden példány saját készlettel rendelkezik,
az ún. osztály- vagy statikus (static) változókból osztályonként csak egy van. Természetesen mivel ezek nem egy példányhoz tartoznak, a hozzáféréshez sincs szükség egy konkrét objektumra, az osztály nevével is hivatkozhatunk rájuk. A csak statikus változókat használó módszerek a statikus, avagy osztálymódszerek. Egészítsük ki a személy példánkat úgy, hogy minden új személy kapjon egy egyedi sorszámot: class Person { . int index; static int counter = 0; // egyedi sorszám // létrejött személyek számlálója Person () { index = counter++; } // a konstruktorban használjuk fel a számlálót static int howManyPersons () { return counter; } . } Tömbök A Java nyelv tömbjei is objektumok, ha kissé speciálisak is. Deklarálásuk nem csak a C-szerű szintaxissal megengedett, de végre tehetjük a szögletes zárójeleket a "logikus helyükre" is, azaz a következő két deklaráció ekvivalens: int a[] = new int[10]; int[] a = new
int[10]; Mint a többi objektumnál, a név itt is csak egy referencia, a tömb deklarációja után azt a new paranccsal létre is kell hozni. Az indexelés művelete azonos a C-ben megismerttel, itt is 0 a legkisebb index. Lényeges különbség viszont, hogy a virtuális gép ellenőrzi, hogy ne nyúljunk túl a tömb határán; kivétel keletkezik, ha mégis megtesszük. Minden tömb objektumnak van egy length nevű egyedváltozója, amely a tömb aktuális méretét adja vissza. Ez azért is fontos, mert a program futása során ugyanaz a tömbváltozó más és más méretű tömbökre hivatkozhat. Természetesen nem csak a beépített, egyszerű típusokból képezhetünk tömböket, hanem tetszőleges típusból, beleértve egyéb tömböket. A Java többdimenziós tömbjei is mint tömbök tömbje jönnek létre. Például: int a[][] = new int[10][3]; System.outprintln(alength); System.outprintln(a[0]length); // 10-et ír ki // 3-at ír ki Még egy érdekesség: a
referenciák használatának következménye, hogy nem csak "négyszögletes" kétdimenziós tömböt lehet készíteni, de olyat is, ahol az egyes sorok nem azonos hosszúak. Pl. egy "háromszög alakú tömb" létrehozása: float f [][] = new float [10][]; for (int i = 0; i < 10; i++) f[i] = new float [i + 1]; Szövegek A Javában a szövegek (String) is teljes jogú objektumok. A szövegek gyakori módosításának hatékonyabb elvégzésére használhatjuk a StringBuffer osztályt is. Részletes magyarázkodás helyett álljon itt egy példa, egy olyan módszer, amelyik megfordít egy szöveget: String reverse (String source) { int i, len = source.length(); StringBuffer dest = new StringBuffer(len); for (i = len - 1; i >= 0; i--) dest.append(sourcecharAt(i)); return dest.toString(); } Csomagok Összetartozó osztályokat a csomagok (package) segítségével a programozók egyetlen fordítási egységgé foghatnak össze. Ezzel osztály-könyvtárakat
építhetünk, és nagy szerep jut a csomagoknak láthatóság szabályozásánál is. Ha csomagot használunk, a forrásszöveg első nem megjegyzés sorában kell megneveznünk, pl. package mmt.networkingsnmp; A csomagoknak programozási konvenciók szerint általában több komponensű, ponttal elválasztott nevet adunk, és a fordító a névnek megfelelő könyvtár-hierarchiába helyezi el a lefordított osztályokat. Létezik egy név nélküli csomag arra az esetre, ha nem adtuk meg a package kulcsszót. Az így létrehozott csomagokból lehet a korábban megismert import kulcsszóval egy vagy több osztályt átvenni, használni. A Java nyelvi környezet egyre bővülő számú szabványos csomagot tartalmaz Ezek közül a legjelentősebbek: java.applet Programkák környezete java.awt Ablakozó rendszer (Abstract Windowing Toolkit), grafikus és kezelői felület elemek. java.awtimage java.awtpeer Segédosztályok az AWT-hez. java.io Be- és kivitel. java.lang
Nyelvi szinten definiált osztályok. java.net Hálózatkezelés. java.util Segédosztályok. Láthatóság A programozók megadhatják, hogy az általuk definiált egyes osztályok, illetve az osztályok változói, módszerei milyen körben használhatók. Erre a célra az ún hozzáférést specifikáló (access specifier) módosítók használatosak. Egyelőre - amíg az öröklődésről nem beszéltünk - 3 ilyen specifikáló lehet: nyilvános (public), magán (private), baráti (friendly). Ez utóbbi az alapértelmezés Ha nem adjuk meg a hozzáférés módját, akkor ez mindig baráti. Osztályokra csak a nyilvános vagy a baráti hozzáférés vonatkozhat: nyilvános osztályokat a programunkban bárhol használhatunk, bármelyik csomagunkba importálhatjuk. Baráti osztályokat csak az adott csomagon belül lehet használni. Egyed- illetve osztályváltozókra, módszerekre a nyilvános és baráti hozzáférés a fentivel azonos jelentésű, ezen túl a magán
változók, módszerek csak az adott osztályon belül láthatók. 2.434 Öröklődés és következményei Az objektumorientáltság igazi csattanója az öröklődés. Ez teszi lehetővé, hogy a korábbi programozási módszereknél jóval könnyebben felhasználhassunk már megírt programrészeket, ezt gyakran szoftver IC-kként reklámozzák. Bár az objektumorientált paradigma egyelőre még nem váltotta be minden ígéretét, azonban tagadhatatlan, hogy nem csak divatos, de sikeres is. Osztályok leszármaztatása Ha egy új osztályt már valamelyik meglévő alapján akarjuk definiálni, azt az osztály fejében a következő szerkezettel kell jelezni: class Child extends Parent { /* Child törzse / } Az ily módon megadott, ún. leszármazott osztály (Child) örökli a szülője (Parent) tulajdonságait, azaz a belső állapotát leíró adatszerkezetét és a viselkedést megvalósító módszereit. Az új osztály a törzsében ezeket természetesen tovább
bővítheti, a viselkedését módosíthatja. Ezt sugallja az extends (kiterjeszt) kifejezés az osztálydefinícióban. A Java a C++-szal ellentétben csak az ún. egyszeres öröklődést engedélyezi, azaz minden osztálynak egyetlen közvetlen szülője lehet. Tulajdonképpen nem csak lehet, de mindig van is szülője, a nyelv ugyanis definiál egy Object nevű beépített osztályt. Ha az új osztály definíciójában máshogy nem rendelkezünk, automatikusan "extends Object" értendő, azaz minden osztály közvetlenül vagy közvetve, de örökli az Object-ben definiáltakat. Az így örökölt módszerek például az egyes objektumok másolásával (clone), összehasonlításával (equals), megnevezésével (toString), illetve osztálya megkülönböztetésével (getClass), vagy a többszálú futtatással (pl. wait, notify) kapcsolatosak Szükség esetén a meglévő osztályainkból való leszármaztatást megtilthatjuk úgy, hogy azt "final class"-ként
definiáljuk. Ezt akkor alkalmazzuk, ha nem akarjuk, hogy egy mások által belőle leszármaztatott osztály nem kívánt módon módosítsa az osztályunkban definiált viselkedést. Láthatóság Az absztrakt adattípusoknál megismert, hozzáférést szabályozó módosítók - public, private, és a ki soha nem írt friendly - a protected (védett) alapszóval bővültek. A védettnek definiált változók és módszerek csak az osztályban, annak valamelyik leszármazottjában, vagy a csomag osztályaiban láthatók. Mi öröklődik, és mi nem Az öröklés során az új osztály örökli a szülője összes egyedváltozóját. Ezek a vátozók még akkor is megvannak a leszármazott típusú objektumban, ha azok egy private deklaráció miatt közvetlenül nem láthatók. Hasonló a helyzet akkor, ha a leszármazott definiál egy, a szülőben már használt nevű változót, ilyenkor ez a változó eltakarja (shadowing) a szülő változóját. Ilyen esetekben ugyan a
szülőtől örökölt változók a leszármazott osztály módszereiben közvetlenül nem láthatók, de a szülő nem-magán módszerei segítségével továbbra is hozzáférhetők. A szülő módszerei is öröklődnek. Egy leszármazott osztályban használható a szülő bármelyik nyilvános, védett vagy baráti módszere. Azonban a módszerek esetében nagyobb szabadságunk van Ha a leszármazottban a szülőben meglévő módszerrel azonos lenyomatú módszert definiálunk, az elfedi a szülőbeli jelentést, így megváltoztatva egy a szülőben definiált viselkedést. A módszerek ilyen jelentésének megváltoztatását megakadályozhatjuk, ha azt a szülőben final-ként definiáljuk. Láttuk, hogy egy módszerben a saját egyedváltozókra a this referencia segítségével hivatkozhatunk, öröklődés esetén a szülőből örökölt egyedváltozókra, módszerekre hivatkozásnál a super referenciát használhatjuk. Persze az új osztályban láthatók lehetnek a
szülő osztályváltozói és az ezeket kezelő statikus módszerek is, ez azonban nem öröklés, hanem inkább kölcsönös használat, hiszen minden statikus változóból és módszerből továbbra is csak egy van. Nem öröklődnek a konstruktorok, minden osztályhoz meg kell írni a saját konstruktorait. Ha nem írunk egyetlen konstruktort sem, a fordító csak az alap - paraméter nélküli - konstruktort hajlandó automatikusan létrehozni. Az így létrejött alapkonstruktor, miután a tárban a kazalon (heap) az egyed számára helyet foglalt, lehívja a szülő alapkonstruktorát, a super()-t. Egy leszármazottbéli konstruktor a szülő alapkonstruktorát automatikusan meghívja, hacsak a leszármazott konstruktorában valamely más paraméterezésű super(.) konstruktort a saját konstruktorának első utasításaként meg nem hív Végezetül az öröklődésben érdekes szerep jut az abstract módosítóval deklarált módszereknek. Olyan módszerek ezek,
amelyeknek egy osztály csak a fejét (lenyomatát) definiálja, de a törzsük helyét kihagyja. Az ilyen absztrakt módszereket valamelyik leszármazott osztálynak kell majd definiálnia Legalább egy absztrakt - saját maga által absztraktnak definiált, vagy így örökölt és nem konkretizált módszerrel rendelkező osztályt absztrakt osztálynak nevezünk. Ezt az osztály fejében a class előtti abstract módosítóval kell jeleznünk. Az absztrakt osztályok sajátossága, hogy nem lehetnek egyedeik, a fordító hibaüzenettel visszautasítja a new utasítást. Polimorfizmus A polimorfizmus többalakúságot, többarcúságot jelent. Az objektumorientált nyelveknél ez valami olyasmit jelent, hogy különböző típusú objektumoknak lehet azonos lenyomatú módszere és e módszerek közül mindig az objektum aktuális típusának megfelelő módszer hajtódik végre. Ez persze csak akkor izgalmas, ha a program szövegéből nem látszik konkrétan, hogy éppen milyen
típusú elemmel van dolgunk. Ez egy szigorúan típusos nyelvnél, mint a Java, csak úgy lehetséges, ha bizonyos lazításokat vezetünk be, pl. megengedjük, hogy egy szülő típusú referencia aktuálisan hivatkozhat valamelyik leszármazott típusra is. Definiáljuk például a jól ismert vicc nyomán a példabeli Person osztályunkban egy absztrakt SqueezeToothPaste nevű módszert a fogpasztának a tubusból való kinyomására. class Person { . void SqueezeToothPaste(); . } Származtassuk le a férfi és a nő osztályokat a személyből, ezekben már konkrétan megadva, hogyan is szokták a férfiak és a nők a fogkrémes tubust megnyomni: class Man extends Person { void SqueezeToothPaste() { /* . tekerd az egyik végérõl kezdve */ }; } class Woman extends Person { void SqueezeToothPaste() { /* . markold középen és nyomjad */ }; } Ezek után képezhetünk egy tömböt, amelynek egyes elemei hol Man, hol Woman típusúak lehetnek Person[] p = new Person [20]; p[1]
= new Man(); p[2] = new Woman(); . A programban előforduló p[n].SqueezeToothPaste() módszerhívásról a fordítóprogram még nem tudja eldönteni, a hogy p adott indexű helyén épp férfival, avagy nővel akadt dolga. Ez csak a konkrét futáskor derül ki, akkor kell majd kiválasztani a két SqueezeToothPaste() közül az alkalmazandót. C++ programozók figyelem: ott ezt a működést virtuális függvényhívásnak hívják és minden esetben külön kell a programban deklarálni. A Javában minden módszer virtuális! A példa azt is illusztrálja, hogy egy szülő típusú referencia esetén használhatunk minden további nélkül gyerek típusra hivatkozást. Egyébként itt is ismert a típus átformálás (type casting) fogalma, de itt kötöttebb mint a C++-ban, mert: - az átformálás csak a referencia típusát érinti, magát az objektumot nem; nem lehet objektum referenciák típusát akármivé átalakítani, csak az öröklődési láncban felfelé vagy
lefelé. Felületek A C++-hoz képest újítás a Java felület (interface) fogalma, a nyelv tervezői az Objective-C hatását emlegetik. Egy felület nem más, mint módszerek lenyomatának gyűjteménye, mégpedig olyan módszereké, amelyek egy osztályban való egyidejű meglétét a programozó fontosnak tartja. Maga a felület a módszerek törzsét nem adja meg, de írhatunk olyan osztályokat, amelyek megvalósítják, implementálják a felületet, azaz konkrétan definiálják a felületben felsorolt valamennyi módszert. Ennek nagy haszna, hogy az öröklődési hierarchiában nem rokon osztályok is viselkedhetnek hasonlóan, és az objektumorientált terminológia szerint azonos protokoll - a módszergyűjtemény segítségével kommunikálnak. Ha egy osztály megvalósít egy felületet, azt a class A implements I { . } formában lehet kifejezni, de ennek az a következménye, hogy A-ban az összes I-beli módszert meg kell valósítani. A Javában többszörös
öröklés ugyan nincs, de egy osztály egyidejűleg több felületet is megvalósíthat. Egyébként a felületen deklarált minden módszer nyilvánosnak és absztraktnak tekintendő, de a módosítókat - sem ezeket, sem másokat - nem lehet kiírni. Egy felület szükség esetén deklarálhat változókat is, ám ezek mind véglegesek, nyilvánosak és statikusak (final public static) és természetesen értéküket azonnal iniciálni kell. 2.435 Szálak A Java a párhuzamos programok írását szálak segítségével teszi lehetővé. A szálaknál is, mint az egyéb párhuzamos tevékenységnél, a következő alapvető problémák merülnek fel: - a szálak létrehozása, futtatása; a szálak szinkronizációja, futásuk összehangolása; információcsere a szálak között; ütemezés, a központi egység használatának felosztása a szálak között. Szálak létrehozása A Java nyelvben a szálak számára a java.lang csomag tartalmaz egy külön osztályt, a Thread-et
Minden párhuzamos tevékenység vagy egy Thread, vagy ebből leszármazott osztály egy példánya. Szálakat tehát például úgy hozhatunk létre, hogy leszármaztatunk a Thread osztályból egy saját osztályt, amelyből aztán létrehozunk egy vagy több példányt. Persze meg kell adnunk, hogy a szálunk milyen tevékenységet hajtson végre, ezt a Thread-ből örökölt nyilvános run felüldefiniálásával teheljük meg: módszer class MyThread extends Thread { public int ID; MyThread (int i) { ID = i; }; public void run() { /* a szál tevékenységének leírása / }; } Ezek után a programunkban létrehozhatunk néhány saját szál objektumot: MyThread first = new MyThread(1); MyThread second = new MyThread(2); A fenti példa azt is illusztrálja, hogy a leszármazott osztály konstruktorát felhasználhatjuk arra, hogy az egyes szálaknak megszületésük előtt egyedváltozókban olyan objektumokat adjunk át, amelyeket az egyes szálak az éltük folyamán
használhatnak. Az így létrehozott szálak még csak "megfogantak", de nem születtek meg, nem kezdték el a run módszer törzsét végrehajtani. Ehhez meg kell hívni a Thread osztályból örökölt start módszert: first.start(); secondstart(); A start módszer indítja majd el a run törzsét. Eztán már mindkét szál fut, versengve az egyetlen központi egységért. Hogy nagyobb legyen a tülekedés, a fenti kettőn túl van még néhány szál, amire talán nem gondoltunk: a főprogram - a main statikus eljárás törzse - is egy szál, sőt a tárgazdálkodáshoz szükséges szemétgyűjtés is párhuzamos tevékenységként valósul meg. A Thread osztálynak van még néhány konstruktora, talán említést érdemelnek azok a konstruktorok, ahol egy String paraméterben nevet is adhatunk a szálunknak, amit pl. a nyomkövetésnél jól használhatunk. Ezt felhasználva írunk egy másik konstruktort is: class MyThread extends Thread { . MyThread (int i, String
name) { super(name); ID = i; }; . } MyThread first = new MyThread(1, "First"); A szálak létrehozásának a Thread-ből leszármaztatáson túl van egy másik módja: a java.lang tartalmaz egy Runnable felületet is, amely egyetlen módszert, a run-t specifikálja. Ha a mi osztályunk implementálja a fenti felületet, létrehozhatunk egy Thread típusú objektumot, konstruktorként megadva a run-t megvalósító objektumunkat. A fenti példa a rövidség kedvéért egyetlen szállal a felületet használva így nézhet ki: class MyObj implements Runnable { public int ID; MyThread (int i) { ID = i; }; public void run () { /* a szál tevékenységének leírása / } }; MyObj firstObj = new MyObj(1); Thread first = new Thread(firstObj); first.start(); Egy trükk: ha később nem akarunk sem az objektumra, sem a szálra hivatkozni, azaz megelégszünk azzal, hogy fut, az utolsó 3 sort össze is vonhatjuk: new Thread(new MyObj(1)).start(); Melyik létrehozási módszert
érdemes használni? Gyakran nincs választási lehetőségünk. Ha az osztályunkat más osztályból akarjuk leszármaztatni pl. programkát írunk, akkor , a Java nem engedélyez többszörös leszármazást, így csak a felületen keresztüli definíció használható. A Threadből közvetlen leszármaztatást csak akkor érdemes használni, ha valamilyen okból felül akarunk definiálni a Thread-ből örökölt néhány módszert - természetesen a run-on kívül. Szálak futása A Java virtuális gép az egyes szálak végrehajtása során nyomon követi azok állapotát. A következő állapotátmeneti diagramm szemlélteti az egyes szálak viselkedését. Az egyes szálak 5 különböző állapotban lehetnek: - Új állapotúak az éppen létrejött szálak (Thread konstruktora lefutott); Futásra készek azok a szálak, amelyek ugyan futhatnának, de egy másik szál használja a Java virtuális gépet; A futó szál utasításait hajtja végre a virtuális gép; A vár
állapotú szálak valamilyen külső vagy belső esemény bekövetkezését várják; A halott állapotú folyamatok megálltak, futásuk befejeződött. Az egyes állapotátmenetek magyarázata: 1. Lehívták a szál start() módszerét, a szál futása megkezdődhet 2. A szál stop() módszerét meghívva a szál végrehajtása befejeződik, bármelyik állapotban is legyen. 3. A szál várakozni kényszerül, mert valaki a suspend() módszerrel a szál futását felfüggesztette; valaki - akár saját maga - a sleep(long millisec) módszerrel a megadott ideig várakoztatja; a wait() módszerrel egy objektumhoz rendelt ún. feltételes változóra vár; egy objektumhoz rendelt monitorra vár; valamilyen I/O művelet lezajlására vár. 4. Bekövetkezett a várt esemény (a fenti sorrendben): valaki a resume() módszerrel a szál futását továbbengedte; letelt a sleep-ben megadott idő, a várt feltételes változóra kiadtak egy notify() vagy notifyAll() hívást; a várt monitor
felszabadult; a várt I/O művelet befejeződött. 5. Az ütemező a futásra kész szálak közül kiválaszt egyet, és folytatja annak futtatását 6. A szál a yield() hívással lemondhat a futás jogáról valamelyik futásra kész szál javára, illetve az ütemező is elveheti az éppen futó száltól a futás jogát. 7. A szál elérte run módszerének végét, futása befejeződik Ütemezés Mint az állapotátmeneti diagramnál láttuk, a virtuális gépen megvalósított ütemező az 5-ös és 6-os számmal jelölt állapotátmeneteknél kap szerepet. A Java nyelv fix prioritású preemptív ütemezést definiál, egyes architektúrákon ez időosztásos is lehet. Nézzük ennek a magyarázatát Minden szál rendelkezik egy, a "fontosságát" meghatározó int számmal, amelyet örököl az őt létrehozó száltól, de a setPriority(int newprio) hívással bármikor be is állíthatja a MIN PRIORITY és MAX PRIORITY közötti értékre. Azért hívják a
rendszert mégis fix prioritásúnak, mivel maga a virtuális gép soha nem módosítja a szálak prioritását. A futásra kész folyamatok közül a futó kiválasztása mindig szigorúan a prioritások alapján történik, a legnagyobb, illetve az azonos prioritással rendelkezők közül a legrégebben várakozó indul tovább. Az ütemezés azért preemptív, mert ha az éppen futónál nagyobb prioritású szál válik futásra késszé, akkor a futó megszakad (6-os átmenet) és a legnagyobb prioritású indul tovább. (Megjegyzés: ez csak nagyobb prioritás esetén történik, azonosnál nem!) Még ilyen preemptív ütemezés esetén is előfordulhat, hogy a legnagyobb prioritású szálak egyike kisajátítja a virtuális gépet, ha nem kényszerül várakozni (3-as átmenet) és a yield() hívással sem mond le a futás jogáról. Ezért használnak néhány rendszerben időosztásos (time-slicing) ütemezést Itt minden szál csak egy adott maximális ideig lehet futó
állapotú, ha az időszelete letelt, a virtuális gép megszakítja és beáll a futásra készek közé, a vele azonos prioritásúak mögé. Ez gyakran nagyon kényelmes, de figyelem - gondolom az egyes architektúrákon előbukkanó implementációs problémák elkerülése végett -, a nyelv nem követeli meg, hogy az ütemezés időosztásos legyen! Ha valaki erre számít, előfordulhat, hogy a programja néhány architektúrán nem helyesen fut. (Ennyit a híres architektúrafüggetlenségről! Nekünk ez nagyon nem tetszik, de mit tehetnénk, legfeljebb az vigasztal, hogy véleményünkkel nem állunk egyedül.) Egyes szálakat a programozó a setDaemon() hívással "démonizálhat". A démonok olyan párhuzamos tevékenységek, amelyek más szálaknak nyújtanak szolgálatokat, általában végtelen ciklusban futnak, gyakran arra várva, hogy más szálak kommunikáljanak velük. A Java program addig fut, azaz a virtuális gép addig működik, amíg van a
rendszerben legalább egy nem démon szál. Szinkronizáció és kommunikáció Az eddig megismert módszerekkel létrehozhatunk párhuzamosan futó szálakat, de ezek csak akkor igazán hasznosak, ha összehangoltan tevékenykednek valamilyen közös cél érdekében. Ehhez szükséges a szálak közötti szinkronizáció és kommunikáció. Ezek közül a szinkronizáció a komolyabb feladat, a kommunikációt - ha a szinkronizáció helyesen működik - a szálak már megoldhatják közösen használható objektumokhoz való hozzáféréssel. Mivel a szálak közös erőforrásokon - pl a táron - osztoznak, a legfontosabb szinkronizációs probléma az ún. kölcsönös kizárás (mutual exclusion) megvalósítása, azaz annak biztosítása, hogy bizonyos erőforrásokat egyidejűleg csak egyetlen szál használhasson, másik szál pedig csak akkor férhessen hozzá, ha az előző már konzisztens, biztonságos állapotban hagyta. A kölcsönös kizárás megvalósítására a
Java a Hoare-féle monitor koncepciót követi. Minden objektum rendelkezik egy zárral (lock, monitor) és a programozó előírhatja, hogy bizonyos módszerek végrehajtása csak akkor kezdődhet meg, ha az objektum szabad. Ez a módszer fejében használt synchronized módosítóval történik. Az ilyen módon definiált módszer meghívása előtt a hívó szál megpróbálja az objektumhoz - statikus módszer esetén az osztályhoz tartozó zárat lefoglalni. Amennyiben senki nem birtokolja a zárat, elkezdi a módszer végrehajtását, természetesen ezzel kizárva az összes többi versengő szálat. Ha viszont foglaltnak találja a monitort, várakozni kezd. Kivételt képez az az eset, ha a monitort ugyanez a szál tartja lefoglalva, ilyenkor a futás továbbhaladhat, a Java monitorja újrahívható (reentrant). Miután a módszer lefutott, kilépéskor a szál felszabadítja a zárat, szükség esetén továbbindítva futásra késszé téve - egyet a zárra várakozók
közül. A programozónak külön meg kell mondania, hogy melyek azok a módszerek vagy kódrészletek, amelyek végrehajtásához kizárólagosságot kell biztosítani. (Megjegyzés: a nyelv a "szinkronizáció" fogalmát sajnos az irodalomban elterjedtnél szűkebben, a kölcsönös kizárás szinonimájaként használja. Ezentúl, bár nem tetszik, de mi is követjük ezt a terminológiát.) A szinkronizált módszerhívás alternatívájaként használhatunk szinkronizált kódrészletet is, ahol a synchronized (obj) { /* szinkronizálandó . */ } kódrészletet a paramétereként megadott objektumnak a zárja védi - és nem a this, mint fentebb. A monitor koncepcióban megvalósuló automatikus, a programozó elől rejtett implementációjú kölcsönös kizárás sajnos általában nem elég az együttműködő szálaknak, kell olyan eszköz is, amelyikkel egy szál bevárhatja, amíg egy másik elér tevékenységének adott pontjára. Erre szolgál a monitor
koncepciót kiegészítő, a Hoare-féle feltételes változó (conditional variable). Minden Java objektumhoz tartozik egy ilyen feltételes változó is, amire várakozni lehet (wait()), illetve egyetlen (notify()) vagy az összes (notifyAll()) várakozót tovább lehet indítani. Mind a wait-et, mind a notify-t csak szinkronizált módszerekben lehet hívni. Wait esetén az azt kiadó szál várakozni kezd a szinkronizált objektum feltételes változójára, és ezzel együtt - ideiglenesen felszabadítja a monitort, hogy másik módszer is futhasson. Egy szinkronizált módszerben kiadott notify kiválaszt az adott objektumra várakozó szálak közül egyet, és azt futásra késszé teszi. Mikor a monitor legközelebb felszabadul, a továbbindított szál újra megszerzi, azaz lefoglalja a monitort, és folytatja a futását. A wait módszerben várakozó szálat csak egy notify vagy notifyAll mozdíthatja ki, de létezik a wait-nek egy paraméterezett, időzített
változata is, ahol a szál legfeljebb a paraméterben millisecundum mértékegységben megadott időtartamon keresztül várakozik. Ilyen wait-ből felébredve a szálnak meg kell győződnie arról, hogy vajon egy notify avagy az idő lejárta miatt ébredt-e fel. 2.436 Programkák A Java programozási nyelv legnagyobb újítása, rohamos terjedésének oka, hogy kitalálták a programkákat (applet). Ezek olyan Java programok, amelyek a Web-en böngészve egy Web oldallal együtt letölthetők a saját számítógépünkre, majd itt, nálunk futni kezdenek. Egy ilyen program életet lehelhet a statikus Web oldalakba, a szerver közreműködése és a hálózat terhelése nélkül például animációt jeleníthet meg, illetve gyakran egy kliens-szerver programrendszer klienseként a szerver felé tartó adatok előfeldolgozását, a válaszok intelligens megjelenítését végezheti. Programkákat alkalmazva lehetőség nyílik a Web böngészők szolgáltatásainak,
képességeinek akár dinamikus, igényeknek megfelelő bővítésére is. Különbségek a programkák és a programok között A Java programok önállóan futnak, közvetlenül az operációs rendszer és az erre épülő Java virtuális gép felügyelete alatt. A programkák hálózaton keresztüli betöltését és futtatását viszont egy Web böngésző program végzi, a programkák futási környezete maga a böngésző. (A Java fejlesztői rendszerek tartalmaznak ugyan a programkák kipróbálására segédprogramokat, mint pl. a SUN appletviewer-e, ám ezek egy böngészőnek pontosan megfelelő környezetet biztosítanak.) A programkák sokat profitálnak ebből az albérletből, használhatják a böngészőben meglévő teljes program-infrastruktúrát: a Web oldalból kihasíthatnak maguknak területeket, ahova rajzolhatnak, felhasználhatják a böngésző grafikus-, multimédia és pl. HTML megjelenítési szolgáltatásait, kezelői felület elemeit,
eseménykezelését, hálózati kommunikációját. Igaz, hogy lehet olyan önálló Java alkalmazást írni, amely egy programkának megfelelően viselkedik, ám egy ilyen környezet felállítása viszonylag nagy munkát igényel, a programkák viszont mindent készen kapnak. Azonban, akárcsak egy valódi albérletben, a programkák itt is korlátokba ütközhetnek. A távoli számítógépről óvatlanul, gyakran a hálón barangoló tudta nélkül letöltött, és a helyi számítógépen futni kezdő programok sokakban jogosan a vírusok rémképét idézik. Ezért a böngésző szigorúan korlátozza a felügyelete alatt futó programkákat, megakadályozza, hogy potenciálisan veszélyes tevékenységet hajtsanak végre. Így például a Java nyelvbe már amúgyis beépített biztonsági rendszeren, konzisztencia ellenőrzésen túl a programkák: - nem, vagy csak nagyon szigorúan korlátozva férhetnek hozzá a helyi állományrendszerhez; - csak azzal a szerverrel, az ott
futó programokkal vehetik fel a kapcsolatot, ahonnan letöltődtek; - nem indíthatnak el a helyi gépen lévő programokat, nem tölthetnek be helyi programkönyvtárakat. Sokak szerint a fenti megkötések túlságosan szigorúak, de amíg a Java rendszerbe nem kerülnek be a programkák módosíthatatlanságát, megbízható azonosítását lehetővé tevő titkosítási algoritmusok dolgoznak rajtuk -, addig nincs mit tenni, mint szigorúan kulcsra zárni az összes olyan kiskaput, ahol rosszindulatú programkák beszivárogva belerondíthatnak a számítógépünkbe. A programkák szerkezete Programkák írásához a Java nyelv külön csomagot (java.applet) és külön osztályt (Applet) tartalmaz, saját programkánkat ebből kell - mégpedig nyilvánosan - leszármaztatnunk. A teljesség igénye nélkül ismertetünk néhány fontosabb, felüldefiniálható módszert. A futás vezérlése Míg a Java programok betöltése után a main statikus módszer indul el, addig a
programkák esetében a böngésző biztosítja azt, hogy megfelelő események bekövetkeztekor meghívja a leszármazott osztály az Applet-ben meglévőt felüldefiniáló módszereit. Persze nem kell minden itt ismertetett módszert feltétlenül felüldefiniálni, ezek az Applet osztályban nem absztrakt, hanem létező, de üres törzsű módszerek. Az egyszerű példaprogramunknak sem lesz mindegyikre szüksége. A programka futásának vezérlésére az init, start, stop és destroy módszerek szolgálnak, valamennyien paraméter nélküliek. Az init törzse a programka letöltésekor fut le, itt végezhetők el a kezdeti beállítások. Rögtön az init után a start is lefut, de a start módszert akkor is meghívja a böngésző, ha újra akarja indítani a programkát. A stop módszer a programka futásának leállítására szolgál Tipikus, hogy egy HTML oldalt letöltve betöltődnek az ott lévő programkák is, majd lehívódik ezek init és start módszere. Ha ezután
böngészés közben továbblépünk erről az oldalról, akkor az összes ott futó programka stop módszere is lehívódik. Ha visszatérünk a lapra, akkor már csak a start-ok indulnak el, az init nem. Ha a stop módszert üresen hagyjuk, a lapot elhagyva a programkánk folytathatja a működését, ezzel esetleg feleslegesen használva rendszererőforrásokat. A stop-ban gondoskodhatunk róla, hogy a programka működése felfüggesztődjön. Végezetül a destroy módszer lehetővé teszi, hogy a programka leállása előtt felszabadítsa az összes általa lefoglalt speciális erőforrást, tisztán és rendben hagyja itt a világot. A destroy-t pl a böngésző leállása, vagy egy futó programka újra betöltése előtt hívja meg. Bár a böngészők tipikusan egy-egy szálat allokálnak a letöltött oldalon lévő minden programkához, és ebből a szálból hívják meg a programkát vezérlő módszereket, ez azonban nem zárja ki, hogy a programkánk végrehajtása
közben maga is létrehozzon szálakat. Szálak használata tipikus a folytonos animációt biztosító programkáknál, de célszerű a hosszú ideig tartó inicializálást - pl. nagy méretű képek hálózatról letöltését - is egy párhuzamosan futó szállal megoldani. Rajzolás A paint módszer, (amelynek egy java.awtGraphics osztályba tartozó paramétere is van), szolgál arra, hogy a programka a böngésző által megjelenített lap területére "rajzolhasson", azaz szöveget, grafikát jelenítsen meg. A böngésző feladata, hogy ezt a módszert mindig lehívja, amikor a programka által használt területre rajzolni kell, pl. init után, vagy ha a böngésző ablaka láthatóvá válik, mérete megváltozik, más helyre kerül a képernyőn, stb. Ha a programka saját maga akarja kezdeményezni a rajzolást, akkor meghívja a repaint módszert, melynek hatására a böngésző egy alkalmas időpontban meghívja a programka update módszerét. Az update is az
Applet-ből öröklődik. Ha nem definiáltuk újra, egyszerűen paint-et hív, de felüldefiniálva lehetőségünk van például kihasználni, hogy a programka már felrajzolta, amit akart, nekünk elég a meglévő képen módosítanunk. Különösebb magyarázat nélkül közlünk egy egyszerű példát, a szokásos "Hello világ" programka változatát. A programkában van egy kis csavar Képes átvenni az őt tartalmazó lapról egy paramétert, így a "world" helyett ezt a szöveget írja majd ki, persze, ha létezik egyáltalán ilyen paraméter. A feladat egyszerűsége miatt elég csak az init-et és a paint-et definiálni. import java.appletApplet; import java.awtGraphics; import java.awtFont; import java.awtColor; public class HelloWorldApplet extends Applet { Font f = new Font("TimesRoman", Font.BOLD, 36); String name; public void init() { name = getParameter("name"); if (name == null) name = "World"; name =
"Hello " + name + "!"; } public void paint(Graphics g) { g.setFont(f); g.setColor(Colorred); g.drawString(name, 5, 50); } } Programkák egy Web oldalon Hogy a programkánk egy Web oldallal együtt letöltődjön és elinduljon, a lapot leíró HTML dokumentumot ki kell egészíteni a programka, annak elhelyezkedése, és esetleges paramétereinek megadásával. Erre a célra szolgál az <APPLET> HTML tag Reméljük, hogy a példánk használatához szükséges, itt közölt HTML lap különösebb magyarázatok nélkül is érthető. <HTML> <HEAD> <TITLE>Hello!</TITLE> </HEAD> <BODY> <P> <APPLET CODE="HelloWorldApplet.class" WIDTH=300 HEIGHT=50> <PARAM NAME=name VALUE="Pisti"> Hello to whoever you are! </APPLET> </BODY> </HTML> Szeretnénk felhívni a figyelmet a "Hello to whoever you are!" szövegre. Az <APPLET> tag definíciója szerint annak
lezárása előtt szereplő bármilyen, nem a programka leírásához tartozó szöveget azok a böngészők jelenítik meg, amelyek nem képesek programkák futtatására. Így ezt a példalapot olvasva mindenki látna valamit, a szerencsésebbek nagy piros betűkkel azt, hogy Hello Pisti! A kevésbé szerencsések csak azt, hogy Hello to whoever you are! De azért ők se csüggedjenek 2.5 Deklaratív fejlesztőeszközök Ahogy ezt már korábban elmondtuk, a deklaratív fejlesztőeszközök használata során elegendő a megoldandó problémát leírni ("mire van szükségünk"), és a probléma megoldása ettől a ponttól kezdve a rendszer dolga. Így tehát kimarad a procedurális eszközökben szükséges lépés, melyben a megoldás algoritmusát definiáljuk. Hosszú távon a jövő a deklaratív rendszereké, mert ezek nyújtják a felhasználóiknak a legnagyobb segítséget, de ezen a téren még rendkívül sok a megoldatlan probléma. Mi természetesen
csak a jelenlegi állapotot tudjuk leírni, de már az itt megismert, a gyakorlatban sikeresen alkalmazott eszközök is komoly bizakodásra adnak okot számunkra. A deklaratív fejlesztőeszközök két csoportjával foglalkozunk, ezek a deklaratív programozási nyelvek illetve a 4GL fejlesztőeszközök. Míg a deklaratív programozási nyelvek működése a 23 fejezetben elmondott elveken alapul, a 4GL fejlesztőeszközök inkább a procedurális programnyelvek fölé épülő fejlesztői környezeteknek tekinthetők. Hogy a 4GL eszközöket mégis deklaratív eszközként tárgyaljuk, azzal magyarázható, hogy előre, programozottan felkínált szolgáltatásaik annyira sokrétűek és annyira könnyen testre szabhatók, hogy így végeredményben - bár sokkal merevebb vezérelvek mentén - eljutunk oda, hogy a megoldandó feladatot sok tekintetben csak specifikálnunk kell, a megoldást pedig a rendszer készen adja. 2.51 Deklaratív programozási nyelvek Ebben a pontban a
Prolog és az SQL, a gyakorlatban használt két legsikeresebb deklaratív nyelv alapvető jellegzetességeit foglaljuk össze. A Prolog (PROgramming in LOGic) a logikai programozás alapnyelve, melynek első verzióit a nyolcvanas évek elején fejlesztették ki Angliában és Magyarországon (SzKI). Azóta számos változata és továbbfejlesztése készült, azonban a széleskörü gyakorlati használata minden elvi szépsége és eleganciája ellenére sem tudott elterjedni. Ennek oka főként a bonyolult szervezésű információhalmazokon (tudásbázis) végrehajtott exponenciális időigényű keresés (NP-teljes problémák) általános kezelésének elvi és gyakorlati nehézségeiben keresendő. Egy szóval, nem tudunk eleget az ilyen problémák általános és hatékony megoldásához. Az SQL (Structured Query Language) a relációs adatbáziskezelők szabványos lekérdező nyelve. Piaci megjelenése óta, mely a hetvenes évek végére tehető, rendkívüli sikert ért
el, így manapság minden bizonnyal egyike a legszélesebb körben használatos nyelveknek. A két nyelv gyakorlati sikere közötti eltérés szembetűnő. Kérdés, vajon mi lehet ennek az oka A válasz tulajdonképpen egyszerű: a Prolog olyan általános programnyelv, mely elvileg lehetővé teszi tetszőleges feladat deklaratív megoldását, míg az SQL egy szűk részterület (relációs adatbázisok lekérdezése és kezelése) problémáinak deklaratív megoldását tűzi ki célul. Az elmondottakból kitűnik, hogy a deklaratív rendszerek gyakorlati sikere azon múlik, mennyire tudjuk feltérképezni a megoldandó problématerülethez kapcsolódó, a megoldást elősegítő információkat (szaktudást). Ez az SQL esetében lehetséges (bár a dolog ott sem nevezhető egyszerűnek), a Prolog esetében azonban a nyelv általános volta miatt nem. A továbbiakban ezt a két nyelvet vizsgáljuk kicsit közelebbről. 2.521 Prolog A PROLOG programozási nyelv a megoldandó
probléma predikátum kalkulusszerű, elsőrendű logikai reprezentációjára épül. A PROLOG program logikai állítások halmaza, melyek mindegyike egy-egy Horn klóz. A Horn klózok olyan speciális klózok (logikai VAGY kapcsolatban álló predikátumokból álló tagok), amelyekben legfeljebb egy ponált literál lehet. A Horn klózok jelentőségét az adja, hogy bármely belőlük felépített logikai állításhalmaz ellentmondásmentessége algoritmikusan eldönthető. Bár a "legfeljebb egyetlen ponált literál" megkötés első pillantásra túl erősnek tűnhet, vegyük észre, hogy a pq klóz is jól formált Horn klóz a ¬p∨q ekvivalencia alapján. Éppen ezen alapul a Horn klóz konverzió is. Ha egy klóz nem tartalmaz ponált literált azt "headless" (fejetlen) klóznak nevezik (ezek felelnek meg a tényeknek). Az egyetlen negatív literált tartalmazó Horn klózokat “headed” (fejes) klózoknak nevezik (ezek implikációvá
alakíthatók). A fejes Horn klózok felelnek meg a PROLOG szabályoknak. A PROLOG interpreter visszafelé láncolásos mélységi keresést valósít meg mintaillesztéssel és visszalépéssel. A mintaillesztés során a klózokat lexikális sorrendben veszi figyelembe, és mindig a legelső illeszkedőt választja. A következőkben a PROLOG szintaxis alapelemeit és az interpreter működését szemléltetjük. A PROLOG programok alapvető építőkövei a következők. Konstansok A konstansok adott, konkrét objektumokat jelölnek (ha objektumorientált terminológiát használnánk, egyedeknek vagy példányoknak mondanánk őket). Kisbetűvel kezdődnek, pl.: jancsi, juliska A konstansok lehetnek atomok vagy integerek (egész számok). A fenti példában szereplő konstansok atomok. Ezek tetszőleges alfanumerikus karakterekből állhatnak, de biztosan tartalmaznak betűket vagy szimbólumokat is. Az egész számok ezzel szemben csak számjegyeket tartalmaznak, és az
aritmetikai műveletek elvégzésének alapjául szolgálnak. Változók A változók általános fogalmakat (kategóriákat, osztályokat) jelölnek, melyek konkrét megjelenéseit az adott pillanatban még nem ismerjük. Nagybetűvel vagy aláhúzással (‘ ’) kezdődnek, pl.: X, Y, Ezisvaltozo, 3 kismalac Az karakter anonim változót jelöl, melynek konkrét kötése érdektelen, csak az számít, hogy létezik-e hozzá alkalmas kötés. Tények Kisbetűvel kezdődő predikátumból, konstans vagy változó argumentumokból állnak, pont zárja le őket, pl.: szereti(jancsi,juliska) Szabályok Implikációt írnak le; bal és jobb oldalukon egymástól vesszővel elválasztott tények; a szabályt pont zárja le, pl.: szereti(jancsi,X) :- szereti(X,pia), szereti(X,kaja). A szabályok <fej> :- <törzs> alakúak, ahol a ":-" olvasata "ha", azaz a szabály feltételrésze a <törzs>, következmény része a <fej>, azonban az
interpreter hátrafelé következtet, azaz a kérdést igyekszik visszavezetni a rendelkezésre álló tényekre és szabályokra, így a szabályok adott felírási módja jobban áttekinthetõ kódot eredményez, mert a következtetés folyamán a szabályok bal oldalát illesztjük (<fej>) és jobb oldalát értékeljük ki (<törzs>). Kérdések A ?- karaktersorozat vezeti be őket, egyébként felépítésük megegyezik a tényekével), pl.: ?- szereti(jancsi,X). A Prolog interpreter működése Az interpreter működését a következő egyszerű példán mutatjuk be: /* /* /* /* /* /* 1 2 3 4 5 6 */ */ */ */ */ */ rablo(rumcajsz). szereti(manka,csibeszke). szereti(manka,kerek erdo). szereti(vizimano,csibeszke). szereti(rumcajsz,X) :- szereti(X,csibeszke). ellopja(X,Y) :- rablo(X), szereti(X,Y). /* Kérdés / ?- ellopja(rumcajsz,X). A kérdés tehát, mit lopna el Rumcajsz? A megadott kérdésre a PROLOG interpreter által generált válasz megkeresésének
lépéseit a következőkben részletesen leírjuk: 1. Az interpreter a kérdéstől indul A mintaillesztés során az X változóhoz keres kötést, ha ez megvan, kiírja az eredményt. Az adatbázist az első elemtől kezdi illeszteni, egyesével, lexikális sorrendben. Az 5 elemnél az illesztés sikeres Az illesztés után keletkező szabálypéldány: ellopja(X/rumcajsz,Y/X):-rablo(rumcajsz),szereti(rumcajsz,Y/X). alakú. X nem azonos a kérdésben szereplő X-szel. Ez annak a következménye, hogy a PROLOG a változóneveket standardizálja. Ennek lényege, hogy a változónevek hatásköre csak az adott tényre vagy szabályra terjed ki, és kötések nem öröklődnek az azonos nevű változók más szabályokban (tényekben) való előfordulásaira. 2. Hogy a szabály fejrésze igaz legyen, teljesülnie kell a törzsben szereplő összes állításnak Mivel ezek szigorúan ÉS kapcsolatban vannak egymással, az interpreter a törzsben szereplő klózokat sorban, egyenként,
rekurzív módon kiértékeli. A rablo(rumcajsz) predikátum illeszkedik az 1. tényre, szereti(rumcajsz,X) predikátum a 4. szabály fejrészére illeszkedik: tehát igaz. A szereti(rumcajsz,X) :- szereti(X,csibeszke). 3. A szereti(X,csibeszke) predikátum a 3 tényre illeszkedik: szereti(X/manka,csibeszke). Ezzel nem maradt több igazolandó állításunk, az interpreter visszatér, és válaszként az X/manka kötést adja vissza. Mint minden mélységi kereső eljárás, a PROLOG interpreter is visszalépést alkalmaz, ha egy adott ágon nem talál megoldást. Az adott állapotig bejárt utat, és a megvizsgált lehetőségeket egy veremben tárolja. A verem minden egyes eleme megfelel egy kielégített részcélnak Minden egyes elem egy-egy ún. hely-jelet (place marker) tárol, amely azt mutatja, hogy a részcél kielégítésekor az interpreter az adatbázis hányadik elemében talált illeszkedést. Ha visszalépésre kerül sor, az illesztést mindig a tárolt jelnek
megfelelő sorszámú elem után kezdi az interpreter. Előre lépésnél mindig az adatbázis elejéről indul. Ez már csak azért is logikus, mert a visszalépés éppen akkor fog egy adott állapotban bekövetkezni, ha a mintaillesztés sikertelen volt egészen az adatbázis végéig. Ha az előző állapotba való visszalépés sikert hoz, a következő állapotban újra kell kezdeni a keresést. A PROLOG értékelése: - Az interpreterben működő implicit rekurzív algoritmus miatt nagyon tömör, és elegáns megoldások adhatók bizonyos rekurzív problémák megoldására. - A vezérlési struktúra kötött és túl egyszerű, ráadásul nincs olyan nyelvi eszköz, mellyel a működése megfelelően kézbentartható lenne. A cut ("!") olyan beépített predikátum, amely korlátozott lehetőséget nyújt erre. Segítségével megakadályozható, hogy a következtetési gráf egy adott részfája újra generálódjon, ha egyszer már kiderült, hogy nem
tartalmaz megoldást, és biztosan tudjuk, hogy semmilyen más kontextusban sem fog megoldáshoz vezetni. A cut használata távol áll a jól formalizálható módszerektől. - A gyakorlatban használt PROLOG verziók - mint ahogy ez elkerülhetetlen - olyan beépített predikátumokat is tartalmaznak, amelyek nem puszta literálokként funkcionálnak a mintaillesztés során, hanem mellékhatásuk is van (pl. I/O műveletek, vagy aritmetikai függvények) Attól függően, hogy ezek milyen mértékben fordulnak elő egy PROLOG programban, a leírás deklaratív jellegét egyre inkább procedurális irányba tolhatják. - Erős a kísértés, hogy PROLOG-ban procedurális módon programozzunk. Ehhez nem kell más, mint a szabályok fejrészét eljárás fejnek, a törzs részét pedig eljárás törzsnek tekinteni. Egyszerűen belátható, hogy a mintaillesztő a paraméterátadás funkcióját megvalósítja. A szabályok, illetve a szabályok törzse ezek után szekvenciálisan
értékelődik ki, mely hatásában egy procedurális jellegű programhoz vezet. Mindez azonban egy, a procedurális programnyelvek futtatórendszereiben alkalmazottnál jóval bonyolultabb mechanizmus szerint zajlik, így a végrehajtás hatékonysága nagyon leromlik. 2.522 SQL Az SQL relációs adatbáziskezelőkhöz kapcsolódó műveletek végrehajtását teszi lehetővé eszköz- és implementációfüggetlen módon. Az SQL mai szabványos formája az IBM által a hetvenes évek közepén létrehozott SEQUEL 18 nevű nyelvből fejlődött ki. Az első, SQL felületet használó, piaci forgalomban kapható relációs adatbáziskezelő az Oracle2 1979-re készült el. Azóta a nyelv használata adatkezelő rendszerekben rendkívüli módon elterjedt, és mára elmondható, hogy minden sikeres relációs adatbáziskezelő rendszer használja ezt a nyelvet. A relációs algebra A relációs adatbáziskezelő rendszerek elvi alapjául a relációs algebra szolgál, melynek elemeire
először Codd tett javaslatot 1970-ben. A relációs algebrát relációk halmaza fölött végezhető műveletek csoportja definiálja. A reláció definíció szerint 19 egy n elemű Descartes szorzat részhalmaza. Más szóval veszünk n darab halmazt, és ezek elemeiből rendezett n-eseket képezünk az összes lehetséges módon (ez a Descartes szorzat), majd az n-esek közül kiválasztjuk azokat, amelyek számunkra az adott pillanatban fontosak (elemei a relációnak), a többit pedig elhagyjuk (ezek nem elemei a relációnak). A fenti definícióból az is következik, hogy egy relációban minden rendezett n-es csak egyszer fordulhat elő. A relációkat többféle módon is ábrázolhatjuk. Ezek közül a legkézenfekvőbb a táblázatos leírásmód, ahol a relációban résztvevő minden halmaznak megfeleltetjük a táblázat egy-egy oszlopát, a táblázat soraiban pedig a reláció elemeit képező, összetartozó, rendezett n-esek helyezkednek el. A relációs
adatbáziskezelők ilyen felépítésű táblázatokban (táblákban) tárolják az adatokat. A relációt alkotó halmazokat (azaz a táblák oszlopait) szokás attribútumoknak (tulajdonságoknak) is nevezni. 18 Az SQL név kiejtése magyarul egységes (es-ku-el), angolul azonban kétféle ejtésmód is létezik. Az egyik a három betű angolul kiejtve egymás után, a másik azonban sequel. Mindez, bár az eredetet nem szokták emlegetni, az SQL gyökereire utal. 19 A reláció definíciója általános, tehát a relációs algebra pontosan ugyanúgy definiálja a reláció fogalmát, mint ahogy az a matematikában általában szokásos. A relációkon (táblákon) végrehajtható elemi műveletek a következők: Unió Két reláció uniója egy harmadik reláció, mely tartalmazza a két reláció minden különböző sorát. Metszet Két reláció metszete egy harmadik reláció, mely tartalmazza azokat a sorokat, amelyek mindkét relációban előfordulnak. Különbség
Két reláció különbsége egy harmadik reláció, mely tartalmazza azokat a sorokat, amelyek az első relációban előfordulnak, de a másodikban nem. Oszlopok kiválasztása A művelet eredeti neve vetítés (projection). A műveletben egy reláció bizonyos oszlopait elhagyjuk. Az eredményül kapott sorhalmazban az egyforma sorok közül csak egyet-egyet tartunk meg. Az így keletkező sorok relációt alkotnak. Sorok kiválasztása A művelet eredeti neve szűkítés (restriction). A műveletben egy reláció bizonyos sorait elhagyjuk. Descartes szorzat Egy A és egy B reláció Descartes szorzata egy olyan C reláció, amelynek sorai A és B sorainak összes lehetséges módon történő kombinációjából alakulnak ki. Összekapcsolás Egy A és egy B reláció összekapcsolásának előfeltétele, hogy létezzen legalább egy olyan x attribútum, amely egyaránt megjelenik A-ban és B-ben is. Ekkor A és B összekapcsolásának eredménye egy olyan C reláció, amelynek
sorai A és B azon sorainak kombinációjából adódnak, ahol az A.x és B.x, azaz az A és B x oszlopának adott sorbeli értékei eleget tesznek egy meghatározott kritériumnak (leggyakrabban egyenlőek). Az összekapcsolás művelete visszavezethető a Descartes szorzat képzés és sorok kiválasztásának egymásutánjára, fontossága miatt azonban mégis elemi műveletként szokás emlegetni. A fenti relációs algebra annyira egyszerű és általános, hogy segítségével az adatmodell logikai megjelenése és kezelése függetleníthető az adatok alacsonyabb szintű tárolásának és kezelésének módjától (pl. fizikai tároló, adatok szervezése, alkalmazott algoritmusok) A relációs adatmodell A relációs algebrára épül a relációs adatmodell, mely a relációs adatbáziskezelők gyakorlati megvalósításának és felhasználásának az alapja. Ebben a pontban a relációs adatmodellel kapcsolatos alapfogalmakat tekintjük át. Az előző pontban
elmondottaknak megfelelően a relációs adatmodellben a relációkat táblák formájában ábrázoljuk. A következő példa két egyszerű tábla oszlopait és sorait mutatja be DEPT DEPTNO DNAME --------10 ACCOUNTING 20 RESEARCH 30 SALES 40 OPERATIONS BOSTON -------------- LOC ------------NEW YORK DALLAS CHICAGO EMP EMPNO ENAME JOB -------- ---------- --------7369 SMITH CLERK 7499 ALLEN SALESMAN 7521 WARD SALESMAN 7566 JONES MANAGER 7654 MARTIN SALESMAN 7698 BLAKE MANAGER 7782 CLARK MANAGER 7788 SCOTT ANALYST 7839 KING PRESIDENT 7844 TURNER SALESMAN 7876 ADAMS CLERK 7900 JAMES CLERK 7902 FORD ANALYST 7934 MILLER CLERK MGR HIREDATE SAL COMM DEPTNO -------- --------- -------- -------- -------7902 17-DEC-80 800 20 7698 20-FEB-81 1600 300 30 7698 22-FEB-81 1250 500 30 7839 02-APR-81 2975 20 7698 28-SEP-81 1250 1400 30 7839 01-MAY-81 2850 30 7839 09-JUN-81 2450 10 7566 09-DEC-82 3000 20 17-NOV-81 5000 10 7698 08-SEP-81 1500 0 30 7788 12-JAN-83 1100 20 7698 03-DEC-81 950 30 7566
03-DEC-81 3000 20 7782 23-JAN-82 1300 10 A fenti táblák egy képzeletbeli kis cég személyzeti nyilvántartását tartalmazzák. A DEPT tábla az osztályokról tart nyilván információkat, nevezetesen az osztály azonosítóját (DEPTNO), nevét (DNAME) és telephelyét (LOC). Az EMP tábla az egyes dolgozókról tárol adatokat, pontosabban a dolgozó azonosítóját (EMPNO), nevét (ENAME), beosztását (JOB), főnökének azonosítóját (MGR), a céghez való belépésének dátumát (HIREDATE), fizetését (SAL), jutalékát (COMM) és osztályának azonosítóját (DEPTNO). Kulcsok A ralációk sorai a matematikai definíció értelmében kölönbözőek, és ez a kikötés gyakorlati szempontból is fontos, ha egy tábla adott sorait ki akarjuk keresni, vagy több táblát össze akarunk kapcsolni. Ezeket a tulajdonságokat ún kulcsokkal biztosítják A kulcsok olyan oszlopok vagy oszlopcsoportok, melyeknek értékeire bizonyos megkötéseket teszünk. Az alkalmazott
megkötések alapján a következő fajtájú kulcsokat különböztethetjük meg: Egyedi kulcs (unique key) Az oszlopban (oszlopcsoportban) nem szerepelhet két egyforma érték. Kötelező kulcs (not null) Az oszlopban (oszlopcsoportban) minden értéket ki kell tölteni, azaz egyetlen mező sem maradhat üresen. A null egyébként a relációs adatmodellben és az SQL-ben olyan speciális érték, mely a nemlétező, ismeretlen, ki nem töltött adatot jelöli. Elsődleges kulcs (primary key) Az oszlopban (oszlopcsoportban) nem szerepelhet két egyforma érték, és az oszlopban minden értéket ki kell tölteni. Egy táblában csak egyetlen elsődleges kulcs fordulhat elő. Idegen kulcs (foreign key) Az oszlopban (oszlopcsoportban) csak olyan érték (vagy null) szerepelhet, amely már előfordul egy másik tábla elsődleges vagy egyedi kulcs oszlopában. Az EMP tábla EMPNO oszlopa és a DEPT tábla DEPTNO oszlopa elsődleges kulcsok, amelyek a dolgozók és osztályok egyedi
azonosítóit tartalmazzák. Az EMP tábla DEPTNO oszlopa idegen kulcs, mely a dolgozók osztályszámát tartalmazza, így értelemszerűen a DEPT tábla DEPTNO oszlopára hivatkozik. Az EMP tábla MGR oszlopa idegen kulcs, mely az EMP tábla elsődleges kulcsára hivatkozik, magyarán a cégnél csak olyasvalaki lehet másnak a főnöke, aki maga is a cégnél dolgozik. A kulcsok segítségével válik lehetővé az információk több táblában való tárolása. Ezzel elérhető, hogy az adatok redundanciája - vagyis az ismétlődő adatértékek száma - minimális legyen. Ha például az EMP és DEPT tábla adatait egyetlen közös táblában tárolnánk, akkor minden dolgozó személyi adataiban megjelenne osztályának összes adata, így az osztályszámon kívül az osztály neve és telephelye is. Mindez a táblákban ismétlődő oszlopcsoport-értékek megjelenéséhez vezet A normalizálás célja az ilyen ismétlődő oszlopcsoportok megkeresése, és különálló
táblába való kiemelése. A normalizálásra vonatkozóan a relációs adatbáziskezelők elmélete precíz kritériumokat (ún. normálformákat) és módszereket dolgozott ki, ezek részletezése azonban már nem tartozik jelen témakörünkhöz. Sémák A séma bizonyos szempontok szerint összetartozó adatbázisobjektumok halmaza. A sémák teszik lehetővé többfelhasználós környezetben a felhasználók objektumainak egymástól való elkülönítését, és az objektumokra vonatkozó megfelelő használati jogok kiosztását. A sémák a táblákon kívül sok másfajta objektumot is tartalmazhatnak, melyeket majd az SQL utasítások kapcsán ismertetünk részletesebben. Adatszótár Az adatbázisban lévő minden objektumról nyilvántartást kell vezetni (pl. táblák nevei, oszlopai, a sorok fizikai elhelyezkedésére utaló információk). Erre a célra egy különálló táblastruktúra, az ún metaadatbázis (az adatbázisban tárolt adatokról adatokat tároló
adatbázis) szolgál. Ezt a metaadatbázist nevezik adatszótárnak (data dictionary). Műveletek csoportjai A relációs adatmodell számos, eltérő jellegű művelet végrehajtását teszi lehetővé. Közös jellegzetességeik alapján ezeket a műveleteket az SQL-ben különálló csoportokra, ún. nyelvekre osztják. A relációs algebra minden alapművelete létező halmazokon végzett kiválasztási művelet, mely a meglévő relációkat nem módosítja, hanem azokból újakat állít elő. Ezek a műveletek alkotják az adatkiválasztó nyelvet. A relációs algebra alapműveletein kívül azonban egy valódi adatbázisban további műveletekre is szükségünk van. Így például létre kell tudjunk hozni táblákat, és föl kell tudjuk tölteni őket adatokkal Ebből a szempontból az adatszótár és a felhasználói sémák kezelése eltérő módszereket kíván. Belátható ugyanis, hogy ha az adatszótár tábláiba veszünk föl sorokat, ezzel új objektumokat
definiálunk, és ha az adatszótár meglévő sorait módosítjuk, ezzel létező objektumok szerkezetét változtatjuk meg (pl. táblákhoz új oszlopokat adunk hozzá) Ha azonban egy felhasználói séma tábláinak sorait bővítjük vagy módosítjuk, ezzel csak a felhasználói táblákban tárolt adatokon változtatunk. Az adatszótárban és a felhasználói sémákban tárolt adatok módosítására ezért két különálló utasításcsoportot szokás megkülönböztetni. Ezek a DDL (Data Definition Language) és a DML (Data Manipulation Language). A DDL utasítások mindig az adatszótárt módosítják, és objektumok szerkezetének definícióját illetve általános rendszeradminisztrációs műveletek végrehajtását teszik lehetővé. A DML utasítások a felhasználói sémák objektumainak tartalmát módosítják, azaz segítségükkel sorokat szúrhatunk be, módosíthatunk vagy törölhetünk. Tranzakciók Ha a rendszerben párhuzamosan több felhasználó is
végrehajt adatkezelő (DML vagy DDL) műveleteket, biztosítani kell, hogy ezek hatása ne tegye inkonzisztenssé az egyes felhasználók által olvasott adatokat, illetve az egyes felhasználók által végrehajtott módosítások ne eredményezhessék az adatbázis inkonzisztens állapotát. Az első kritériumot olvasási konzisztenciának nevezik A második kritériumot logikus szóhasználattal írási konzisztenciának nevezhetnénk, e helyett azonban inkább a megvalósítására alkalmazott elv nevét, a kölcsönös kizárást szokás emlegetni. A tranzakció olyan adatmódosító utasítások sorozata, melyek hatása csak együttesen vezet az adatbázis konzisztens állapotához. Ha a tranzakcióban részt vevő műveletek közül akár csak egy is hibásan hajtódna végre, a tranzakcióban végrehajtott minden művelet hatását semlegesíteni kell. A tranzakcióra vonatkozó klasszikus példa a számlák közötti átutalás. Tegyük föl, hogy hónap közepén
megérkezik az átutalási számlánkat vezető bankhoz az elektromos művek kérése a - mondjuk 123 Ft összegű - villanyszámlánk kiegyenlítésére. Ez egy tranzakciót fog eredményezni, melyben a bankszámlánk egyenlegét 123 Ft-tal csökkentik, az elektromos műveknél vezetett tartozási számlánkat pedig kiegyenlítik (ebből is levonják a 123 Ft-ot). Ha mindkét művelet sikeresen lebonyolódik, mindenki elégedett. Ha azonban a két művelet közül csak az egyik hajtódik végre, valaki jól jár a másik rovására, aki persze reklamálni fog. Ha a számlánkat megterhelik, de a második művelet már nem tud végrehajtódni, mondjuk hálózati hiba vagy áramkimaradás miatt, akkor mi leszünk elégedetlenek, hiszen elúszott 123 Ft-unk. Ha valamilyen okból kifolyólag a számlánk megterhelése nem következne be, de az összeg mégis levonódna a tartozási számlánkról, akkor mi jól járnánk, a bankunk azonban vesztene a bolton. A tranzakciók kezelésének
alapja, hogy a rendszerben tárolt adatoknak kétfajta állapota van: egy ideiglenes (ezek folyamatban lévő tranzakcióhoz kapcsolódnak) és egy végleges (ezek olyan adatok, melyeket módosító tranzakciók már befejeződtek). Az ideiglenes adatokhoz nyilvántartja a rendszer az előző értéküket, melynek alapján visszaállítható az adatoknak a tranzakció megkezdése előtti értéke. A tranzakciók kétféleképpen érhetnek véget: Jóváhagyással (commit) A tranzakció által érintett minden adat állapota véglegesítődik, és az adatok régi értékei törlődnek. Visszagörgetéssel (rollback) A tranzakció által érintett minden adat értéke visszaállítódik a tranzakció megkezdése előtti értékre, így a tranzakcióban végrehajtott műveletek hatása semlegesítődik. A tranzakciókhoz szorosan kapcsolódó fogalom a már említett olvasási konzisztencia, mely alapvetően a követkző két dolgot jelenti: Folyamatban lévő tranzakciók által
végrehajtott módosítások hatása más tranzakciókból nem látszik. - Folyamatban lévő olvasási műveletek eredményét nem befolyásolják az idő közben befejeződő tranzakciók által végrehajtott módosítások. Más szóval minden olvasási művelet a végrehajtásának teljes ideje alatt a végrehajtás kezdő időpontjában érvényes adatokat látja. A kölcsönös kizárás úgy valósul meg, hogy a tranzakciók a módosítani kívánt adatelemeket kizárólagosan lefoglalják. A lefoglalt adatelemek lehetnek táblák, blokkok (táblák valahány bájtos, fix méretű fizikai részei) vagy sorok. A kisebb egységek lefoglalása kedvezőbb, mert így kisebb a valószínűsége, hogy a párhuzamos tranzakciók hátráltatják egymás munkáját, azonban minél kisebb egységekben történik a lefoglalás, annál nagyobb lesz a kölcsönös kizárás megvalósításához szükséges szinkronizációs változók száma, melyek kezelése rendkívül komoly problémákat
vet föl. - Az SQL alapvető elemei Az SQL lehetővé teszi szabványos szintaxisú adatlekérdező, adatdefiníciós (DDL), valamint adatmódosító (DML) utasítások végrehajtását. Az SQL - nevével ellentétben - nem strukturált nyelv A legmagasabb szintű SQL programegység az utasítás. Az utasítások között adatok nem cserélhetők ki, és az SQL utasítások nyelvi szinten nem paraméterezhetők. Mindezek a feladatok arra a környezetre hárulnak, amelyben az SQL utasításokat kiadjuk. Bár erre az SQL nyelvi szinten nem teszt semmilyen megkötést, végrehajtása a gyakorlatban mégis kliens/szerver architektúrákhoz kötődik, ahol a szerver komponens hajtja végre az adatdefiníciós, adatmódosító és lekérdező utasításokat, az utasítások bevitele, és az eredmények megjelenítése (egyszóval a kezelői felület megvalósítása) pedig a kliens dolga. A kliens és a szerver tipikusan hálózati kommunikációs felületen tartják egymással a
kapcsolatot. Minden környezetben léteznek olyan egyszerűbb kliens oldali alkalmazások, melyek lehetővé teszik az SQL utasításoknak egy ablakba vagy terminál képernyőre való begépelését, ezután elküldik őket végrehajtásra a szervernek, majd az eredményeket kiírják számunkra a képernyőre (esetleg egy állományba). Ha nem interaktív végrehajtási környezetben gondolkozunk (mert például magunk akarunk kliens oldali alkalmazást írni), az SQL utasítások megfelelő fejlesztőeszköz birtokában procedurális nyelvű (pl. C++, COBOL, Java) forrásszövegbe is beágyazhatók. Ily módon SQL utaításokat tartalmazó strukturált program hozható létre. Az SQL utasításokban az egyes kulcsszavak mellett értékeket jelölő elemek is megjelenhetnek. Az értékek lehetnek oszlopnevek, literálok és kifejezések. Az értékek lehetnek egysorosak és többsorosak Az egysoros értékek literálok, melyek egyetlen értéket jelölnek (pl. alma, 123) A
többsoros értékek oszlopneveket tartalmaznak, melyek egy tábla egy oszlopában lévő értékek halmazát jelölik (pl. HIREDATE) A kifejezések egy- és többsorosak is lehetnek attól függően, hogyan képezzük őket (pl. 123+456 - egysoros kifejezés, SAL*1.3 - többsoros kifejezés) Az SQL típusos nyelv, azaz minden értékhez adattípus tartozik. Az SQL a következő szabványos adttípusokat használja: VARCHAR(n) Változó hosszúságú karakteres adat. Maximális hossza: n (n értéke legfeljebb 2000). CHAR(n) Fix n hosszúságú karakteres adat (n alapértelmezése 1, maximum 255). NUMBER Lebegőpontos szám. NUMBER (n) n jegyből álló egész szám. NUMBER (n,s) Összesen n jegyből álló szám, ebből s tizedesjegy. DATE Dátum adat, amelyben az időpont is tárolható. Az oszlopok típusát a táblák létrehozásakor definiáljuk. Bizonyos korlátozásokkal lehetséges eltérő típusú adatokkal műveletet végezni, mindemellett az explicit
típuskonverzióra számos függvény létezik. Így például a TO CHAR karakteres típusúvá konvertál dátumokat és számokat, a TO DATE dátum típusúvá alakít karakteres és szám típusú adatokat, a TO NUMBER pedig karakteres típusú adatokat alakít számmá. Az átalakításkor az átalakítandó érték mellett többnyire egy formátummaszkot is meg kell adni. Az SQL megengedi aritmetikai és logikai kifejezések használatát. Aritmetikai kifejezések bárhol megjelenhetnek, ahol egy utasításban érték szerepelhet. Az aritmetikai kifejezések tetszőleges SQL adattípushoz tartozó értéket szolgáltathatnak. Aritmetikai kifejezésekben operátorként a +, -, *, / használható, melyek precedenciája20 zárójelezéssel befolyásolható. Az SQL ismeri a || operátort is, mellyel karaktersorozatokat lehet összefűzni, így például a Hü||Jenő kifejezés értéke HüJenő. Logikai kifejezések feltételes utasításrészekben fordulhatnak elő (ezeket a WHERE
vagy HAVING kulcsszó vezeti be). A logikai kifejezések értéke igaz, hamis vagy null lehet A null érték nem igaz, nem hamis, és negáltja önmaga, ezért felbukkanására külön gondot kell fordítani. Az egyszerű logikai kifejezések egy operátorból és egy vagy több operanduszból állnak. Az operandusz érték kell legyen Összetett logikai kifejezéseket egyszerű logikai kifejezésekből az AND, OR és NOT operátorokkal illetve zárójelezéssel lehet kialakítani. Az SQL kifejezésekben megengedi beépített függvények használatát, melyek száma meglehetősen nagy. Az SQL függvények lehetnek egysorosak (ezek az argumentumukban megadott oszlop minden sorára egy-egy újabb értéket adnak vissza eredményül), és lehetnek többsorosak (ezek az A precedencia a szokásos, tehát a szorzás és osztás megelőzi az összeadást és kivonást. Azonos precedenciájú operátorok kiértékelési sorrendje balról jobbra. 20 argumentumukban megadott oszlopban
előforduló valahány értékből származtatnak egyet-egyet, például átlagolással, összegzéssel vagy számlálással). Egyszerű logikai kifejezésekben a következő operátorok alkalmazhatók: Operátor Művelet =, !=, <= , >=, < , > Aritmetikai logikai operátorok: egyenlőségvizsgálat, egyenlőtlenségvizsgálat, kisebb vagy egyenlő, nagyobb vagy egyenlő, kisebb, nagyobb. Például: sal < 1000 hiredate = 15-FEB-98 BETWEEN Zárt intervallumba tartozás vizsgálata. Az operátornak három argumentuma van. Egy érték, az intervallum alsó határa és az intervallum felső határa A kifejezés értéke igaz, ha az érték a megadott zárt intervallumba esik, egyébként hamis. Például: sal between 1000 and 2000 hiredate between 01-JUL-87 and 15-FEB-98 Halmazhoz tartozás vizsgálata. Az operátornak két argumentuma van Az első egy érték, a második pedig zárójelek között felsorolt, egymástól vesszővel elválasztott értékek listája. A
kifejezés értéke igaz, ha az első érték megegyezik a listán szereplő egyik értékkel, egyébként hamis. IN Például: job in (MANAGER, ANALYST, CLERK) ename in (SCOTT, SMITH, CLARK) Karakteres típusú minták illesztése. Első argumentuma egy érték, második argumentuma egy minta, melyben szerepelhetnek % és wildcard karakterek, ahol a % tetszőleges számú, és bármilyen karaktert helyettesíthet, míg az egyetlen, de tetszőleges karaktert helyettesíthet. A kifejezés értéke igaz, ha az első argumentumként megadott érték karakterei illeszkednek a második argumentumként megadott minta karaktereire, figyelembe véve a wildcard karaktereket is. LIKE Például: -- A nevükben A betűt tartalmazó beosztások. job like %A% -- Az S kezdetű, harmadik betűjében O nevű dolgozók. ename like S O% IS NULL Egyargumentumú operátor, mely igaz értéket szolgáltat, ha argumentumának értéke null, egyébként értéke hamis. Például: -- Azok a
dolgozók, akik nem kapnak jutalékot comm is null A műveletek negálhatók (NOT), és egyszerű logikai kifejezésekből az AND és OR műveletekkel összetett logikai kifejezések állíthatók elő. Az SQL a következő alapvető objektumtípusokat ismeri 21: 21 Konkrét SQL implementációkban ezeknél jóval több objektumféleség is megjelenhet. Tábla (Table) A relációkat tároló alapvető struktúra. Sorai és oszlopai vannak Az oszlopokhoz adattípus tartozik, a sorok pedig az oszlopok összetartozó értékeit tartalmazzák. A táblák kezelésekor az oszlopok és sorok sorrendjére vonatkozóan semmiféle előfeltevéssel nem élhetünk. A táblákhoz az adatbázisban fizikailag elkülönülő adatterületek tartoznak. Nézet (View) Virtuális tábla, melyet egy alkalmas lekérdező művelet segítségével definiálunk. Nem tartozik hozzá fizikailag elkülönülő adatterület A nézeteken keresztül végzett műveletek valójában a nézet alaptábláin
hajtódnak végre. Bár a nézetek különböző SQL műveletekben alapvetően táblaként viselkednek, bizonyos műveletek csak korlátozásokkal végezhetők el a segítségükkel. Index Nagyméretű táblákban végzett keresés gyorsítására szolgál. Szerkezetére és kezelésére többféle trükkös eljárás is létezik, így például lehet indexet kezelni a B-fa eljárással, Hash táblákkal vagy a bitminta indexelés segítségével. Ezek részletezése izgalmas és hasznos, de meghaladja jegyzetünk kereteit, másrészt a nagy adatbáziskezelők automatikusan generálják és kezelik az indexeket. Szinoníma (Synonym) A szinoníma alternatív név egy létező objektumra. Főként olyankor hasznos, ha az alapobjektum neve bonyolult (pl. több tagból áll), nem fejezi ki a lényeget vagy egyszerűen csak meg akarjuk változtatni, és a régi nevet is meg akarjuk tartani. Tárolt eljárás (Stored procedure) A tárolt eljárások olyan, procedurális nyelven megírt
programegységek, melyek beágyazott SQL utasításokon keresztül komplex műveleteket végeznek az adatbázis objektumain, és SQL utasítással végrehajthatók. Csak a komolyabb relációs adatbáziskezelők teszik lehetővé tárolt eljárások használatát. Mivel nyelvük - az SQL-lel ellentétben rendszerfüggő, a továbbiakban - komoly gyakorlati jelentőségük ellenére a tárolt eljárásokkal nem foglalkozunk Trigger A trigger olyan procedurális programegység, mely táblákhoz kapcsolódik, és a táblán végrehajtott, előre definiált típusú DML utasítás (insert, update vagy delete) hatására automatikusan végrehajtódik. A triggerekre ugyanazok a további megjegyzéseink, mint a tárolt eljárásokra. Az SQL főbb utasításai A relációs algebra és a segítségével fölépíthető relációs adatmodell általánosságából következően az SQL eszközfüggetlen nyelv, mellyel el tudunk végezni minden olyan műveletet, amely egy relációs
adatbáziskezelőhöz kapcsolódik. A következőkben bemutatjuk az SQL leglényegesebb utasításait, és használatukat példákon keresztül szemléltetjük. Az SQL a következő utasításcsoportokat ismeri: SELECT Adatok olvasása az adatbázisból, rendezés, szűrés, csoportosítás, járulékos számítások. INSERT UPDATE DELETE Adatok megváltoztatására az adatbázisban (DML utasítások) CREATE ALTER DROP Objektumok szerkezetének definiálása (DDL utasítások) COMMIT ROLLBACK SAVEPOINT Tranzakciók kezelése. GRANT REVOKE Felhasználói jogok kezelése. A továbbiakban néhány példát mutatunk be az egyes utasítások használatára vonatkozóan. A példákban az SQL utasításokat egy interaktív környezeten keresztül hajtjuk végre, mely az utasítás szövegének begépelése után az utasítást a szerverrel végrehajtatja, majd az eredményt a képernyőre írja. SELECT A select utasítás általános szintaxisa a következő: SELECT
<oszlop>[{,<oszlop>}] FROM <tábla>[{,<tábla>}] [WHERE <logikai kifejezés>] [GROUP BY <oszlop>|<kifejezés>[{,<oszlop>|<kifejezés>}]] [HAVING <logikai kifejezés>] [ORDER BY <oszlop>|<kifejezés> [DESC][{,<oszlop>|<kifejezés> [DESC]}] A következőkben egyszerű példákon keresztül bemutatjuk a SELECT utasítás főbb lehetőségeit. Oszlopok kiválasztása, kifejezések és literálok használata, null értékek kezelése, oszlopálnevek: Példa: Listázzuk ki minden dolgozó nevét és éves jövedelmét! SQL> 2 ENAME ---------SMITH ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER select ename, sal*12+nvl(comm,0) from eves jov emp; EVES JOV -------9600 19500 15500 35700 16400 34200 29400 36000 60000 18000 13200 11400 36000 15600 Oszlopok lülönböző sorainak kiválasztása: Példa: Hány különböző beosztás van a cégnél osztályonkénti
bontásban? SQL> 2 DEPTNO -------10 10 10 20 20 20 30 30 30 SALESMAN select distinct deptno, from job emp; JOB --------CLERK MANAGER PRESIDENT ANALYST CLERK MANAGER CLERK MANAGER Eredménysorok rendezése belépési dátum szerint csökkenő sorrendben. Példa: Írjuk ki minden dolgozó nevét, beosztását és belépési dátumát a belépési dátum szerint csökkenő sorrendben. (A későbbi dátum nagyobbnak számít) SQL> 2 3 select ename, job, hiredate hiredate emp desc; PRESIDENT SALESMAN SALESMAN MANAGER MANAGER MANAGER SALESMAN SALESMAN HIREDATE --------12-JAN-83 09-DEC-82 23-JAN-82 03-DEC-81 03-DEC-81 17-NOV-81 28-SEP-81 08-SEP-81 09-JUN-81 01-MAY-81 02-APR-81 22-FEB-81 20-FEB-81 from order ENAME ---------ADAMS SCOTT MILLER JAMES FORD KING MARTIN TURNER CLARK BLAKE JONES WARD ALLEN SMITH CLERK by JOB --------CLERK ANALYST CLERK CLERK ANALYST 17-DEC-80 Eredménysorok szűrése összetett logikai feltétellel: Példa: Listázzuk ki minden olyan dolgozó
azonosítóját, nevét, beosztását és fizetését, akik hivatalnokok (CLERK), és fizetésük 1000 és 2000 közé esik! SQL> 2 3 4 select EMPNO ENAME -------7876 ADAMS 7934 MILLER empno, where sal ename, from between 1000 and JOB ---------- --------CLERK CLERK 1300 job, sal emp and 2000 job=CLERK; SAL -------1100 Literálok és összefűzés használata értelmes töltelékszövegek kiíratására: SQL> select ename|| az egy tok jo ||job||. 2 from emp; ENAME||AZEGYTOKJO||JOB||. ----------------------------------SMITH az egy tok jo CLERK. ALLEN az egy tok jo SALESMAN. WARD az egy tok jo SALESMAN. JONES az egy tok jo MANAGER. MARTIN az egy tok jo SALESMAN. BLAKE az egy tok jo MANAGER. CLARK az egy tok jo MANAGER. SCOTT az egy tok jo ANALYST. KING az egy tok jo PRESIDENT. TURNER az egy tok jo SALESMAN. ADAMS az egy tok jo CLERK. JAMES az egy tok jo CLERK. FORD az egy tok jo ANALYST. MILLER az egy tok jo CLERK. Táblák összekapcsolása: Példa: Listázzuk ki
minden dolgozó nevét, foglalkozását, fizetését, osztályának nevét és telephelyét, valamint osztályának azonosítóját! SQL> select ename, job, sal, dname, loc, e.deptno 2 from emp e, dept d 3 where e.deptno = ddeptno; ENAME ---------SMITH ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER JOB SAL DNAME --------- --------- -------------CLERK 800 RESEARCH SALESMAN 1600 SALES SALESMAN 1250 SALES MANAGER 2975 RESEARCH SALESMAN 1250 SALES MANAGER 2850 SALES MANAGER 2450 ACCOUNTING ANALYST 3000 RESEARCH PRESIDENT 5000 ACCOUNTING SALESMAN 1500 SALES CLERK 1100 RESEARCH CLERK 950 SALES ANALYST 3000 RESEARCH CLERK 1300 ACCOUNTING LOC DEPTNO ------------- --------DALLAS 20 CHICAGO 30 CHICAGO 30 DALLAS 20 CHICAGO 30 CHICAGO 30 NEW YORK 10 DALLAS 20 NEW YORK 10 CHICAGO 30 DALLAS 20 CHICAGO 30 DALLAS 20 NEW YORK 10 Tábla összekapcsolása önmagával: Példa: Listázzuk ki minden dolgozó nevét, és közvetlen főnökének nevét! SQL> select
b.ename beosztott, fename fonok 2 from emp b, emp f 3 where b.mgr = fempno; BEOSZTOTT ---------SMITH ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT TURNER ADAMS JAMES FORD MILLER FONOK ---------FORD BLAKE BLAKE KING BLAKE KING KING JONES BLAKE SCOTT BLAKE JONES CLARK Összesítések képzése csoportfüggvények segítségével: Példa: Listázzuk ki osztályonkénti és munkakörönkénti bontásban az átlagfizetéseket és az összes fizetést! SQL> select deptno, job, avg(sal) atlag, sum(sal) osszesen 2 from emp 3 group by deptno, job; DEPTNO --------10 10 10 20 20 20 30 30 30 JOB ATLAG OSSZESEN --------- --------- --------CLERK 1300 1300 MANAGER 2450 2450 PRESIDENT 5000 5000 ANALYST 3000 6000 CLERK 950 1900 MANAGER 2975 2975 CLERK 950 950 MANAGER 2850 2850 SALESMAN 1400 5600 Szűrés csoportfüggvények értékeire: Példa: Listázzuk ki azokat a munkaköröket, amelyekben a fizetés magasabb, mint 1500! SQL> 2 3 4 select job, avg(sal) atlag from emp group by job having
avg(sal) > 1500 JOB ATLAG --------- --------ANALYST 3000 MANAGER 2758.3333 PRESIDENT 5000 Szűrés beágyazott lekérdezés segítségével: Példa: Listázzuk ki azoknak a nevét és fizetését, akik többet keresnek, mint a cégátlag! SQL> select ename, job, sal 2 from emp 3 where sal > (select avg(sal) from emp) ENAME ---------JONES BLAKE CLARK SCOTT KING FORD JOB SAL --------- --------MANAGER 2975 MANAGER 2850 MANAGER 2450 ANALYST 3000 PRESIDENT 5000 ANALYST 3000 CREATE, ALTER, DROP Ezek az utasítások táblák, nézetek, indexek és szinonímák létrehozására, definíciójuk módosítására és törlésére szolgálnak. Mivel szintaxisuk objektumfüggő, ezért az egyes objektumféleségekre külön tárgyaljuk őket. Táblák Táblákat a CREATE TABLE utasítással hozhatunk létre. Az utasítás kétféle szintaxissal írható le Az első fajta szintaxisban a táblát oszloponként definiáljuk, és megadjuk a szükséges kiegészítő információkat
(adattípusok, kulcsok). A második fajta szintaxissal egy létező tábláról (vagy annak egyes részeiről) készíthetünk másolatot, egy beágyazott SELECT utasítás segítségével. Az első fajta szintaxis a következő: CREATE TABLE <táblanév> ( <oszlopspecifikáció>|<táblaszintű kényszer> [{,<oszlopspecifikáció>|<táblaszintű kényszer>}]) <oszlopspecifikáció> ::= <oszlopnév> <adattípus> [DEFAULT <kezdőérték>] [{<oszlopszintű kényszer>}] <táblaszintű kényszer> ::= [CONSTRAINT <kényszernév>] UNIQUE (<oszlopnév>[{,<oszlopnév>}]) | PRIMARY KEY (<oszlopnév>[{,<oszlopnév>}]) | FOREIGN KEY (<oszlopnév>[{,<oszlopnév>}]) REFERENCES <táblanév>(<oszlopnév>[{,<oszlopnév>}]) | CHECK (<kifejezés>) <oszlopszintű kényszer> ::= [CONSTRAINT <kényszernév>] NOT NULL | UNIQUE | PRIMARY KEY | REFERENCES
<táblanév>(<oszlopnév>)| CHECK (<kifejezés>) A kényszerek (constraint) olyan járulékos ellenőrzéseket írnak elő, melyek az adatintegritás fenntartása érdekében nélkülözhetetlenek. A NOT NULL kényszer kötelező kulcsot, a UNIQUE kényszer egyedi kulcsot, a PRIMARY KEY kényszer elsődleges kulcsot, a FOREIGN KEY (REFERENCES) kényszer pedig idegen kulcsot definiál. A CHECK kényszerről korábban nem volt szó. Ennek segítségével egy olyan kifejezést adhatunk meg, amely a megfelelő oszlopoknak történő értékadáskor (INSERT, UPDATE) automatikusan kiértékelődik, és ha a kifejezés értéke hamis, az értékadás meghiúsul. Az oszlopszintű és táblaszintű kényszerek között az az alapvető különbség, hogy míg az oszlopszintű kényszerekben csak az adott oszlop értékeire hivatkozhatunk, addig táblaszintű kényszerekben a tábla tetszőleges oszlopára. Az EMP tábla definíciója: CREATE TABLE EMP ( EMPNO NUMBER(4)
CONSTRAINT EMP PK PRIMARY KEY, ENAME VARCHAR2(10) CONSTRAINT ENAME CHK CHECK( ENAME=UPPER(ENAME) ), JOB VARCHAR2(10), MGR NUMBER(4) CONSTRAINT EMP MGR FK REFERENCES EMP(EMPNO), HIREDATE DATE DEFAULT SYSDATE, SAL NUMBER(7,2) NOT NULL, COMM NUMBER(7,2), DEPTNO NUMBER(2) NOT NULL, CONSTRAINT EMP DEPT FK FOREIGN KEY (DEPTNO) REFERENCES DEPT(DEPTNO) ); A CREATE TABLE utasításnak létezik egy beágyazott SELECT utasításon alapuló szintaxisa is, mely a következő: CREATE TABLE <táblanév> [(<oszloplista>)] AS <select utasítás> Hatására egy <táblanév> nevű, <oszloplista> által meghatározott, vagy a <select utasítás> oszloplistájában szereplő oszlopokat tartalmazó tábla keletkezik, melynek tartalmát a <select utasítás> szolgáltatja. Az <oszloplista> egymástól vesszővel elválasztott oszlopnevekből áll, és ha megadjuk, pontosan annyi elemű kell legyen, mint ahány elemet a <select utasítás> oszloplistája
tartalmaz. Segítségével az eredeti oszlopnevek megváltoztathatók A <select utasítás> oszlopneveinek és az <oszloplista> oszlopneveinek az összerendelése pozíció szerint történik. Ha a <select utasítás> oszloplistájában összetett névhivatkozás vagy számított oszlop szerepel, kötelező megadni az oszloplistát, vagy az ilyen oszlopokat álnévvel elfedni. A <select utasítás> nem tartalmazhat ORDER BY utasításrészt. A CREATE TABLE AS SELECT utasítás hatására létrejön egy új tábla, melynek oszlopai típus és név szerint megegyeznek az utasításban megadottal, és az új táblába átmásolódnak a SELECT utasítás eredményeként előálló adatok. Mindezt a következő példa is szemlélteti SQL> 2 3 4 5 create table emp10 as select empno, ename, job, sal from emp where deptno = 10; Table created. SQL> select * from emp10; EMPNO --------7782 7839 7934 ENAME ---------CLARK KING MILLER JOB SAL ---------
--------MANAGER 2450 PRESIDENT 5000 CLERK 1300 A DROP TABLE utasítás a tábladefiníciót (és természetesen a táblában tárolt összes adatot) törli. Szintaxisa nagyon egyszerű. A két bevezető kulcsszó után csak a tábla nevét kell megadni, például: DROP TABLE emp; Az ALTER TABLE utasítás ezzel szemben meglehetősen nyakatekert, olyannyira, hogy szintaxisát itt nem is részletezzük, csak megemlítjük, hogy segítségével a táblákhoz oszlopokat és kényszereket adhatunk, illetve kényszereket törölhetünk, valamint kényszereket ki-be kapcsolhatunk. Nézetek A nézeteket lekérdezések segítségével definiáljuk, melyekhez nevet rendelünk. Ezután a nézetekre többnyire pontosan ugyanolyan módon hivatkozhatunk, mint a táblákra. A nézetekkel egyszerű módon fogalmazhatunk meg bonyolult lekérdezéseket, eltakarhatjuk az egyes felhasználók elől a nemkívánt részleteket, egyszóval strukturáltabbá, és könnyeben kezelhetővé válik a
rendszerünk. A nézet definíciójának szintaxisa a következő: CREATE VIEW <nézetnév> [(<oszloplista>)] AS <select utasítás> Az <oszloplista> egymástól vesszővel elválasztott oszlopnevekből áll, és ha megadjuk, pontosan annyi elemű kell legyen, mint ahány elemet a <select utasítás> oszloplistája tartalmaz. Segítségével az eredeti oszlopnevek megváltoztathatók. A <select utasítás> oszlopneveinek és az <oszloplista> oszlopneveinek az összerendelése pozíció szerint történik. Ha a <select utasítás> oszloplistájában összetett névhivatkozás vagy számított oszlop szerepel, ilyenkor kötelező megadni az oszloplistát, vagy az ilyen oszlopokat álnévvel elfedni. A <select utasítás> nem tartalmazhat ORDER BY utasításrészt. A nézetek lehetnek egyszerűek (egyetlen alaptáblára hivatkoznak, és nem tartalmaznak számított oszlopokat) és összetettek (több alaptáblára hivatkoznak,
és/vagy számított oszlopokat tartalmaznak. A következő példa egyszerű nézet létrehozását és használatát mutatja: SQL> 2 3 4 5* create view analysts as select ename, job, sal, hiredate from emp where job = ANALYST; View created. SQL> select * from analysts; ENAME ---------SCOTT FORD JOB SAL HIREDATE --------- --------- --------ANALYST 3000 19-APR-87 ANALYST 3000 03-DEC-81 Összetett nézet használata: Példa: A feladat minden dolgozó neve mellett kilistázni osztályának átlagfizetését is. SQL> 2 3 4 5 6 create view ossz (deptno, atlag, osszes, db) as select deptno, avg(sal), sum(sal), count(sal) from emp group by deptno; View created. SQL> select ename, job, sal, atlag 2 from emp, ossz 3 where emp.deptno = osszdeptno; ENAME ---------CLARK KING MILLER SMITH ADAMS FORD SCOTT JONES ALLEN BLAKE MARTIN JAMES TURNER WARD JOB SAL ATLAG --------- --------- --------MANAGER 2450 2916.6667 PRESIDENT 5000 2916.6667 CLERK 1300 2916.6667 CLERK 800 2175 CLERK
1100 2175 ANALYST 3000 2175 ANALYST 3000 2175 MANAGER 2975 2175 SALESMAN 1600 1566.6667 MANAGER 2850 1566.6667 SALESMAN 1250 1566.6667 CLERK 950 1566.6667 SALESMAN 1500 1566.6667 SALESMAN 1250 1566.6667 Két táblából összekapcsolt összetett nézet használata: SQL> 2 3 4 5 create view emp dept as select ename, job, sal, dname, loc from emp, dept where emp.deptno = deptdeptno; View created. SQL> select * from emp dept; ENAME ---------SMITH ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER JOB SAL DNAME --------- --------- -------------CLERK 800 RESEARCH SALESMAN 1600 SALES SALESMAN 1250 SALES MANAGER 2975 RESEARCH SALESMAN 1250 SALES MANAGER 2850 SALES MANAGER 2450 ACCOUNTING ANALYST 3000 RESEARCH PRESIDENT 5000 ACCOUNTING SALESMAN 1500 SALES CLERK 1100 RESEARCH CLERK 950 SALES ANALYST 3000 RESEARCH CLERK 1300 ACCOUNTING LOC ------------DALLAS CHICAGO CHICAGO DALLAS CHICAGO CHICAGO NEW YORK DALLAS NEW YORK CHICAGO DALLAS CHICAGO DALLAS NEW
YORK A nézeteken pontosan ugyanúgy hajthatók végre lekérdezések, mint táblákon. A DML műveletekkel kapcsolatban azonban korlátozások érvényesek. Ezek lényegében kizárják DML műveletek végrehajtását összetett nézeteken. A korlátozások közül egyesek technikai jellegűek, mások azonban elviek. Így például általánosságban megoldhatatlan az alaptábla több oszlopának frissítése, olyan nézeteken keresztül, ahol az alaptábla oszlopai a nézet egyetlen számított oszlopát definiáló kifejezésben bukkanak föl (pl. sal+comm) Konkrét adatbáziskezelő rendszerekben a DML utasításokra vonatkozó egyes korlátozások lehet, hogy áthághatók. Nézetek nem módosíthatók, és a DROP VIEW <nézetnév> utasítás segítségével törölhetők. INSERT Segítségével egy meglévő táblába szúrhatunk be újabb sorokat. Két formája létezik, az egysoros és a többsoros. A többsoros INSERT utasítás beágyazott lekérdezés segítségével
másol egyik táblából sorokat egy másikba. Az egysoros INSERT utasítás szintaxisa a következő: INSERT INTO <táblanév> [(<oszlop>[{,<oszlop>})] VALUES (<érték>[{,<érték>}]) A <táblanév> utáni oszloplista opcionális. Ha elhagyjuk, akkor a VALUES listában kötelesek vagyunk megadni az adott tábla összes oszlopának értékét, mégpedig pontosan ugyanolyan sorrendben, ahogy az oszlopok a tábla definíciójában követték egymást. Ha megadjuk az oszloplistát, akkor az értékeknek pontosan olyan számban, olyan sorrendben és megfelelő típussal kell követniük egymást, mint ahogy azokat az oszloplistán fölsoroltuk. Ha az oszloplistán egy adott oszlop nem szerepel, annak értéke null érték (vagyis az adott mező üresen marad). Ezeket a jellegzetességeket szemlélteti a következő példa. SQL> insert into dept values 2 (12, SIKKASZTÓK, PANAMA); 1 row created. SQL> insert into dept 2 (loc, deptno) values 3
(ZENEKAR, 13); 1 row created. SQL> select * from dept; DEPTNO --------10 20 30 40 12 13 DNAME -------------ACCOUNTING RESEARCH SALES OPERATIONS SIKKASZTÓK LOC ------------NEW YORK DALLAS CHICAGO BOSTON PANAMA ZENEKAR A töbsoros INSERT utasítás szintaxisa a következő: INSERT INTO <táblanév> [(<oszlop>[{,<oszlop>})] <select utasítás> Ha az oszloplistát elhagyjuk, a select utasításnak a <táblanév> tábla oszlopaihoz képest típusában, számában és sorrendjében megegyező értékeket kell szolgáltatnia, egyébként ugyanez igaz az oszloplista elemeire vonatkozóan. Ezt szemlélteti a következő kis példa SQL> 2 3 4 insert into emp20 select empno, ename, job, sal from emp where deptno = 20; 5 rows created. SQL> select * from emp20; EMPNO --------7782 7839 7934 7369 7566 7788 7876 7902 ENAME ---------CLARK KING MILLER SMITH JONES SCOTT ADAMS FORD JOB SAL --------- --------MANAGER 2450 PRESIDENT 5000 CLERK 1300 CLERK 800
MANAGER 2975 ANALYST 3000 CLERK 1100 ANALYST 3000 UPDATE Az UPDATE utasítás segítségével táblák sorait módosíthatjuk. Az utasítás általános szintaxisa a következő: UPDATE <táblanév> SET <oszlop> = <érték> {,<oszlop> = <érték>}] [WHERE <logikai kifejezés>] Például: SQL> update e 2 set ename = Kovacs 3 where ename = SMITH; 1 row updated. A feltételrész elhagyásakor az UPDATE utasítás a tábla minden sorát módosítja. DELETE A DELETE utasítás segítségével táblák sorait törölhetjük. Szintaxisa a következő: DELETE [FROM] <táblanév> [WHERE <logikai kifejezés>] Ha nem adunk meg feltételt, a tábla összes sora törlődik. COMMIT, ROLLBACK, SAVEPOINT Tranzakciók kezdetét, végét és lényeges pontjait jelölik. A COMMIT utasítás hatására a tranzakció során végrehajtott DML műveletek hatása véglegesítődik, és a tranzakció lezárul. A ROLLBACK utasítás hatására a
tranzakció során végrehajtott DML műveletek hatása semlegesítődik, azaz visszaállnak a tranzakció megkezdése előtti adatértékek, majd a tranzakció lezárul. A SAVEPOINT utasítással a tranzakció belsejében olyan közbülső pontokat jelölhetünk ki, amelyre a rendszer állapota (az adatértékeken keresztül) visszaállhat. Ezzel a tranzakciónkat olyan részekre oszthatjuk, melyek visszagördítése egymástól bizonyos mértékig függetleníthető, a jóváhagyás azonban továbbra is csak egységesen történhet. GRANT, REVOKE A GRANT segítségével jogokat adhatunk egy adott felhasználónak adott műveletek elvégzésére általánosságban vagy adott objektumhoz kötődően. A REVOKE segítségével jogokat lehet megvonni egy felhasználótól. Példa: CREATE USER mici IDENTIFIED BY macko; GRANT CONNECT, RESOURCE TO mici; -- nem szabványos SQL jogok 22 GRANT SELECT, INSERT, REFERENCE ON emp TO mici; Az SQL végrehajtási környezete A gyakorlatban az SQL
végrehajtása kliens/szerver architektúrájú rendszerekhez kötődik, melyben az adatbáziskezelő szerver hajtja végre az autonóm módon működő adatmegjelenítő, adatmódosító és adatbeviteli alkalmazásoktól érkező SQL kéréseket, majd az eredményt visszaküldi a kliens számára, aki azon további műveleteket végezhet. Kezdünk az általános műveletek irányából az implementációfüggő rendszereadminisztrációs utasítások világába átevezni. Hogy egy adatbáziskezelő a jogokat hogyan kezeli és milyen módon osztályozza, sokkal esetlegesebb, mint mindaz, amiről a korábbiakban szó volt. 22 Az SQL utasításokat a szerver részeként működő SQL interpreter hajtja végre a következő lépések sorozatán keresztül: 1. Létrejön egy munkaterület, melyen keresztül a későbbiekben elérhetők lesznek az utasítás végrehajtásakor eredményül kapott adatok, a végrehajtás aktuális állapota, a hibaüzenetek, és általában minden, az
utasítás végrehajtásához szükséges segédinformáció. Ezt a munkaterületet kurzornak hívják, és a hagyományos programnyelvek konstrukciói közül leginkább egy fájlleíróra 23 emlékeztet. 2. A szerver elemzi az SQL utasítás szövegét Ez jóval bonyolultabb folyamat, mint általában egy magasszintű program egy utasításának elemzése, és a következő műveleteket foglalja magába: - Szintaktikai elemzés: Az utasítás szövegének elemzése, a szintaktiai hibák ellenőrzése. - Jogosultságok ellenőrzése: Az adatszótáron végrehajtott megfelelő lekérdezések segítségével az interpreter ellenőrzi, hogy az utasítás végrehajtását kérő felhasználó jogosult-e az adott utasítás végrehajtására. - Objektumhivatkozások ellenőrzése: Az adatszótár lekérdezésével ellenőrizni kell, hogy a felhasználó jogosult-e az általa hivatkozott objektumokon az adott utasítások végrehajtására, illetve, hogy a hivatkozott objektumok léteznek-e
egyáltalán. - Végrehajtási terv készítése: Ez az SQL interpreter működésének legkényesebb része, mely végső soron a kódgenerálást jelenti. A végrehajtási terv jellegzetességeit egy példán keresztül szemléltetjük. Tekintsük a következő, viszonylag összetett SELECT utasítást, mely kikeresi minden olyan dolgozó nevét, foglalkozását, fizetését és osztályának nevét, akinek az átlagfizetése magasabb, mint az osztályának az átlaga. SELECT ename, job, sal, dname FROM emp e, dept d WHERE e.deptno = ddeptno AND sal > (SELECT avg(sal) FROM emp b WHERE b.deptno = edeptno); Az utasítás egy lehetséges végrehajtási terve a következő ábrán látható. 23 A fájlleíró közismert módon egy olyan struktúra, melyen keresztül hozzáférhetünk egy állományban tárolt adatokhoz, azonban maga a fájlleíró nem tárol adatokat. A végrehajtási terv adatfolyam fa, melynek csomópontjaiban az SQL utasítás végrehajtásának olyan elemi
lépései helyezkednek el, mint a táblákban a sorok keresésének módja (pl. indexelt keresés vagy a teljes tábla végigolvasása), a táblaösszekapcsolás módja (pl. összefűzés rendezéssel, összefűzés egymásba ágyazott ciklusokkal) és egyebek. Ugyanazt a műveletet - különösen bonyolult SELECT utasítások esetén - többféle struktúrájú végrehajtási terv alapján is el lehet végezni, ráadásul az egyes csomópontokban sokféle elemi utasítás is elfogadható lehet. Így a szóbajöhető végrehajtási tervek száma rendkívül nagy, a végrehajtás szempontjából azonban az egyes végrehajtási tervek sebessége és költsége nagyságrendi eltéréseket mutathat. SELECT utasítás Szűrés Ciklikus összekapcsolás Teljes tábla olvasás (EMP) Olvasás fizikai sorcím alapján Rendezés átlagolás céljából Teljes tábla olvasás (EMP) Eljutottunk tehát a deklaratív rendszerek általános problémájához: rendkívül nagyszámú
lehetséges megoldás közül kell kiválasztanunk Sorkeresés a megfelelőt, mindezt az SQL elsődleges esetében nagyon rövid (másodperces kulcs alapján nagyságrendú) idő alatt kell (DEPT) elérnünk. Ezt a feladatot az SQL 2.11 ábra: Egy végrehajtási terv felépítése optimalizáló oldja meg, mely a legkedvezőbb végrehajtási tervet általában valamiféle heurisztikus kereséssel állítja elő. 3. A szerver elvégzi a végrehajtási tervben kijelölt műveleteket A végrehajtási terv bármely csomópontjában megjelenő részművelet végrehajtható, ha a közvetlenül alatta lévő csomópontban elhelyezkedő műveleteket már mind végrehajtottuk, és azok eredményeket produkáltak. Ebből következően a végrehajtási terv kiértékelése a végágak irányából indul, majd így halad egyre följebb, míg el nem jutunk a magát az SQL utasítást reprezentáló legfelső csomópontig. Ha ezen is túljutottunk, az utasítás végrehajtása befejeződött. Az
ábrán látható végrehajtási terv első lépéseként az SQL interpreter kikeresi az EMP tábla, összes sorát, majd az EMP tábla minden sorához kikeresi a DEPT tábla elsődleges kulcsa alapján a megfelelő osztály nevét, és a két összetartozó sordarabot összekapcsolja. Érdemes megfigyelni, hogy a ciklikus összekapcsolás esetén az adatok nem csak alulról fölfelé áramlanak, hanem paraméterek (esetünkben az osztályszámok) formájában fölülről lefelé is. Ez röviden annyit jelent, hogy egy magasabb szintű csomópont átadhat paramétereket az alacsonyabb szintűnek, ha ezt végrehajtási módja megkívánja. Ezután az osztályátlagok számítása következik az EMP tábla teljes végigolvasásával, és osztályszám szerinti rendezésével. Mivel az egy csoportba tartozó adatok ekkor garantáltan egymás után következnek, az összesítéshez csak még egyszer végig kell olvasni a rendezett táblát, és eközben az összesített értékek
(esetünkben az átlagfizetés) egyszerűen kiszámítható. Az utolsó lépés (a szűrés) az osztályátlagok és az összekapcsolás eredményeként keletkező sorokban megjelenő fizetések összehasonlítása, és az átlagnál kisebb vagy egyenlő sorok eldobása. A szűrés során a rendszer azt is figyelembe veszi, hogy az adott sor melyik osztály dolgozójára vonatkozik, ugyanis a megfelelő osztály átlagához kell a fizetést hasonlítania24. Érdemes meggondolni, mit jelent a különböző kategóriájú SQL utasítások végrehajtása. A SELECT utasítás eredménye valahány darab sor, amelyeket majd az utasításhoz kapcsolódó kurzoron keresztül tud visszaolvasni a kliens. Az adatkezelő utasítások közül végrehajtás szempontjából különösen az UPDATE és DELETE érdekesek, ezek végrehajtásához ugyanis az SQL interpreternek előbb az adatszótár alapján ki kell keresnie az utasítások által érintett sorokat, ami azt jelenti, hogy ezek
végrehajtása mindig egy implicit SELECT utasítás végrehajtását is maga után vonja. Az adatdefiníciós utasítások mindegyikének hatására az interpreter valahány darab implicit INSERT, UPDATE és DELETE utasítást hajt végre, azonban az adatkezelő utasításokkal ellentétben ezek nem a felhasználók tábláin, hanem az adatszótárban végeznek módosításokat. Ez egyben azt is jelenti, hogy az adatszótár tábláin a felhasználók közvetlenül nem hajthatnak végre INSERT, UPDATE és DELETE utasításokat 25. 4. A kliens az eredményül kapott sorokat egyesével visszaolvassa, és feldolgozza Ez SELECT utasítások végrehajtásakor fontos. A beolvasás az utasításhoz megnyitott kurzorban található mutatón keresztül történik, mely minden újabb sor felhozatala után automatikusan eggyel tovább mozdul. A mutató visszafelé nem mozgatható, és az eredményhalmazon nem is pozicionálható Ez egyben azt is jelenti, hogy egy adott eredménysor csak akkor
olvasható be újra, ha az adott utasítást újra végrehajtjuk. A kurzoron keresztül való olvasás egyirányú volta a legelapvetőbb különbség a fájlleírókhoz képest. 5. Az eredmények feldogozása után a kurzor lezárul, ezzel megszűnik a munkaterület, és felszabadul az eredmények tárolására fenntartott ideiglenes terület. A példában szereplő beágyazott lekérdezést korrelált allekérdezésnek nevezik. Arról nevezetes, hogy a beágyazott lekérdezés végrehajtása csak a külső lekérdezés aktuális sorának (esetünkben az osztályszámnak) az ismeretében lehetséges, és a beágyazott lekérdezés elvileg újra végrehajtódik a külső lekérdezés minden sorára. 24 Persze, ha valaki vonzódik az olyan jellegű bátorságpróbákhoz, mint a gumikötélen szakadékba ugrálás, az konkrét adatbáziskezelő rendszerekben kellő jogok birtokában közvetlenül módosíthatja az adatszótárt, de pontosan olyan feltételek mellett, mint amelyek a
Trabanttal szűk hajtűkanyarban 130-cal való közlekedésre érvényesek: lehet, de csak egyszer. Azért egy kis különbség mégis van A Trabantos művelet után az ember jó esetben túléli a dolgot, keletkezik mondjuk százezer forintnyi kára, és egy tapasztalattal gazdagabb lett. A Rotschild Bankház összeomlott pénzügyi adatbázisát túlélni ugyan feltehetően könnyebb, az okozott kár azonban néhány nagységrenddel nagyobb lesz, és ezek után még örülhetünk, ha gyorsan kirúgnak bennünket az állásunkból, és peres úton nem köteleznek olyan kártérítésre, amelyet életünk végéig sem tudunk kifizetni. 25 2.52 4GL fejlesztőeszközök A 4GL betűszó a 4th Generation Language (negyedik generációs nyelv) szavak rövidítése. Bár az elnevezés bevett, és lépten nyomon találkozunk vele, félrevezető, ugyanis a 4GL eszközök valójában nem nyelvek, hanem egy (vagy több) magasszintű nyelvre épülő komplex, objektumorientált
programfejlesztői környezetek. Így például a Basic egy programozási nyelv, de a Visual Basic 4GL Szoftverrendszer alkalmazásfejlesztő eszköz. Az első 4GL alkalmazásfejlesztő eszközök a nyolcvanas évek közepén jelentek meg, és használatuk a kilencvenes Alkalmazáslogika évek második felére tömeges méreteket öltött. A 4GL eszközök működése azon a tényen alapul, hogy a szoftverrendszerek nem elszigetelt módon működnek, hanem a 2.8 ábrának megfelelően feladataik végrehajtása közben folyamatos párbeszédet folytatnak a környezetükkel. Kezelői felület Kommunikációs felület A környezet két részre bontható: Emberi környezet: A kezelő, akivel a rendszer egy alkalmasan kialakított kezelői felületen keresztül tartja a kapcsolatot. Környezet Gépi környezet 26: Külső számítógépes rendszerek, amelyekből beérkező információk vagy események a rendszerünk működését befo2.8 ábra: Szoftverrendszerek általános
lyásolják. Ezekkel a kommunikációs felületen architektúrája a 4GL eszközök szemszögéből keresztül tartja a rendszer a kapcsolatot. A szoftverrendszer harmadik komponense az alkalmazáslogika, mely a rendszer magjaként a feladatspecifikus műveletek végrehajtására szolgáló algoritmusokat foglalja magába. Egy régi, de minden bizonnyal ma is érvényes statisztikai megfigyelés azt mutatja, hogy egy átlagos programozó általa jól ismert programnyelven a feladat kitűzésétől a kész program átadásáig, beleszámítva az elemzés, tervezés, implementáció (kódolás, belövés) idejét, naponta átlagosan 20 sor programot készít el, ha alkalmazói rendszerről van szó, 10 sort, ha rendszerprogramról, és 2(!) sort, ha I/O tevékenységet kell programoznia. A számok természetesen függhetnek az alkalmazott nyelvtől, még inkább a programozó tudásától és tehetségétől, az arányok azonban már nem ennyire esetlegesek, és első hallásra
meghökkentőek. Az I/O tevékenységek aránytalanul nehezebb programozhatóságának fő oka egyébként kicsit tüzetesebb szemlélődés után nyilvánvalóvá válik: ezek a felületek eseményvezéreltek. Hogy ez általánosságban mit jelent, azt már tudjuk. A programfejlesztés szempontjából a dolog úgy fest, hogy a programozónak nem egyedi eseményeket, hanem eseménysorozatokat kell helyesen kezelnie, melyek elvileg végtelen sokfélék lehetnek, és kölcsönhatásaik teszőlegesen bonyolultak. Ezért a kezelői- és kommunikációs felületek tesztelése és belövése rendkívül nehéz feladat27. 26 Feltételezzük, hogy az alkalmazásunk egy másik számítógépes programmal áll kapcsolatban. Ez a megkötés nem azt jelenti, hogy a fizikai külvilágról nem szerezhetünk információkat, pusztán csak a problémák körét határolja be: nem vizsgáljuk a valós világgal kapcsolatban fölmerülő érzékelési, mérési és mérésadatgyűjtési problémák
hardver vonatkozásait. E helyett az érzékelő- és mérőrendszerek helyébe szoftvermodulokat képzelünk, melyek elfedik az említett problémákat. Az ilyen modulok pedig már nyilvánvalóan a számítógépes környezet részei. Megfigyelhető, hogy a játékprogramok általában stabilak, és ebből a szempontból közülük is kiemelkednek a célgépeken futó (pl. Nintendo) programok, melyek lényegében sohasem szállnak el Sokkal 27 Mindebből az következik, hogy a legnagyobb bonyodalmak általában nem az alkalmazáslogika megvalósítása körül bukkanak elő, hanem a kezelői felület és a kommunikációs felület létrehozásakor. Más szóval a magasszintű nyelvek segítségével megvalósíthatók ugyan a kezelői felülethez és a kommunikációs felülethez kapcsolódó funkciók, de lényegesen költségesebben, mint a program "belső részeinek", azaz az alkalmazáslogikának a programozása. Ennek a gondolatmenetnek logikus folytatásaként
adódik, hogy a költségek csökkentésének, a programfejlesztés sebességének és az elkészült program megbízhatóságának növelése érdekében olyan fejlesztői környezetre van szükség, amely a 2.8 ábrán látható séma minden elemének létrehozását erőteljesen támogatja megfelelő céleszközök segítségével. Ilyen fejlesztői környezetek a 4GL eszközök. A 4GL eszközök a kezelői felület létrehozására speciális szerkesztőket, ún. látványtervezőket (layout editor, dialog editor) alkalmaznak, melyek segítségével a kezelői felület elemei egyszerűen megrajzolhatók, elrendezhetők, és tulajdonságaik (pl. méret, szín) könnyedén beállíthatók A kommunikációs felület létrehozása kissé más jellegű feladat, ugyanis míg a kezelő tipikusan ember (és az emberek sok szempontból eléggé hasonlítanak egymásra), a rendszer környezete nagyon sokféle lehet. Ezért a 4GL eszközök vagy előre definiált környezetet tételeznek föl
(pl relációs adatbáziskezelő rendszerek, ipari mérésadatgyűjtő rendszerek, kórházi betegfelügyelű rendszerek), és az adott környezettel való kapcsolattartásra alkalmas elemeket eleve tartalmazzák, vagy 28 nyílt rendszerként működnek, és képesek különböző, szabványos illesztőfelületeken keresztül kommunikálni (pl. ODBC, JDBC, különböző hálózati protokollok) Akármelyik eset áll is fönn, a 4GL rendszerek a kommunikációs felület implementálására általában nem tartalmaznak külön eszközöket, legföljebb lehetővé teszik az előre definiált kommunikációs felület viselkedésének a megváltoztatását. Ez érthető is, hiszen a kommunikációs felület létrehozása többnyire a 4GL eszközzel fejlesztett alkalmazás által megvalósítandó funkcióknál sokkal alacsonyabb szintű gondolkodásmódot igényel. Az alkalmazáslogika implementálására magasszintű, gyakran objektumorientált programozási nyelv szolgál. Nagyon sokféle
nyelvre alapulnak 4GL környezetek Így például a Borland Delphi az Object Pascal-ra, a Visual Basic és az Oracle Power Objects a Basic-re 29, az Oracle Forms a PL/SQL-re, a Visual C++ a C++-ra, a Borland JBuilder a Java-ra, és még hosszan sorolhatnánk. komolyabb alkalmazások, mint pl. az MS Word ezzel szemben viszonylag gyakran omlanak össze Ezt egyébként már a rómaiak is megfigyelték, és ennek hatására keletkezett az a szállóige, hogy "A Word elszáll, az írás megmarad." Az ok nyilvánvalóan nem az, hogy a Nintendo annyival lelkiismeretesebb cég volna, mint a Microsoft, hanem egyszerűen az alkalmazások természetéből fakad. A játékprogramok ugyanis egy rendkívül hosszú, de lényegében oldalágak nélküli (mondhatnánk "keskeny") szekvenciát futtatnak, melynek során a változatok száma nem túl nagy. A kezelői felületük pedig, talán elsőre meglepő, de rendkívül egyszerű Kezelésükhöz elegendő néhány nyomógomb (a
Nintendo esetében ezek száma legföljebb 12). Amit bonyolultnak látunk , az a grafika, ezt azonban már az alkalmazáslogika generálja. A Word ezzel szemben több száz paranccsal, preferenciák tömkelegével, és bonyolult, operációs rendszer szintű kapcsolatokkal rendelkezik. Mindez a lehetséges parancsszekvenciák olyan változatos kavalkádjához vezet, melyeket figyelembe véve a Word (és a hozzá hasonló alkalmazások) stabilitását inkább csodálnunk kell. A vagy itt megengedő, azaz egy 4GL rendszer tartalmazhat előre definiált kommunikációs felületet, melyet kedvünk szerint le is cserélhetünk. 28 29 Elvileg a nyelv Visual Basic illetve Oracle Basic, de szerintünk a Basic csak Basic marad, akármilyen szép dobozba csomagolják is. Érdekes megfigyelni, hogy bár minden 4GL eszköz objektumorientált, előfordulhat, hogy az alapnyelvük nem az (pl. Oracle Forms 30) A 2.9 ábra a 4GL alkalmazások általános adatfeldolgozási sémáját mutatja. A
környezetből a kommunikációs felületen keresztül beérkező információkon az alkalmazáslogika belső objektumai megfelelő transzformációkat végeznek, majd a megjelenítő objektumokra juttatják azokat. A megjelenítő objektumokhoz tartozik egy kép, mely a képernyőn megjelenő lapokon helyezkedik el, másrészt belső viselkedéssel rendelkeznek, mely meghatározza, milyen módon jelenítik 2.9 ábra: A 4GL alkalmazások általános meg a hozzájuk érkező adatokat, és mit információfeldolgozási sémája kezdenek az esetleges felhasználói beavatkozásokkal (pl. egérkattintások, begépelt szöveges illetve numerikus adatelemek) A kezelő által végrehajtott módosítások, illetve az általa kiadott parancsok hatása a megjelenítő objektumoktól (illetve általában a kezelői felülettől) kiindulva visszafelé jut el a megfelelő belső objektumok közvetítésén keresztül a környezetig. A továbbiakban részletesen elemezzük a 4GL alkalmazások
említett elemeit. 2.521 Kezelői felület A kezelői felületen keresztül tudjuk egy működő szoftverrendszer viselkedését befolyásolni. Elméleti szinten a kezelői felületek kialakítása komoly szaktudást igényel, amely nem csak programozástechnikai, hanem ergonómiai, esztétikai és egyéb problémák kezelését is magába foglalja. A kezelői felületek a kezelőnek a rendszerről kialakított modelljét kell tükrözzék, melyen keresztül a kezelő a géppel kapcsolatba kerülhet. A kezelő fejében a világról számos modell él Ezek közül jelen vizsgálódásaink szempontjából hármat érdemes kiemelni: Rendszermodell : A kezelő modellje az általa használt rendszer funkcióiról és belső működéséről, melynek alapján képes a rendszer számára absztrakt (azaz bizonyos értelemben véve a világtól elvonatkoztatott) parancsok megfogalmazására. Ilyenkor a kezelő a lehető legnagyobb mértékben alkalmazkodik a rendszer képességeihez annak
érdekében, hogy a kezelői felület a legegyszerűbb, és ezáltal végrehajtási sebesség és megvalósíthatóság szempontjából a leghatékonyabb legyen. Ezt a fajta kommunikációt formális párbeszéd néven is szokás emlegetni. Ebbe a csoportba tartoznak a parancsnyelvek, a menürendszerek és a forma (űrlap) alapú karakteres kezelői felületek. Vizuális modell: A kezelői felület ez esetben olyan eszközöket nyújt, amelyek megjelenésüket és használatuk módját tekintve alkalmazkodnak a kezelő mindennapi életében megszokott tárgyakhoz és eljárásokhoz. Ebbe a csoportba tartoznak a grafikus kezelői felületek, melyek előszeretettel alkalmaznak olyan módszereket, mint a fogd-és-vidd (drag and drop), és képesek elvileg bármilyen kijelző és adatbeviteli eszköz olyan módon történő megvalósítására, hogy az 30 Elvileg csúnya megoldás, hogy a Forms a nem objektumorientált PL/SQL nyelvet használja, a praktikusság azonban olykor fontosabb
szempont a szépségnél. A PL/SQL az Oracle adatbáziskezelők procedurális nyelve, melynek segítségével tárolt eljárások és triggerek írhatók. Így Oracle kliens oldali alkalmazásokat PL/SQL-ben fejleszteni számtalan olyan elõnnyel jár, melyek feledtetik a nyelv nem objektumorientált voltát. eszköz egy valós világbeli tárgyra emlékeztessen megjelenését és használatát tekintve egyaránt31. Ez a fajta megközelítés jól alkalmazkodik a kezelő intuitív világlátásához, ezért a grafikus kezelői felületek megtanulása általában egyszerűbb, mint a formális párbeszéden alapulóké, viszont a grafikus felületek megvalósítása jóval bonyolultabb, végrehajtásuk pedig sokkal erőforrásigényesebb, ezért használatuk csak a kilencvenes ávek közepére tudott tömegessé válni. Szemantikus modell: A rendszerrel a kezelő természetes nyelven kommunikál, így elvileg bármit kifejezhet, amire gondol. A természetes nyelvű parancsok a
rendszer deklaratív kezelését teszik lehetővé, azaz összetett utasításokat fogalmazhatunk meg tömör, egyszerű módon, ráadásul a rendszer a kezelő által a mindennapi életében használt nyelvet közvetlenül megérti. Itt azonban számos, jelenleg még megoldatlan probléma vetődik föl. A természetes nyelvek elemzése, mint önálló feladat sem teljesen megoldott (már csak azért sem, mert maga a nyelvészet is nyitott tudomány, tehát vannak a természetes nyelvekben nem teljesen tisztázott jelenségek), ráadásul esetünkben az elemzés csak kiindulópontja a megoldáskeresés folyamatának 32. A jövő minden bizonnyal ez, de a megoldás rendkívül bonyolult, így még várat magára. A jelenlegi 4GL eszközök a lehetséges kezelői felület modellek közül érthető módon csak az első kettőt támogatják. A kezelői felület bemeneti és kimeneti hardver eszközei hosszú fejlődés után - néhány egzotikus alkalmazástól eltekintve - napjainkra
egységessé váltak: bemeneti eszközként klaviatúra és egér szolgál, kimeneti eszközként pedig képernyő (esetleg nyomtató is). A klaviatúráról parancsokat és adatokat gépelhetünk be, az egérrel pedig a képernyő elemei közül választhatunk, és rajtuk különböző műveleteket végezhetünk (pl. kurzor pozíciójának megváltoztatása, szövegrészek kiválasztása, nyomógombok aktivizálása, másolás). Az alkalmazás pillanatnyi állapotát a képernyőn követhetjük nyomon. A kezelői felület megvalósításakor alkalmazható elemek és eljárások szempontjából lényeges a képernyőnk felbontása, mely kétfajta lehet: Karakteres fejlesztőeszközök esetén a képernyőt n×m karakterre bontják, ahol n az egy sorban elhelyezhető karakterek maximális száma, m pedig a képernyőn megjeleníthető sorok maximális száma (tipikus felbontás a 80×25). A karakteres megjelenítés régi, jól bevált módszer, és manapság is rengeteg ilyen módon
működő alkalmazás létezik. Ezt használja minden terminál, és számos operációs rendszer, így például a DOS és a UNIX 33. Grafikus fejlesztőeszközök esetén a képernyőt képpontokra (pixel) bontják. A képpontok száma mindenképpen több százezer, de egészen hétköznapi számítógép konfigurációkon is könnyen meghaladhatja a milliót. Mindez a megjelenítést rendkívül rugalmassá teszi Míg a karakteres fejlesztőeszközök csak a formális párbeszéden alapuló kezelői felületek létrehozását támogatják, addig a grafikus fejlesztőeszközök emellett a grafikus kezelői felületek megvalósítását is lehetővé teszik. A csak karakteres felületet használó 4GL eszközök már elavultnak tekinthetők, és bár léteznek olyanok is (pl. Oracle Forms), melyek mindkét képernyőkezelési módszert lehetővé teszik, ezekről is elmondható, hogy az alkalmazásfejlesztés grafikus felületen keresztül történik. Így a továbbiakban a karakteres
felületek kezelését a grafikus felületek részhalmazának tekintjük, és nem foglalkozunk külön a karakteres felületekkel. A teljesség kedvéért még meg kell említenünk, hogy ez a megközelítésmódunk igaz az alkalmazásfejlesztés fázisára, nem igaz azonban az alkalmazás végrehajtására, ahol az eltérő hardver eszközökből kifolyólag az alkalmazás futtatható kódjának Közismert egyszerű példa ilyen eszközökre a virtuális CD-lejátszó, ahol egy program szimulálja egy valódi CD-lejátszó gombjait és kijelzőit, melyek segítségével az egér és a billentyűzet használatával mindent elérhetünk a számítógépünkbe épített CD-lejátszóval, amit egy különálló audio CD-lejátszóval megtehetnénk. 31 32 A probléma némileg hasonlatos az SQL utasítások végrehajtásához azzal a különbséggel, hogy az elemi utasítások száma általánosságban rendkívül nagy lehet. 33 A UNIX természetesen csak akkor, ha nem használunk
felette valamilyen ablakozó felületet (pl. Motif), vagy távoli gépről, karakteres terminálmódban használjuk. természetesen eltérőnek kell lennie a két esetben. Ez azonban csak a kódgenerálást módosítja, ami jelenlegi vizsgálódásainkat nem érinti. A kezelői felület megjelenítő objektumokból és parancsokból áll. A kezelő a megjelenítő objektumokon keresztül kaphat információkat a rendszertől, és rajtuk keresztül be is avatkozhat a rendszer működésébe. A parancsok parancsbillentyű kombinációk (pl Ctrl+C) vagy menük A szöveges parancsfelület létrehozását a 4GL eszközök közvetlenül nem támogatják, az alapnyelvük segítségével azonban ez is megvalósítható. Először a megjelenítő objektumok kezelésével foglalkozunk. Első lépésként a megjelenítő objektumokat erre a célra kialakított speciális rajzolóprogramokkal, ún. látványtervezőkkel (dialog editor, layout editor) felrajzoljuk alkalmas felületekre,
melyeket a továbbiakban lapoknak nevezünk. A látványtervező egyszerű módon biztosítja a megjelenítő objektumok mozgatását, átméretezését, másolását, törlését, és ezen kívül még számos hasznos művelet elvégzését. A lap előre meghatározott méretű terület, melyet a látványtervező segítségével létrehozunk, és ez tartalmazza a kezelői felület alapelemeit. Egy alkalmazáson belül a kezelői felület elvileg tetszőleges számú lapot tartalmazhat. Grafikus környezetben a lapok általában egy-egy operációs rendszer szintű ablakban jelennek meg, karakteres környezetben azonban, ahol az operációs rendszer nem rendelkezik ablakozó felülettel, maguk a lapok viselkednek ablakokként. Ilyenkor az ablakozó felületet a 4GL eszköz futtató rendszere biztosíthatja. A 4GL eszközök a kezelői felületnek a lapokon megjelenő objektumait osztálykönyvtárakban tárolják, melyek a fejlesztő számára eszköztárak (toolbox, toolbar)
formájában jelennek meg a 4GL eszköz felületén. Az eszköztár általában ikonos nyomógombok csoportja, melyen belül minden nyomógomb egy-egy adott megjelenítő objektumféleség létrehozására szolgál. Maga a létrehozás nem igényel többet, mint az egér segítségével az eszköztárból a megfelelő nyomógomb kiválasztása, majd az új elem (objektum) helyének kijelölése a megfelelő lapon. Az objektumok - ahogy ezt tőlük el is várjuk - tulajdonságokkal és módszerekkel rendelkeznek. Minden létrehozott megjelenítő elemhez tartozik egy adatlap (property sheet), melynek segítségével az objektum tulajdonságai beállíthatók, ezáltal pedig az egyes objektumok (ezen keresztül az alkalmazás) viselkedése tág határok között befolyásolható. Az adatlapok formája a jelenleg forgalomban lévő 4GL eszközökben meglehetősen egységes. Az adatlap külön ablakban jelenik meg, melynek fő komponense az adott objektum tulajdonságaira vonatkozó,
(tulajdonság, érték) párokból álló lista. A tulajdonságok értéke - többnyire közvetlenül, átírásssal - magán az adatlapon belül megváltoztatható. A megjelenítő objektumokat az alkalmazás információfeldolgozási sémájához való kapcsolatuk alapján két csoportba sorolhatjuk: Passzív objektumok: Tartalmuk, megjelenésük, és általában állapotuk is állandó. Kezelői eseményekre nem reagálnak, és adatokat nem jelenítenek meg. A passzív objektumok a kezelői felület olyan nélkülözhetetlen részei, melyek az egyes lapok áttekinthetőségét szolgálják, vagy emelik az alkalmazás fényét. Ebbe a csoportba tartoznak az egyszerű geometriai alakzatok (egyenes, kör, ellipszis, négyszög, sokszög, szabadkézi vonal), a feliratok és a statikus ábrák (pl. cégjelzés, háttér képek). Aktív objektumok: Az alkalmazás információfeldolgozási folyamatának szerves részét képezik. Tartalmuk időben változó, és az általuk megjelenített
adatokat az alkalmazáslogika képzi, vagy közvetlenül a környezetből származnak. Ha egy aktív objektum e mellett kezelői beavatkozásokra is reagál, kezelőszervnek (control) nevezik. Szabványos aktív kezelőszervek A következőkben összefoglaljuk a 4GL eszközökben szokásos aktív objektumok alapvető osztályait, és bemutatjuk a legelterjedtebben használt MS Windows 34 ablakozó rendszerben szokásos megjelenésüket. Nyomógombok (button) A nyomógomb olyan kezelőszerv, amely meghatározott műveletet vált ki, ha a felhasználó rákattint. Ilyen a gyakran előforduló OK nyomógomb, amely lezár egy párbeszédablakot, amikor a nyomógombra rákattintunk. Ha valaki nem látott volna még ilyet, így néz ki: Kapcsolók (checkbox) A kapcsoló kétállapotú kezelőszerv: kikapcsolt vagy bekapcsolt állapotban lehet. A kapcsoló egyes állapotaihoz értékek rendelhetők, melyek a kapcsolón keresztül beállíthatók, és a kapcsoló az adott értéket
megjelenítéskor az adott állapotával jelzi. Választógombok (radio button) A választógombok egymást kölcsönösen kizáró választási lehetőségek halmazát jelenítik meg. A választógombok általában egy választógomb kerethez tartoznak, amely csoportokba fogja őket, és tárolja az aktuálisan kiválasztott lehetőségnek megfelelő értéket. Például egy dolgozói információs rendszerben szereplő forma tartalmazhat egy választó keretet két választógombbal, melyekkel megadhatjuk, hogy az illető dolgozó férfi vagy nő. A mellékelt ábra egy választógomb csoportra mutat példát: Szövegmezők A szövegmező olyan kezelőszerv, amely adatok megjelenítésére, közvetlen bevitelére és módosítására alkalmas. A szövegmező a környezetből származó, számított vagy a felhasználó által begépelt adatokat tartalmazhat. Minden olyan adattípus megjelenítésére alkalmas, mely szöveges formában ábrázolható, így karaktersorozatok, számok
és dátumok. A mellékelt ábra egy szövegmezőt mutat: Listák A listák adatok megjelenítésére képes mezők. A listákhoz adott értékek sorozata kapcsolódik, melyek közül választást tesznek lehetővé. Alapvetően három fajtájuk szokásos: az állandó listák (T list), beugrólisták (popup list) és a vegyes listák (combo box). A listákhoz tartozó értéksorozatokat a listaobjektumok tulajdonságaként statikus módon, vagy programozottan lehet megadni. Az utóbbi esetben a felkínált értékek mindig a környezet aktuális állapotát tükrözik, más szóval a lista dinamikus. A választó listáknak és a beugró listáknak két értékük van: Belső: A listamező értéke. Az alkalmazáslogika belső objektumai ezt az értéket látják , amikor a listamezővel műveleteket végeznek. Kijelzett: A listamezőben megjelenő érték. Az egyes listamezők különleges tulajdonságai a következők: Állandó listák (T-list) Más ablakozó rendszerekben (pl.
Motif, Apple) az elemek megjelenése kicsit (de nem alapvetően) eltérő lehet, de a funkciójuk és viselkedésük megegyezik. 34 Az állandó lista segítségével a felhasználó adott értékek egy görgethető listájából választhat, de ezeken kívül más értékeket nem adhat meg. Az állandó lista folyamatosan mutat a felkínált elemek közül néhányat. Gyakori alkalmazása, mikor a felhasználónak meg akarjuk mutatni a választási lehetőségeket, miközben egy másik vezérlővel dolgozik. A mellékelt ábra egy állandó listát mutat: Beugrólisták (popup list) A beugrólista által felkínált értékek csak akkor jelennek meg, mikor a felhasználó a beugrólista vezérlőre kattint. Miután a lista megjelent, a felhasználó a vezérlőhöz tartozó görgetősávval nézheti végig a felkínált elemeket. A beugrólista az állandó listához hasonlóan a megadott elemekre korlátozza a felhasználó választási szabadságát. Állandó lista helyett
gyakran beugrólistákat alkalmazunk, ha az adott lapon kevés a hely. A mellékelt ábra beugrólistát mutat (az értéklista éppen lezárt állapotban van). Vegyes listák (combo box) A vegyes lista a beugrólista alternatívája, mely a listán felkínált értékek választásán kívül megengedi azt is, hogy a felhasználó a listamezőbe adatokat gépeljen. A vegyes lista olyankor használatos, mikor a felhasználónak javasolni szeretnénk néhány lehetséges értéket, de biztosítani akarjuk a felsorolásban nem szereplő értékek bevitelének lehetőségét is. A vegyes listák csak belső értékkel rendelkeznek, amely megegyzik a kijelzett értékkel, ugyanis a felhasználó által begépelt értékekek esetén a kijelzett érték nem különíthető el a belső értéktől. Megjelenésében csak abban különbözik a beugrólistától, hogy a lista megjelenítésére szolgáló nyilacska kissé külön válik a listamezőtől. A mellékelt ábra vegyes listát mutat
Görgetősávok A görgetősáv segítségével beállíthatjuk más alkalmazásobjektumok értékét. Vízszintes és függőleges görgetősávokat is használhatunk. A mellékelt ábrán vízszintes görgetősáv látható: Egyedi kezelőszervek Bizonyos célrendszerek kialakítására szolgáló 4GL eszközök a fenti kezelőszerveken kívül számos egyéb megjelenítő objektumot és készen kínálhatnak. Ez különösen az ipari felügyelő rendszerek kialakítására szolgáló 4GL eszközökre jellemző, ahol a legváltozatosabb technológiai folyamatok elemeit kell megfelelő módon kezelni. Ilyen környezetben szabványosnak mondható a toló- és tekerő potenciométerek, a különböző analóg, mutatós kijelzők, és számos egyéb elem (pl. tartályok, hőcserélők, reaktorok) megjelenítése. Mindemellett az általános célú 4GL környezetek is adnak lehetőséget egyedi kezelőszervek megvalósítására. Az alkalmazott módszerek szabványos objektumorientált
felületeken alapulnak, melyek objektum protokollokat definiálnak, vagyis bizonyos mértékben megszabják az objektumok közötti üzenetváltások módját, és az objektumok adott körülmények közötti viselkedését. Az egyedi kezelőszervek megvalósítása ezek után szabványos felületű objektumosztályokon keresztül lehetséges, melyeket megírunk, megfelelő módon lefordítva külső állományokban tárolunk, és a 4GL alkalmazás ezek alapján a szabványos felületen keresztül képes lesz majd egyedi kezelőszerv példányokat generálni és használni. Ilyen szabványos objektumorientált felületek a VBX (Visual Basic Custom Control), az OCX (OLE Custom Control) és az ActiveX. Mindez nem áll túl távol az olyan nagyszabású nyílt objektumorientált szabványok világától, mint amilyenek a CORBA, a COM és a DCOM, azonban ezek már kívül esnek jelenlegi témakörünkön. Beviteli eszközök kezelése A felhasználó a rendszer működésébe a két
szabványos beviteli eszköz, a klaviatúra és az egér segítségével avatkozhat be. A beavatkozás meghatározott kezelőszervekhez kapcsolódó eseményeket vált ki, melyeket aztán az alkalmazáslogika kezel. Ebben a pontban azokat a fogalmakat és eljárásokat ismertetjük, melyeket a 4GL eszközök a klaviatúra és egér kezelésére általában beépített módon, készen kínálnak. Mivel klaviatúrából és egérből csak egy van, kezelőszervből viszont tetszőlegesen sok lehet, az első probléma annak meghatározása, hogy az egér- vagy klaviatúraesemény a kezelői felület melyik objektumára vonatkozik. Ezt a problémát a 4GL eszközök (és általában minden grafikus kezelői felülettel rendelkező szoftverrendszer) a fókusz (focus) és az egérmutató (mouse pointer) fogalmának bevezetésével és használatával oldják meg: A fókusz határozza meg, hogy (a parancsbillentyűket kivéve) a klaviatúra billentyűinek leütéséből származó események a
kezelői felület melyik objektumára vonatkozzanak. A fókusz az alkalmazás használata során a kezelő igényeinek megfelelően vándorol objektumról objektumra. Mivel az egész rendszerben csak egy fókusz létezik, a fókuszt mindig egy és csak egy elem birtokolja, melynek megjelenését a fókusz kis mértékben módosítja. A fókuszt szövegmezőkben és vegyes listákban egy kurzor (általában villogó, függőleges vonal) mutatja, míg gombokon, választógombokon vagy kapcsolókon vékony, szaggatott keret jelzi. Az egérmutató határozza meg, hogy az egér gombjainak megnyomásából származó események a kezelői felület melyik objektumára vonatkozzanak. A kezelői felület lényeges eleme a fókusz és az egérmutató mozgatása. Az egérmutató az egér mozgatásával szinkronban változtatja a helyét a képernyőn. A fókusz mozgása ennél jóval bonyolultabb mechanizmus szerint történik, melyet navigáció néven szokás emlegetni. A továbbiakban a
navigáció kérdésével részletesebben foglakozunk. A navigáció eszköze lehet az egér vagy a klaviatúra is. A navigáció legtermészetesebb módon egérrel végezhető. Ehhez az egérmutatót a kívánt elem fölé visszük, majd az egyik (többnyire a bal) egérgombra kattintunk, melynek hatására (szerencsés esetben) a fókusz a régi helyéről az új elemre vándorol. Ahhoz, hogy a klaviatúráról is tudjunk navigálni, külön e célra szolgáló, speciális parancsbillentyűket vagy parancsbillentyű-kombinációkat kell definiálnunk. Ily módon a klaviatúránkon lesznek majd olyan billentyűk, melyek a fókuszt birtokló elem alapvető funkcióinak kiváltására valók (pl. nyomógomb megnyomása általában az Enter billentyűvel, listaelem kiválasztása általában a le-föl nyilakkal, szövegmező tartalmának változtatása pedig az alfanumerikus karakterekkel, valamint a törlés és jobbra-balra nyilakkal lehetséges). E mellett lesznek olyan billentyűink,
amelyek a fókusz változtatására szolgálnak (pl. általában ilyenek a Tab, a Shift+Tab és a Ctrl+Tab billentyűk, melyek hatását később még részletezzük). Azon kívül, hogy a klaviatúráról való navigálás látható módon jóval barokkosabb, van még egy alapvető különbség az egérrel való navigáláshoz képest, ez pedig a navigálási sorrend, mely az egér esetében kötetlen (olyan sorrendben kattintunk egyik elemről a másikra, ahogyan kedvünk tartja), a klaviatúra esetén viszont kötött. A Windows környezetben szabványosnak mondható Tab billentyű például olyan sorrendben adja tovább a fókuszt a kezelői felület egyes objektumai között, amilyen sorrendet az alkalmazás létrehozásakor definiáltunk. A Shift+Tab a navigációs sort visszafelé járja be. A navigáció (és természetesen már az alkalmazás tervezése) során figyelembe vehetjük a kezelői felület esetleges hierarchikus struktúráját is, így például megfelelő
parancsbillentyűkkel lapok között is válthatunk fókuszt (ami a következő lap első elemére való navigálást jelent). Aki ezek után úgy érzi, hogy a klaviatúráról való navigálás nehézkes, és fölösleges, annak fölhívjuk a figyelmét jelen fejezetünk 2. lábjegyzetére, ahol azt fejtegetjük, miért stabilabb egy Nintendo program, mint az MS Word. Megállapítható, hogy az egérrel való navigálás a kezelő számára sok esetben talán rugalmasabb, de az alkalmazás belövését - így végső soron megbízhatóságának növelését - a kötött eseménysorrendű beavatkozások - vagyis a klaviatúráról történő navigálás - egyszerűsíti. Íme egy újabb dilemma. Egérrel való manipulálás során bizonyos esetekben (például gombokra való kattintáskor) nem kívánatos, hogy a fókusz megváltozzon. Például ha egy szövegszerkesztőt használunk, és egy kijelölt szövegrészt mondjuk vastag betűssé szeretnénk tenni, és a Bold gombra
kattintás hatására a fókusz a nyomógombra vándorolna, ez a kiválasztás megszűnését eredményezhetné, ami lehetetlenné tenné a művelet elvégzését. Ezt a problémát a 4GL eszközök megfelelő objektumtulajdonságok beállításán keresztül teszik kezelhetővé. 2.522 Kommunikációs felület A kommunikációs felületen keresztül tartja a rendszerünk a kapcsolatot a gépi környezettel. A kommunikációs felület tehát egyfajta adatcserét és szinkronizációt kell biztosítson külső alkalmazásokkal. A külső alkalmazás mindig olyan komplex, gyakran általános részfeladatot old meg, melynek megvalósítása nem a mi alkalmazásunk feladata. Ebből a szempontból - a teljesség igénye nélkül - a következő öt fontos alapesetet különböztetjük meg: 1. Operációs rendszer szintű szolgáltatásokhoz akarunk hozzáférni, mint pl állományok kezelése, nyomtatás, hálózati kommunikáció. 2. Egy (vagy több) adatbázishoz szeretnénk
hozzáférni, és a bennük tárolt adatokat lekérdezni, módosítani, és az adatbázisokba újabb sorokat bevinni. 3. Létező, komplex alkalmazások (pl hálózati böngészők, szövegszerkesztők, táblázatkezelők) szolgáltatásainak igénybevételével nézegetnénk, szerkesztenénk és tárolnánk dokumentumokat. 4. Méréseket szeretnénk elvégezni, a mérések erdeményeit megjeleníteni, feldolgozni, és tárolni 5. Komplex ipari rendszert akarnánk az alkalmazásunkon keresztül felügyelni és/vagy irányítani Bár komplexitásukat és szemléletmódjukat tekintve ezek az esetek rendkívül eltérőek, azért mégis van egy közös vonásuk: nem jöhet szóba, hogy a külső rendszer funkcióit az alkalmazásunkon belül, mi magunk valósítsuk meg. Egyrészt nyilvánvalóan nem állhatunk neki olyan - esetenként többszáz emberévnyi munkát igénylő - szoftverrendszerek megvalósításának, mint amilyenek az operációs rendszerek, adatbáziskezelők,
szövegszerkesztők vagy Web böngészők, nem beszélve a komplex mérőrendszerek és ipari technológiák kialakításáról, ahol már csak a dolog hardver vonzatai és a kialakításukhoz szükséges rendkívül összetett speciális tudás hiánya miatt sem vehetjük föl a kesztyűt. Másrészt a működő, kipróbált rendszerek reprodukciója kifejezetten káros. A fejlesztők számára azért, mert feleslegesen dolgoznak 35, a felhasználók számára pedig azért, mert a megszokott, szabványos felületük (pl.Word) helyett biztos, hogy kicsit (vagy éppenséggel nagyon) mást kapnak, amitől kényelmetlenül érzik magukat, és bennünket szidnak majd. A mi alkalmazásunk (a továbbiakban M) szempontjából a külső alkalmazások (a továbbiakban K) kétfélék lehetnek: Passszív: M kéréseket küld K-nak, amelyeket K kiszolgál, és a kérés eredményét M rendelkezésére bocsájtja. K sohasem fordul kéréssel M-hez, tehát M szempontjából a kapcsolat nem
eseményvezérelt, vagyis M nem képes a K-tól érkező eseményekre reagálni. Aktív: M képes a K-tól érkező eseményekre reagálni, azaz a kapcsolat M szempontjából eseményvezérelt. Az aktív kapcsolat általában magában foglalja M-nek azt a képességét is, hogy kéréseket küldjön K-nak. Lényeges a kommunikációs csatorna, mely lehet hálózati kommunikációs felület, vagy egygépes objektumorientált felület (OLE2, COM). A kettő kombinációjából alakultak ki az elosztott Jó, ha szoftverfejlesztőként megszívleljük Ettore Bugatti jelmondatát: Amit másvalaki már megcsinált, az bennünket nem érdekel. Azaz, amit már másvalaki kitalált, azt használjuk föl, és csak új dolgokon törjük a fejünket. 35 objektumrendszerek (pl. CORBA, DCOM), ezek tárgyalása azonban számunkra most messzire vezetne. A következőkben röviden összefoglaljuk a felsorolt öt alapesetet támogató kommunikációs felületek főbb jellegzetességeit. 1. Az
operációs rendszerek szolgáltatásait rendszerhívásokon keresztül lehet igénybe venni, így a 4GL eszköz feladata, hogy az adott rendszerhívások végrehajtását az alkalmazáslogika programkódjából lehetővé tegye. Erre általában beépített hívások formájában van lehetőségünk Objektumorientált környezetben ez előredefiniált osztályok segítségével oldható meg, melyekből olyan objektumok generálhatók, amelyeknek a módszerei lehetővé teszik a megfelelő rendszerhívások végrehajtását. 2. Az adatbáziskezelőkhöz fejlesztett adatmegjelenítő, adatbeviteli/adatmódosító alkalmazások fejlesztése a 4GL alkalmazásfejlesztő eszközök egyik legelterjedtebb felhasználási területe. A kommunikációs felületnek ilyen esetekben a következőket kell lehetővé tennie: - Az adatbázis kapcsolat felvétele és lebontása: Első lépésként mindig ki kell jelölnünk azt az adatbázist, amelyikkel kommunikálni akarunk. Ezzel az adatbázissal ki
kell építeni egy (többnyire hálózati) kapcsolatot Az adatokkal való műveletek elvégzése után a kapcsolatot le kell bontani. Minderre a megfelelő 4GL eszközök közvetlen támogatást nyújtanak, gyakran a grafikus tervezőfelületen is megjelenő objektumok formájában. - SQL kérések összeállítása és elküldése: A kezelői felületen megjelenő kezelőszerveken keresztül a felhasználó megváltoztathatja különböző adatok értékeit, új sorokat vihet be, vagy lekérdezési feltételeket fogalmazhat meg. A kezelőszervek állapota alapján a 4GL eszköz adott parancsra automatikusan összeállítja a megfelelő (SELECT, INSERT, UPDATE vagy DELETE) utasítás szövegét, és az utasítást végrehajtatja az adatbáziskezelővel. - Eredmények visszaolvasása: A 4GL eszköz automatikusan beolvassa a SELECT utasítás végrehajtásakor eredményül kapott sorokat, és az esetleges hibaüzeneteket. Mindezt automatikusan megjeleníti a kezelői felületen a
felhasználó számára. - A kezelői felületen keresztül megváltoztatott vagy bevitt adatok visszaírása: Ha a kezelő egy adatelemet megváltoztat, töröl, vagy éppen új sort visz be, a módosításokat előbb utóbb be kell vinni az adatbázisba is. Mindez DML utasítások automatikus megfogalmazását és végrehajtatását teszi szükségessé. Mivel a DML utasításokból már tranzakciók keletkeznek, a 4GL eszköz feladata a tranzakciók lezárása, esetleges visszagörgetése, mentési pontok elhelyezése, egyszóval maga a tranzakciókezelés is. Léteznek adatbázisokhoz szabványos kommunikációs felületek is. Ilyenek pl az ODBC (Open Data Base Connectivity) és az ODBC alapon kifejlesztett JDBC (Java Data Base Connectivity). 3. Létező alkalmazások szolgáltatásainak igénybe vételére több objektumorientált szabvány is használatos, mint pl. OLE2, VBX, OCX, COM, CORBA, DCOM A 4GL alkalmazások lehetővé teszik olyan (Windows környezetben jelenleg
tipikusan OLE2 vagy OCX) objektumok létrehozását és kezelését, melyeken a felhasználó a megfelelő típusú külső alkalmazás (pl. Word, Excel) segítségével képes lesz a megfelelő műveletek (pl. szerkesztés, módosítás) végrehajtására 4-5. Az előző három esetben a külső alkalmazás tipikusan passzív Mérőrendszerek és ipari technológiák esetén a megfigyelt rendszer (mérendő objektum illetve maga az ipari folyamat) sajátosságaiból kifolyólag a külső alkalmazás legtöbbször aktív, ugyanis a méréseket általában a mérőeszközök (mérőműszer vagy az érzékelőkhöz kapcsolt ipari mérésadatgyűjtő és folyamatirányító számítógépek) adott periódusidővel, ciklikusan hajtják végre, és a mérési adatok tárolásáról, feldolgozásáról, kijelzéséről, esetleg a szükséges beavatkozások előkészítéséről az alkalmazásunknak folyamatosan gonoskodnia kell. Az ilyen környezetben működő eszközök mindig párhuzamos
működésűek, és bennük egy (vagy több) külön folyamat gondoskodik a kommunikációs felületen megjelenő mérési adatok fogadásáról, és a feldolgozó és megjelenítő folyamatok számára való továbbításáról. 2.523 Alkalmazáslogika Az alkalmazáslogika valósítja meg az alkalmazásunk procedurális belső működéseit. A végrehajtandó tevékenységeket két általános csoportba sorolhatjuk. Eseménykezelés Ezek a kezelői felületről és a kommunikációs felületről érkező események kiszolgálásával kapcsolatos tevékenységek. A felületen megjelenő előredefiniált elemek száma bármely 4GL környezetben adott, és így definiálható azon események köre, melyek ezekhez a felületi elemekhez kapcsolódnak. Alkalmazásspecifikus tevékenységek Olyan egyedi, általunk megírt kódrészletek ezek, melyek az alkalmazás igényeinknek megfelelő működését eredményezik. A kétfajta tevékenység a következő módon kapcsolódik egymáshoz.
A 4GL eszközök az általuk ismert események körét előre definiálják, és kezelésükre egy (remélhetőleg) jól dokumentált, előregyártott sémát, lényegében egy kész alkalmazásvázat kínálnak. Az események kezeléséhez az alkalmazások egyes objektumainak megfelelő módszerei kapcsolódnak, melyeket a 4GL eszköz futtatórendszere a megfelelő esemény bekövetkeztekor automatikusan végrehajt. Mivel, ahogy azt jól tudjuk, a módszerek osztályszintű kódrészletek, így a 4GL eszközökben az egyes objektumokhoz a módszereken keresztül számos előredefiniált tevékenység tartozik (pl. ellenőrzések, navigációs tevékenységek, egérkezelés, tranzakciókezelés). Ennek következményeként egyszerűbb, tipikus feladatokat megoldó alkalmazások akár egyetlen programsor megírása nélkül, pusztán a megfelelő objektumok létrehozásával, és a hozzájuk kapcsolódó beépített tevékenységek kihasználásával megvalósíthatók. Az
alkalmazásspecifikus tevékenységeket ezek után a megfelelő objektumok megfelelő módszereinek törzsében elhelyezett - gyakran mindössze néhány soros - kódrészletekkel valósítjuk meg. Ez a módszer rendkívül hatékony programfejlesztést tesz lehetővé, de a hagyományos programnyelvekhez képest eltérő, és sok programfejlesztő által kevéssé preferált szemléletmódot igényel. A hagyományos szemléletű fejlesztés során a programozó egy virtuális gépen futó, egybefüggő programkódot ír, így a történésekkel teljes egészében tisztában van. Ezzel szemben a 4GL eszközök esetében a saját kód elhelyezésére csak belépési pontokat kapunk, miközben a 4GL ütemező működése sok esetben homályba vész, és apró részleteiről csak a dokumentáció meglehetősen fáradságos és aprólékos tanulmányozása (lényegében folyamatábrák visszafejtése) során kaphatunk precíz információkat, holott az alkalmazás működése
szempontjából az egyes események feldolgozásának és a feldolgozó módszerek aktivizálásának sorrendje és feltételei alapvetően fontosak. Ezért a legkomolyabb problémát a 4GL fejlesztés során általában nem is annyira a megfelelő kódrészletek megírása, hanem a kódrészletek végrehajtatására alkalmas módszerek megkeresése jelenti. Az egyedi tevékenységek megvalósítása során a 4GL eszközök lehetővé teszik a programozó számára, hogy a rendszer beépített működéseit kiegészítse vagy egyszerűen felüldefiniálja. Az utóbbi esetben a 4GL eszköz által nyújtott alapviselkedés teljes egészében elfedhető, és igényeinkhez igazítható. Osztályok létrehozására 4GL eszközökben gyakran alkalmazott módszer az ún. delegálás, melynek lényege, hogy egy objektum mintájára hozunk létre egy osztályt, felhasználva az objektum tulajdonságait és a hozzá kapcsolódó módszereket. 3. Objektumorientált elemzés és tervezés
Ahogyan a bevezetőben elhangzott, a szoftverfejlesztési ciklus lépéseit (elemzés, tervezés, implementáció, üzemeltetés) a műveletek valódi végrehajtási sorrendjével ellentétes irányban járjuk végig. Mivel eddig áttekintettük azokat az alapvető eszközöket, amelyekkel manapság bonyolult szoftverrendszereket implementálnak, most elérkezett az idő, hogy az elemzés és tervezés módszereit is bemutassuk. A szoftverrendszerek létrehozása során alkalmazott elemzési és tervezési eljárásokkal kapcsolatban célszerű megkülönböztetni a módszer (method) és a módszertan (methodology) fogalmát. A módszer a szoftverfejlesztés során követendő, szabályozott (“katonás”) lépések sorozata, melyek segítségével a fejlesztés alatt álló szoftverrendszer különböző aspektusú modelljei írhatók le egy előre meghatározott jelrendszer segítségével. Más szóval minden módszer egy folyamatból (a végrehajtandó lépések és a
végrehajtás sorrendje) és egy jelrendszből áll, amely megszabja, milyen módon írhatók le a modellek. A folyamat arra való, hogy a fejlesztő csapongó fantáziáját meghatározott keretek közé szorítva biztosítsa a végtermék lehető legjobban ellenőrizhető és egységes minőségét, a jelrendszer alapvető célja pedig, hogy az egyik fejlesztő által létrehozott modellek tartalmát a többi fejlesztő is megértse, enélkül ugyanis a csapatmunka elképzelhetetlen. A módszertan módszerek olyan csoportja, amelyeket egy-egy adott rendezőelv alapján dolgoztak ki. A fenti terminológiai megkülönböztetés lényeges a további magyarázatok világos megértéséhez, meg kell azonban jegyeznünk, hogy a mindennapi szóhasználatban a két fogalom nem különül el ennyire élesen, így általában egyes módszereket szokás módszertanoknak is nevezni. A korábbi fejezetekben nagy hangsúlyt kaptak az objektumorientált eszközök. Ez nem csoda, hiszen ahogy a
világ a hetvenes-nyolcvanas években strukturált volt, manapság objektumorientált. Ez másként úgy is megfogalmazható, hogy az objektumorientált implementációs eszközök korszerűbbek, sok szempontból hatékonyabbak, és nagy rendszerek létrehozásakor biztonságosabbak, mint a strukturált programozás alapelvei szerint kialakított rendszerek. Föltételezzük, hogy az olvasó tisztában van a strukturált programozás alapelveivel, így ezekről eddig nem szóltunk. Tehettük ezt azért is, mert az objektumorientált nyelvek (így természetesen a Java is) alkalmas strukturált programok írására 36, más szóval a strukturált, magasszintű nyelvekben alkalmazott konstrukciók az objektumorientált nyelvek részhalmazai. Más a helyzet a szoftverfejlesztési módszerek esetén, amelyek túllépnek a puszta eszközökön, és éppen arra hivatottak, hogy megmutassák, milyen rendezőelvek mentén az adott eszközöket használni. Mivel a módszertanok egyik alapvető
célja a szoftverfejlesztési folyamatnak az implementációs eszközöktől való lehető legnagyobb mértékű függetlenítése, első pillantásra azt gondolhatnánk, hogy a módszerek szintjén már nem jelenthet komoly különbséget, hogy az implementáció objektumorientált vagy egyszerű strukturált nyelv falhasználásával készül el. A valóságban azonban a módszerek éppen a fejlesztés során követendő alapeveket, és ezeken keresztül az alkalmazott absztrakciós és dekompozíciós eljárásokat rögzítik, amelyek a strukturált- és objektumorientált módszertan esetében lényeges különségeket mutatnak. 36 Erre talán a legjobb példa a C++, melyben úgy lehet C programot írni, hogy az ember közben akár el is felejtheti, hogy valójában objektumorientált nyelvet használ. Persze egy pillanatig sem állítjuk, hogy ez helyes gyakorlat volna. 3.1 Dekompozíció, hierarchia, absztrakció Az emberi felfogóképesség jellegzetességei - mondhatnánk
korlátai miatt - komplex rendszerek tervezése nemigen képzelhető el másként, mint dekompozícióval. A dekompozíció közismert módon azt jelenti, hogy a megoldandó komplex problémát megfelelő számú kisebb részproblémára osztjuk föl, a részproblémákat megoldjuk, és az így kapott részmegoldásokból rakjuk össze a végleges megoldást. A dekompozíciót valós problémák megoldásakor több szinten végezzük, azaz a részproblémákat tovább osztjuk kisebb részproblémákra. A többszintű dekompozíciót hierarchikus dekompozíciónak is szokás nevezni. A hierarchikus dekompozíció egyes szintjei mindig valamilyen absztrakciót fejeznek ki, mellyel az adott szintű részproblémákat leírjuk, a lényeges vonásokat kiemeljük, az adott szinten elhanyagolható vonásokkal pedig nem foglalkozunk. A hierarchikus dekompozíció során alapvető kérdés, hogyan fogalmazzuk meg az egyes absztrakciós szinteket, illetve a dekompozíció során milyen alapon
különítjük el egymástól az egyes részproblémákat. A következőkben kétféle lehetséges absztrakciót vizsgálunk. 3.11 Algoritmikus dekompozíció Lényege, hogy a megoldandó feladatot olyan részműveletek sorozatára bontjuk, melyeket képzelt (virtuális) gépek képesek végrehajtani. A dekompozíciót egészen addig ismételjük az egyes részműveletekre, amíg el nem jutunk a fizikailag létező gépünk (a valóságban többnyire egy magasszintű programnyelv) utasításainak a szintjére. Ez a top-down (fölülről lefelé) tervezés elve A megoldás elvileg összerakható alulról fölfelé (bottom-up), az elemi utasításokból építkezve is. Jól látható, hogy az algoritmikus dekompozíció alapját olyan végrehajtandó tevékenységek képezik, amelyek segítségével elérhetjük az általunk kívánt célt. Az algoritmusok azonban különböző adatstruktúrákon hajtanak végre műveleteket, melyeket az algoritmikus dekompozíció során szintén
figyelembe kell vennünk. Ez úgy történik, hogy a komplex feladatot egyetlen tevékenységként fogjuk föl, mely meghatározott bemenő adatok alapján meghatározott kimenő adatokat állít elő, és ehhez az elképzeléshez ragaszkodunk a dekompozíció alacsonyabb szintjein is: a résztevékenységek meghatározott bemenő adatok alapján meghatározott kimenő adatokat állítanak elő, azonban ezek bemenetüket és kimenetüket tekintve egyaránt kapcsolódhatnak másik résztevékenységekhez is. Az algoritmikus dekompozíció alapvető implementációs eszközei az eljárások és függvények, melyek a bemenő és kimenő adatokat formális paramétereiken, és függvények esetében visszaadott értékükön keresztül képesek kezelni. 3.12 Objektumorientált dekompozíció Ennek alapja, hogy a világot önálló tevékenységre képes, egymással együttműködő egységek rendszereként fogjuk föl, melyek működése magasabb szintű viselkedést eredményez. Az
egyes objektumok a hozzájuk rendelt viselkedést megfelelő üzenetek hatására produkálják. Az objektumorientált dekompozíció egyik leglényegesebb vonása, hogy a tevékenységeket nem tekinti elsődlegesnek az adatokkal szemben, ezért az alkalmazások szélesebb skáláját képes hatékonyen kezelni. Elég itt azt a nyilvánvaló tényt megemlítenünk, hogy léteznek természetüktől fogva adatvezérelt alkalmazások (pl. adatbáziskezelők vagy szakértőrendszerek), melyek megvalósítása az algoritmikus dekompozíció elvét követve nagyon nehézkes. Az objektumok tehát olyan egységek, melyekben a megfelelő tevékenységek módszerekként jelennek meg, a tevékenységekhez kapcsolódó adatstruktúrák pedig az egyes objektumok tulajdonságaiként. Mindez egy meglehetősen másfajta gondolkodásmódot igényel, ugyanakkor az objektumorientált dekompozíció többnyire hatékonyabb, és jobban megfelel bonyolult rendszerek létrehozásakor. 3.2 Az észszerű
tervezési folyamat Ha fizikai folyamatokon alapuló technológiákat tekintünk, elmondható, hogy a kidolgozott technológia esetén a jóminőségű végtermék létrehozásának lépéseit a fizika törvényei határozzák meg, és sokkal kevésbé a terméket előállító munkás tehetsége. Más szóval, ha valaki szamurájkardot akar kovácsolni, jól teszi, ha betartja az évszázadok során kikísérletezett bonyolult és hosszadalmas műveletsort, egyébként számíthat rá, hogy munkájának eredménye tiszteletet parancsoló fegyver helyett csak óriás bugylibicska lesz, mely az első összecsapás alkalmával darabokra törik. A szoftvertechnológiák esetén azonban más a helyzet. Bárki kitalálhat magának új módszert, sőt mondhatja azt is, hogy ez az egész hókuszpókusz a módszertanokkal teljességgel fölösleges37. Mindez a szoftverfejlesztési folyamat túlzott rugalmasságából ered. A gyakorlat mégis azt mutatja, hogy a sikeres szoftverprojektek
hasonló elvek alapján jutnak el a célig, míg azok a projektek, amelyek figyelmen kívül hagyják ezeket az alapelveket, nagyon gyakran kudarcot vallanak. Így tehát megfogalmazhatók olyan kritériumok, melyek alapján egy szoftverfejlesztési módszertan ésszerűnek, ezáltal a gyakorlatban is használhatónak minősíthető. A módszernek olyan szoftverrendszert kell eredményeznie, melynek architektúrája megfelel a következő kritériumoknak: Az architektúra jól definiált absztrakciós szintekből építkezik, ahol egy-egy szint egységes, koherens rendezőelvek alapján keletkezik, és a szintek egymással rögzített, dokumentált felületeken keresztül tartják a kapcsolatot. A felületek megkerülésével vagy szintek kihagyásával a szintek között adatcsere nem történhet. - Az egyes szintek felülete és implementációja világosan elkülönül egymástól, így bármely szint implementációja megváltoztatható, feltéve, hogy a felület változatlan
marad. - Közös viselkedésformákat csak egy-egy absztrakció testesít meg, melyekre minden olyan absztrakció hivatkozik, amelynek az adott viselkedésformára szüksége van. Ezáltal az architektúra jelentősen egyszerűsödik. A módszer nem lehet anarchikus, mely teljesen szabad utat enged a fejlesztők csapongó fantáziájának, így ugyanis a minőség, a határidők, sőt magának a projektnek a puszta befejezése sem garantálható. A módszer nem lehet azonban drákói sem, amely a fejlesztőket merev, bürokratikus keretek közé szorítja, ahol minden lépés apró részletekig kőbe vésett, nem adva semmiféle teret az egyéni kezdeményezésnek és ötleteknek. Ez ugyanis lehetetlenné teszi a felhasználói igények rugalmas és precíz követését, ráadásul a fejlesztőknek is kedvét szegi, és könnyen bekövetkezhet a sasmadárindián effektus 38, amely a csapatot szétzilálja. - Az anarchikus és a drákói módszerek közötti legelapvetőbb
különbség, hogy az anarchikus módszer a szoftverfejlesztési folyamat során tetszőleges számú visszalépést megenged (más szóval az anarchikus folyamat tetszőleges mértékben iteratív jellegű), míg a drákói módszer a visszalépést teljesen kizárja (más szóval teljes egészében inkrementális jellegű). A kérdés tehát, hogy megengedjük-e már létrehozott részmegoldások vagy részrendszerek eldobását és újrakonstruálását, vagy ragaszkodunk a már megvalósított dolgainkhoz, és azokat csak újakkal engedjük bővíteni. Meglepő módon éppen a nagytudású, öreg programozók hajlamosak arra, hogy az ilyen fajta magasszintű segédeszközöket elvessék, hiszen minden feladat megoldható Fortranban, vagy ha mégsem, hát a megfelelő gép assembly nyelvén, esetleg közvetlenül gépi kódban. Minden csodálatunk az ilyen öreg guruké, de ne adja Isten, hogy egy csapatban kelljen velük dolgoznunk. Az öreg guruk ugyanis időnként
hosszabb-rövidebb időre (esetleg végleg) távoznak a csapatból, és ilyenkor az általuk gyártott kód ránk marad: tessék továbbfejleszteni, karbantartani, módosítani. Mindez hosszú hetekre gondoskodik az éjszakai rémálmainkról Volt részünk ilyen élményben, de az olvasóknak nem kívánunk hasonlót. 37 38 Ez csak egy bugyuta kis vicc, mely valahogy így szól: Miben hasonlít egymásra az indián és a sasmadár? Semmiben. A sasmadár ugyanis nagyon magasan és nagyon gyorsan repül, és lenézi az indiánt Az indián ezzel szemben a földön jár, lassan és megfontoltan, a sasmadár pedig baromira nem érdekli. Mivel egyik véglet sem tűnik célszerűnek, az igazán hatékony és rugalmas módszerek az iteratív és inkrementális szoftverfejlesztési ciklus kombinációját alkalmazzák. Ahogy látni fogjuk, az objektumorientált módszertan ilyen. Az iteratív és inkrementális életciklus túllép a bevezetőben vázolt vízesés életciklus modellen,
és megenged a fejlesztés során bizonyos visszalépéseket. Mivel bonyolult szoftverprojektekben a visszalépések amúgyis törvényszerűek, jobb, ha ezeket eleve beépítjük a módszerünkbe, mintha valamiféle eredendő bűnként kezeljük őket, melyet tűzzel vassal irtani kell. 3.3 A strukturált csoportosítása elemzési és tervezési módszerek Bár a következőkben részletesen csak az objektumorientált elemzés és tervezés módszereivel foglalkozunk, körüljárjuk a strukturált- és az objektumorientált megközelítés közötti alapvető különbségeket, és összefoglaljuk két, elterjedten használt strukturált tervezési módszer alapvető jellegzetességeit. Mivel minden szoftverrendszer két alappillére az algoritmusok és adatstruktúrák, ennek alapján elvileg a következő típusú módszertanok képzelhetők el: - Strukturált tervezés: a dekompozíció alapjául a tevékenységek (vagyis az algoritmusok) szolgálnak. - Adatvezérelt
tervezés: a dekompozíció alapjául a rendszer bemeneti és kimeneti adatai szolgálnak. Az adatvezérelt tervezés olyan adatorientált alkalmazások tervezésére alkalmazható, amelyekben az adatsrtuktúrák és az adatok egymáshoz való viszonya döntő fontosságú és bonyolult, míg a rajtuk végzett műveletek viszonylag egyszerűek és uniformizálhatók. Látszólag ilyenek az adatbáziskezelő alkalmazások, a valóságban azonban még ezek sem teszik lehetővé a tisztán adatvezérelt elvű tervezést. Ezért ez a módszertan inkább elvi jelentőségű - Objektumorientált tervezés: a dekompozíció alapjául a rendszer működésében részt vevő objektumok szolgálnak. Az objektumorientált tervezéssel ebben a fejezetben részletesebben foglalkozunk. A strukturált tervezési módszertan főbb tulajdonságait a következő két pontban két jellegzetes módszer rövid bemutatásával érzékeltetjük. Mivel az adatvezérelt tervezés jelentősége elvi, erre
konkrét példát nem mutatunk be. 3.31 Strukturált elemzés és tervezés (SA/SD) Az SA/SD módszer a szoftvermodellek formális leírására többféle jelrendszert alkalmaz. Az elemzés során adatfolyamhálókat, folyamat diagramokat, állapotgráfokat, egyed kapcsolat diagramokat és adatszótárt használ. A tervezés során az elemzés közben megalkotott modellekhez újabb részleteket adunk, és az adatfolyamhálókból struktúradiagrammok keletkeznek, melyek alapján a rendszer magasszintű programnyelvű forráskódja automatikusan legenerálható. Az adatfolyamhálók képezik az SA/SD módszer alapját. Ezekkel írhatók le a rendszeren keresztül áramló adatokon végzett transzformációk. Az adatfolyamhálók folyamatokból, aktorokból és tárolókból állnak. Az adatfolyamhálók jól támogatják az algoritmikus dekompozíciót A tervezési folyamat során a megoldandó feladatot adatfolyamhálók hierarchikus rendszerére dekomponáljuk, egészen addig, míg
az adatfolyamhálóban megjelenő folyamatok komplexitása el nem éri azt a szintet, ahol már közvetlenül képesek vagyunk implementálni azokat. Az adatszótár tartalmazza mindazon információkat, melyek alapján a tervek részletei összeállnak egyetlen egésszé. Az állapotgráfok a folyamatok idő- és eseményfüggő viselkedésének leírását teszik lehetővé. Az egyed-kapcsolat diagramok a tárolók közötti adatkapcsolatok leírására szolgálnak, melyeket egyébként csak a folyamatok leírásában tudnánk közvetett módon megadni. 3.32 A Jackson módszer (JSD) A Jackson 39 módszer nem tesz különbséget az elemzés és tervezés között, a szoftverfejlesztés folyamatát specifikációra és implementációra osztja. A Jackson módszer felfogása szerint a megvalósítandó rendszer egyedek (entity), tevékenységek (action) és a tevékenységek sorrendje alapján írható le. A megoldandó feladat informális specifikációjában az egyedek általában
főnevek, a tevékenységek általában igék formájában jelennek meg. A JSD folyamat a következő hat egymást követő lépésből áll: egyed-tevékenség lépés, egyed-struktúra lépés, kezdeti modell lépés, funkció lépés, rendszeridőzítés lépés és végül az implementációs lépés. Az egyed-tevékenység lépés során a szoftverfejlesztő a valós világ részét képéző egyedeket és tevékenységet sorol föl. Az egyedek és tevékenységek kiválasztását a megvalósítandó rendszer célja határozza meg. Az egyed-tevékenység lépés bemenete a követelmények leírása, a kimenete pedig egyedek és tevékenységek egy listája. Az egyed-struktúra lépés célja az egyedekhez kapcsolódó tevékenységek időbeli sorrendjének részleges meghatározása. Erre azért van szükség, mert a tevékenységek a valós világban mennek végbe, elemiek, nem dekomponálhatók, ráadásul részben kívül eshetnek a rendszerünk hatáskörén. A kezdeti modell
lépés során írjuk le, hogyan kapcsolódik a való világ az absztrakt modellünkhöz. A kapcsolat módja a JSD megközelítés szerint állapotvektor vagy adatfolyam lehet. Az állapotvektor kapcsolat esetén a rendszer bemenetén megjelenő adatok a rendszer belső állapotát befolyásolják, ahol nem minden adat jár szükségképpen állapotváltozással (pl. állapotjelző bitek) Az adatfolyam kapcsolat lényege, hogy a megjelenő adatok mindegyike valamilyen módon meg kell jelenjen a rendszer kimenetén (pl. nyomtatópuffer) A funkció lépés során a tevékenységek kimenetét írjuk le alkalmas pszeudokód segítségével. Ennek a lépésnek a végén a fejlesztő kezében van a kívánt rendszer teljes specifikációja. A rendszeridőzítés lépés célja a rendszer működése során megengedett időkorlátok föltérképezése, és informális megjegyzések formájában való rögzítése. Az implementációs lépés a folyamatok ütemezésére és az
erőforrásszétosztásra koncentrál, melynek során a megfelelő folyamatok végrehajtása adott processzorokhoz kerül. A felsorolt lépések végrehajtása után következik a kódolás és az adatbázisok megtervezése. 3.4 A folyamat: Objectory (Rational Unified Process) Az Objectory folyamatot jelenlegi formájában a Rational fejlesztette ki Grady Booch, Ivar Jacobson és James Rumbaugh vezetésével hármójuk korábban kidolgozott objektumorientált módszereinek tapasztalatai alapján. Az Objectory iteratív és inkrementális folyamat, azaz a probléma megoldását fokozatos finomítások sorozatán keresztül éri el. Az Objectory ellenőrzött folyamat, ez azonban csak a követelmények és változatok nagyon gondos kezelésével érhető el, melynek eredményeként a csapat minden tagja a fejlesztési folyamat minden pillanatában tisztában lehet a kitűzött célokkal, és az adott pillanatig elért eredményekkel. Az Objectory folyamat egyes tevékenységei
modelleket hoznak létre és tartanak karban. Írásos dokumentumok készítése helyett az Objectory a modellek - a szoftverrendszer részletes szemantikus leírásainak - kezelését tartja elsődlegesen fontosnak. 39 A módszer kidolgozójának neve is Michael Jackson. Lehet, hogy nem egészen mindegy, hogy az embert hogy hívják? Az Objectory fejlesztési tevékenységei használati esetekre (use case) alapulnak. A használati esetek és forgatókönyvek fogalma vezérli a folyamatot a követelmények megfogalmazásától a tesztelésig, egységes és nyomon követhető szálakat eredményezve a fejlesztés során és a kész rendszerekben egyaránt. Az Objectory objektumorientált folyamat, így a benne megalkotott modellek nagy része objektumorientált modell, mely objektumokra, osztályokra és a köztük lévő kapcsolatokra épül. A modellek leírására az Objectory jelrendszerként az UML (Unified Modelling Language) modellező nyelvet használja. Az Objectory
lehetővé teszi a komponens alapú szoftverfejlesztést. A komponensek összetett modulok vagy alrendszerek, melyek jól definiált funkcióval és architektúrával rendelkeznek, és olyan szabványos felületeket használnak, mint a CORBA vagy a COM/DCOM, melyekre az újra felhasználható komponensek gyártása kialakulóban van. Az Objectory konfigurálható folyamat, mivel nem létezhet olyan kőbe vésett folyamat, amely minden szoftver projekt céljainak egyformán megfelelne. Az Objectory egy viszonylag egyszerű folyamat architektúrára épül, mely alapján az aktuális igényekhez igazodó folyamatok családjai alakíthatók ki. Az Objectory lehetővé teszi a folyamatos minőségellenőrzést. Az Objectory folyamat végrehajtásához konkrét fejlesztőeszközök léteznek, melyek a folyamat jelentős lépéseit képesek automatizálni. Ezen eszközök segítségével az egyes modellek létrehozhatók és karbantarthatók, és lehetőséget nyújtanak a vizuális
modellezés, programozás és tesztelés végrehajtására. Nélkülük elképzelhetetlen lenne az egyes iterációs lépések során keletkező változatok nyomon követése és a koherens rendszer adminisztrálása. 3.41 Két dimenzió Az Objectory folyamat szerkezetét kétdimenziós térben szemléltethetjük: - A tartalomtengely (content) a végrehajtandó tevékenységeket reprezentálja logikailag csoportosítva. Az időtengely (time) mentén a folyamat életciklusának egyes állomásait szemlélhetjük. A tartalom dimenzió a folyamat statikus aspektusait mutatja, azaz többek között a folyamat komponenseit, a végrehajtandó tevékenységeket és munkafolyamatokat. Az idő dimenzió a folyamat dinamikus aspektusait mutatja ciklusok, fázisok (phases), iterációk (iterations) és határkövek (milestones) formájában. A folyamat leírása két alapvető nézőpontot tükröz: - A technikai nézőpont a fejlesztendő termékkel kapcsolatos részegységekre
koncentrál. A vezetői nézőpont számára az idő, költségvetés, emberi és egyéb erőforrások csoportosítása és más gazdasági szempontok játsszák a központi szerepet. 3.42 A folyamat statikus szerkezete Ez a rész a folyamatot a tartalom dimenzió szerint írja le, azaz a folyamat statikus szerkezetével foglalkozik. Az Objectory folyamat olyan komponensekből épül föl, melyeket tevékenységek (activity), munkafolyamatok (workflow), dolgozók (workers) és termékek (artifact) segítségével jellemzünk. Az Objectory folyamat hét folyamatkomponensből áll, melyek közül négy tervezői és három segéd folyamatkomponens. A tervezői folyamatkomponensek: - Követelmények megfogalmazása. - Elemzés és tervezés. - Implementáció. - Tesztelés. A segédkomponenesek: - Vezetés. - Telepítés. - Környezet. 3.421 Tevékenységek A folyamat minden komponense összefüggő tevékenységek halmazából áll. A tevékenységek adják meg a részfeladatot
(átgondolás, végrehajtás, ellenőrzés), melynek során a dolgozók termékeket hoznak létre vagy módosítanak. A tevékenység magába foglalhatja a végrehajtásához szükséges technikákat és tanácsokat illetve az automatizálásához szükséges eszközökre vonatkozó utalásokat is. 3.422 Munkafolyamatok A munkafolyamatok a tevékenységek sorrendjét írják le a komponenseken belül vagy közöttük. Az Objectory minden folyamatkomponensre előír egy-egy tipikus munkafolyamatot. Létezik néhány középszintű munkafolyamat is, mely különböző folyamatkomponensek közötti iteratív átmenetek céljaira szolgál. 3.423 Dolgozók A dolgozók adják meg a feladat végrehajtásán dolgozó egyének vagy kisebb csoportok szerepkörét és felelősségét. 3.424 Termékek A folyamaton belüli tevékenységek bemenetei és kimenetei termékek, olykor végtermékek (deliverables). A projekt során keletkező termékek öt fő csoportba sorolhatók: -
Követelmények. Tervek. Implementáció. Telepítés. - Vezetés. A modellek az Objectory folyamatban központi szerepet játszanak. Követelmények A követelmények csoportosítanak minden információt, ami a rendszerrel kapcsolatban támasztott igényekre vonatkozik. Ide tartozhat a szaktudást leíró modell, a használati esetek modellje és egyebek, például különböző előírásokból fakadó kényszerek. Tervek A rendszer konstrukciójával kapcsolatos információkat tartalmazza, ide értve az időkényszereket, költségvetést, valamint a jogi, újrafelhasználhatósági és minőségbiztosítási kérdéseket. Implementáció A rendszert megvalósító forráskódot, konfigurációs fájlokat és a rendszer integrálásának módjára vonatkozó információkat tartalmazza. Telepítés A rendszer összeállítására, szállítási konfigurációjára, installálására és adott célrendszereken való futtatására vonatkozó információkat tartalmaz. Alapja
a telepítési modell (deployment model) Vezetés A projekt végrehajtásának tervezésére, ütemezésre, költségek és kockázatok becslésére vonatkozó információkat tartalmazza. 3.43 Az életciklus szerkezete A következőkben a folyamat dinamikus szerkezetét írjuk le az időtengely mentén. A szoftver életciklus részciklusokra bomlik, ahol minden részciklus a termék egy-egy újabb generációját eredményezi. Az Objectory folyamat a fejlesztési ciklust négy szakaszra osztja: - Indítás (inception). - Kidolgozás (elaboration). - Építkezés (construction). - Átadás (transition). Minden szakasz végét egy-egy jól definiált határkő (milestone) jelzi. A határkövek olyan határidők, melyek leteltével bizonyos lényeges döntéseket meg kell hozni, amihez viszont bizonyos részcélokat el kell érni. 3.431 A folyamat szakaszai és főbb határkövei Adott termékre a négy szakaszt magába foglaló első fejlesztési ciklust kezdeti ciklusnak nevezzük.
Hacsak a termék élete véget nem ér, a termék újabb generációi fejlődnek ki az indítás, kidolgozás, építkezés és átmenet lépések ismétlésével. Ezt a szakaszt hívják evolúciónak, az első ciklust követő ciklusokat pedig evolúciós ciklusoknak nevezik. 3.432 Két egymást követő fejlesztési ciklus Indítási szakasz Az indítási szakaszban meghatározzuk a rendszerrel szemben támasztott üzleti követelményeket és a projekt célkitűzésit. Ehhez meg kell határoznunk minden olyan külső egységet (aktort), mellyel a rendszerünk kommunikálni fog, és meghatározzuk a kommunikáció természetét. El kell különítenünk az összes használati esetet, és a legfontosabbakat le is kell írnunk. Az üzleti követelmények olyan kritériumokat tartalmaznak, mint a kockázatbecslés és az erőforrásigény fölmérése, valamint a fő határköveket és időpontjaikat tartalmazó szakaszterv. Az indítási szakasz végére elkészülnek a projekt
életciklusának célkitűzései, melyek alapján eldönthető, hogy a fejlesztést érdemes-e folytatni. Kidolgozás A kidolgozás célja a megoldandó probléma szakterületének elemzése, szilárd architekturális alap megalkotása, a projekt tervének kifejlesztése és a projektben felbukkanó legkomolyabb kockázati elemek kiküszöbölése. Architekturális döntéseket csak az egész rendszer megértése alapján hozhatunk Ebből következően a használati esetek legtöbbjét le kell írnunk, és figyelembe kell vennünk egyes járulékos kényszereket is. Az architektúra ellenőrzésére demo rendszert implementálunk, mely igazolja az architekturális döntések helyességét, és képes a leglényegesebb használati esetek végrehajtására. A kidolgozási szakasz végére megvizsgáljuk a rendszer részletes célkitűzéseit és hatáskörét, a választott architektúrát és a legfőbb kockázati elemeket. Építkezés Az építkezés során iteratív és
inkrementális módon kifejlesztjük a teljes rendszert, amely kielégíti a felhasználók által támasztott követelményeket. Ehhez le kell írnunk a fennmaradó használati eseteket, befejezni a tervezést és az implementációt és tesztelni a szoftvert. Az építkezés végére eldől, hogy a szoftver, a felhasználói helyek és maguk a felhasználók készek-e az átadásra. Átadás Az átadási szakaszban a felhasználók használatba veszik a szoftvert. Miután a termék a felhasználó kezébe került, gyakran előfordulnak olyan esetek, melyek további fejlesztéseket követelnek a rendszer hangolására, fel nem fedett problémák kiküszöbölésére, vagy olyan szolgáltatások implementására, melyeknek megvalósítását az átadás utánra halasztottuk. Az átadás komolyabb rendszerek esetében béta-verzióval kezdődik. Az átadási szakasz végén eldönthető, hogy az életciklus követelményeket sikerült-e kielégítenünk, és esetleg szükség van-e egy
újabb ciklus elindítására, melynek során az előző ciklus tanulságai alapján módosítható a folyamat. Iterációk Az Objectory folyamat minden szakasza további iterációkra bontható. Az iteráció egy teljes fejlesztési ciklus, melynek eredményeként az adott szoftvertermék egy újabb (belső vagy külső) végrehajtható Az Objectory folyamatban minden iteráció a rendszer egy újabb végrehajtható változatát eredményezi. A dolgozók, tevékenységek és használati esetek kapcsolata. változata (release) keletkezik, mely a teljes rendszer egy részhalmaza. A teljes rendszer így inkrementálisan nő iterációról iterációra, míg el nem érjük a kívánt végterméket. Minden iteráció végigköveti a teljes szoftverfejlesztési ciklust, de a hangsúlyok a mellékelt ábrának megfelelően eltérőek. Az információs halmazok fejlődése az iterációk során. Minden folyamatkomponens termékek egy halmazáért felelős. A görbék magassága jelzi
a befektetett munkamennyiséget. Az iteratív megközelítés legfőbb következménye, hogy az előzőekben leírt termékek a mellékelt diagramnak megfelelően időben növekedenek és javulnak. 3.44 A folyamat ábrázolása A folyamat komponenseit az Objectory dolgozók, tevékenységek termékek és munkafolyamatok segítségével írja le. A dolgozók, tevékenységek, termékek és használati esetek viszonyát a mellékelt ábra szemlélteti. Minden dolgozónak megvannak a saját tevékenységei és termékei. A hozzárendelt tevékenységek halmazán keresztül a dolgozó implicit módon a tevékenység végrehajtásához szükséges képességeket is meghatározza. A projektvezető adott képességekkel rendelkező egyének közül választhat. Az ő feladata, hogy az egyéneket a folyamat megfelelő dolgozóival azonosítsa, mégpedig oly módon, hogy az egyén képes legyen betölteni a dolgozó szerepét, azaz elvégezni a megfelelő tevékenységeket, és létrehozni
a kívánt termékeket. A egyének és dolgozók összerendelése a pillanatnyi igények szerint időben változhat. A folyamatkoponensek és modellek kapcsolata Az ábra egyaránt mutat példát Feladatok kiosztéása: egyének és dolgozók megfeleltetése arra, hogy egyetlen egyén többféle dolgozói szerepkört is betölthet, ugyanakkor egy dolgozói szerepkör egy időben több személynek is kiosztható. A szerepek kiosztásakor a projekvezetőnek törekednie kell a dolgozók közötti információáramlás csökkentésére, azaz a munkájukat a lehető legnagyobb mértékben függetleníteni kell egymástól. 3.441 Folyamatkomponensek és modellek Ahogy ezt korábban említettük, a folyamatkomponensekhez tevékenységek és termékek egy-egy halmaza tartozik. A leglényegesebb termékek azok a modellek, amelyek az egyes folyamatkomponensek során keletkeznek vagy módosulnak. Ilyenek a használati eset modellek, az implementációs modellek és a tesztmodellek. A
következő ábra a folyamatkomponensek és modellek kapcsolatát mutatja. Minden folyamatkoponenshez egy-egy modell tartozik 3.442 Követelményrögzítés A követelményrögzítés folyamatkomponens célja megfogalmazni, mi az, amire a rendszernek képesnek kell lennie, és lehetővé teszi, hogy a fejlesztő és a megrendelő megállapodjon a megfelelő követelményekben. Ehhez a rendszerrel kapcsolatos nézőpontunkat korlátozzuk: definiáljuk a környezetét, és a rendszertől elvárt viselkedést. Mindezt a megrendelők és leendő felhasználók birtokában lévő információk és tudás alapján hajthatjuk végre. A követelményrögzítés eredményeként egy használati modell keletkezik és néhány kiegészítő követelmény. A használati eset model a projekt minden résztvevője számára fontos A használati eset modell aktorokból (actor) és használati esetekből (use case) áll. Az aktorok felhasználókat vagy bármely más külső eseményforrást
jelképeznek, mellyel a rendszer működése során kapcsolatba kerülhet. Az aktorok segítségével szabhatjuk meg a rendszer határát, ezzel pedig tisztább képet kaphatunk. A használati esetek a rendszer viselkedését írják le. Mivel a használati eseteket az aktorok igényei szabják meg, így nagyobb az esélyünk, hogy olyan rendszert kezdünk konstruálni, amit a felhasználók valóban fognak tudni használni. A mellékelt ábra egy hulladékfeldolgozó gép használati eset modelljét mutatja az UML jelrendszer segítségével leírva. A továbbiakban az Objectory folyamat ismertetése során olykor fölbukkannak majd UML modellek. Reményünk szerint a példák elég egyszerűek ahhoz, hogy az UML részletesebb ismerete nélkül is érthetőek, ha azonban valakiben bizonytalanság támadna, előre lapozhat az UML-t tárgyaló részekig, ahol a grafikus modellek szintaxisát részletesen ismeretetjük. Minden használati esetet részletesen le kell írni. A leírás
tartalmazza, hogy a rendszer lépésről lépésre hogyan kommunikál az aktorokkal, és eközben mit 40 csinál maga a rendszer. A használati esetek vezérfonálként szolgálnak a rendszerfejlesztési ciklus során. Ugyanazt a használati eset modellt használjuk a követelményrögzítés, majd az elemzés és tervezés, végül a tesztelés során. Dolgozók és munkafolyamatok A követelményrögzítéshez a következő dolgozók tartoznak: - Használati eset modell konstruktőr (Use-Case Model Architect). - Használati eset specifikátor (Use-Case Specifier). - Követelménylektor (Requirements Reviewer). - Konstruktőr (Architect). A követelményrögzítés munkafolyamatát a következő ábra mutatja. Egy egyszerű használati eset modell 40 Lényeges: mit, de nem hogyan! Tervmodell részlete kommunikáló osztályokkal és csomagokkal A követelményrögzítés munkafolyamata A munkafolyamatot dolgozók és tevékenységeik segítségével ábrázoltuk. A nyilak a
tevékenységek logikai sorrendjét jelzik. 3.443 Elemzés és tervezés Az elemzés és tervezés komponens célja megszabni a rendszer megvalósításának módját, amely majd az implementációs lépés során konkretizálódik. Az elemzés és tervezés során végrahajtjuk a használati eset modellben előírt feladatokat, és kielégítjük a megfelelő követelményeket. Az elemzés és tervezés eredménye egy tervmodell, amely a későbbi forráskód absztrakciójaként szolgál, azaz a forráskód struktúrája és stílusa kliséjének tekinthető. A tervben az egyes használati esetekhez megadjuk az azok megvalósításához szükséges osztályokat és egyedeket. A tervmodell csomagokba szervezett osztályokból áll, és leírásokból, melyek magadják, hogyan működnek együtt ezek az osztályok az egyes használati esetek megvalósításához. A következő ábra az előzőekben bemutatott használati eset modellel rendelkező hulladékfeldolgozó rendszer
tervmodelljének egy részét mutatja. A tervezési tevékenységek az architektúra fogalma köré csoportosulnak. Az architektúra létrehozása és ellenőrzése a korai terviterációk alapvető célja. A szoftver architektúra olyan fogalom, melynek jelentésével minden szoftverfejlesztő intuitív módon tisztában van, így megértése nem okoz gondot, pontos definíciójának megadása mégsem egyszerű. Ennek oka egészen egyszerűen az, hogy nem vonható éles határvonal a tervek és a szoftver architektúra közé: az arhitektúra a tervezés egy speciális aspektusa. A szoftver architektúra egyik leglényegesebb eleme a szoftverrendszernek az algoritmusok és adatstruktárák fölötti strukturális szerveződése. Ide tartoznak a globális vezérlési struktúra, a kommunikációs, szinkronizációs és adathozzáférési protokollok, a fizikai feladatszétosztás és más hasonló problémák. Mindezeken felül az architektúra egy adott környezetben működő
rendszer legmagasabb szintű leírása. Az architektúra egyik oldala a felhasználói környezet (ebben működik), másik oldala a fejlesztői környezet (ebben keletkezik). Az Objectory folyamat számára a rendszer architektúrája (egy adott pillanatban) a rendszer alapvető komponenseinek szerveződése, melyek egymással meghatározott felületeken keresztül kommunikálnak, és hierarchikus szervezésű, egyre finomabb, egymással szintén megfelelő, alacsonyabb szintű felületeken keresztül kommunikáló komponensekből épülnek föl. Az Objectory a szoftver architektúrát ún. architekturális nézetek segítségével írja le Minden architekturális nézet előre definiált szempontok egy halmaza alapján keletkezik, és a fejesztési folyamat adott résztvevőinek igényeit elégíti ki (pl. vezetők, fejlesztők vagy felhasználók) Az architekturális nézetek a rendszerünk több szempont alapján megalkotott hierarchikus dekompozícióját adják. Dolgozók és
munkafolyamatok Az elemzés és tervezés lépéshez a következő dolgozók tartoznak: Az elemzés és tervezés lépéshez kapcsolódó munkafolyamatok leírása dolgozók és tevékenységek segítségével. - Konstruktőr (architect). - Használati eset tervező (use-case designer). - Tervező (designer). - Tervlektor (design reviewer). A következő ábra áttekintést nyújt az elemzés és tervezés munkafolyamatairól. A munkafolyamat architekturális szintű és osztályszintű tervezésre oszlik. A nyilak a tevékenységek logikai sorrendjét jelzik. 3.444 Implementáció A működő rendszer az implementációs lépés során valósul meg a forrásszöveg létrehozásával (forráskód fájlok, fejléc (header) fájlok, make fájlok és egyebek). A forrásszöveget az implementációs modellben valósítjuk meg, mely implementációs csomagokba strukturált modulokból áll. Az Az implementációhoz kapcsolódó munkafolyamatok leírása dolgozók és tevékenységek
segítségével. implementáció alapjául a tervmodellek szolgálnak. Az implementáció tartalmazza az osztályok és csomagok egyedi tesztelését is, de még nem foglalkozik a rendszer egészének (tehát az osztályok és csomagok kölcsönhatásainak) a tesztelésével, ez ugyanis a következő, “ellenõrzés” folyamatkomponens célja. Dolgozók és munkafolyamatok Az implementációs lépéshez a következő dolgozók tartoznak: - Konstruktőr. - Rendszerintegrátor (system integrator). - Kódoló (implementer). - Kódlektor (code reviewer). A következő ábra az implementációs lépés munkafolyamatát szemlélteti. Az implementáció magába foglalja az implementációs nézetek készítését, az osztályok implementálását valamint a rendszerintegráció megtervezését és végrehajtását. A nyilak a tevékenységek végrehajtásának logikai sorrendjét jelzik. 3.445 Ellenőrzés Az ellenőrzés során a teljes rendszer működőképességét vizsgáljuk.
Először a használati eseteket külön teszteljük, hogy megbizonyosodjunk az őket megvalósító osztályok helyes együttműködéséről. Ezután a rendszer egészének bizonyos aspektusait teszteljük a használati eset leírásokat használva a vizsgálatok bemeneti adatai gyanánt. A tesztelés végén a rendszer átadható Az ellenőrzéshez kapcsolódó munkafolyamatok leírása dolgozók és tevékenységek segítségével Dolgozók és munkafolyamatok Az ellenőrzés lépéshez a következő dolgozók tartoznak: - Teszttervező (test designer). - Integrálás tesztelő (integration tester). - Rendszertesztelő (system tester). - Tervező (designer). - Kódoló (implementer). Az ábra az ellenőrzés munkafolyamatát szemlélteti. A munkafolyamat tesztek tervezésével, megvalósításával és végrehajtásával kapcsolatos tevékenységeket tartalmaz. A nyilak a tevékenységek végrehajtásának logikai sorrendjét jelzik. 3.5 Jelrendszer: az UML nyelv alapjai
Ahogy azt a fejezetünk elején elmondtuk, bármely objektumorientált elemző és tervező módszer két alapeleme a folyamat és a jelrendszer. Eddig leírtuk a jelenleg ismert egyik legkorszerűbb folyamat (Objectory) alapvető lépéseit. Most a jelrendszer ismertetésén van a sor Az UML (Unified Modeling Language, azaz egységesített modellező nyelv) a kilencvenes évek elejére kiteljesedő objektumorientált módszerek nyomán kidolgozott modellező eszköz, mely leginkább Booch, Rumbaugh és Jacobson módszereire épül, ugyanakkor jelentőségét tekintve túlmutat azokon. Már az UML nyelvre 1995-96-ban megfogalmazott első javaslatok megjelenése nagy szakmai visszhangot váltott ki, szinte ezzel egyidőben megkezdte a szabványosítását az OMG (Object Management Group), miközben a szabványosítási folyamathoz szinte példátlan módon a világ összes nagy szoftvergyártója csatlakozott, köztük a Microsoft, az IBM, az Oracle, a HP, a Sun, a DEC, a Compaq és
még sorolhatnánk. Idő közben megjelentek a piacon az UML alapú fejlesztőeszközök is. Az UML tervezőrendszerek alapvonása, hogy megfelelő grafikus eszközökkel hatékony segítséget nyújtanak a szoftverfejlesztési folyamat során létrehozandó modellek megalkotásában, majd a modellek alapján képesek különböző célnyelvekre automatikusan kódot generálni, ezzel az implementációt is jelentősen gyorsítják és megkönnyítik. Az UML jelenlegi formájában nem csak egy jelrendszert, hanem egy metamodellt is tartalmaz. A jelrendszer olyan grafikus szimbólumok halmaza, melyeket megfelelő módon egymáshoz kapcsolva fölépíthetjük a számunkra szükséges modelleket. A jelrendszer tehát úgy is fölfogható, mint az UML szintaxisa. Az UML metamodell olyan, UML-ben leírt modell, mely lényegében a nyelv szemantikáját definiálja. A modell meglehetősen nehezen követhető és nem formális, azaz nem tartoznak hozzá olyan formális bizonyító eljárások,
mint amilyen például a predikátum kalkulus esetében a rezolúció. Annyit mindenesetre igazol, hogy az UML legalább a saját konstrukcióinak a modellezésére alkalmas. Jelentősége inkább azok számára komoly, akik az UML nyelvet implementálni szeretnék, ugyanis az UML megértéséhez és használatához nincsen szükség a metamodell ismeretére. A metamodell teljes specifikációját az UML Semantics dokumentáció tartalmazza (http://www.rationalcom/ alatt megtalálható) A következőkben bemutatjuk az UML grafikus modellező nyelv elemeit. Az UML használatát egy egyszerű hallgatói adminisztrációs rendszer modelljének részletein keresztül vizsgáljuk. A rendszer célja a hallgatók, tárgyak és előadók egymáshoz rendelése, és a kapcsolatok adminisztrálása. A példa nem minden részletében kidolgozott, de úgy gondoljuk, hogy ez a hallgatói adminisztrációs rendszerek eredendő tulajdonsága. A rendszertől a következő szolgáltatásokat várjuk el:
- Az adminisztrátor fölveszi az adott szemeszterben meghirdetett tárgyak listáját. Egy-egy tárgyat több különböző előadó is meghirdethet. Minden hallgatónak négy elsődleges és két melléktárgyat kell választania. A hallgatók a jelentkezés után, de a félév megkezdése előtt bizonyos ideig még fölvehetnek újabb tárgyakat, illetve leadhatnak régieket. Az előadók a rendszer segítségével lekérdezhetik csoportjaik névsorát. A rendszer a felhasználóit jelszavakkal azonosítja, melyet bejelentkezéskor kell megadni. 3.51 Használati eset diagrammok Ahogy ez a 3.4 fejezetben elhangzott, a használati esetek a rendszertől meghatározott helyzetekben elvárt viselkedésmintákat írják le. Minden használati eset a rendszer és egy aktor között lejátszódó párbeszéd egyes lépéseit definiálja. A használati esetekre alkalmazott UML jelrendszer a mellékelt ábrán látható. A használati eset modellekben az aktorokon (melyeket
pálcikaemberkék jelölnek) és a használati Admnisztrátor Tárgylista kezelése Használati eset Aktor Jelentkezés Hallgató Névsor lekérdezése Elõadó Társítás A hallgatói adminisztrációs rendszer használati eset diagramja az egyes elemek jelentésével eseteken kívül (melyeket ellipszisek jelölnek), társításokat látunk (ezeket az ábrán nyilak jelölik). Az aktorok és a használati esetek névvel ellátott objektumok, melyek nevét az UML-ben az ábra szerint a megfelelő szimbólum alá, vagy a használati esetet jelölő ellipszis belsejébe kell írni. Az ábrán látható egyszerű elrendezés úgy keletkezett, hogy a feladat megfogalmazása alapján elvégeztük az igényfelmérést, azaz sorra vettük, hogy a specifikációban mely elemek lehetnek az aktorok, és ezeknek mik az igényeik. E szerint a történetnek a rendszeren kívül álló szereplői az adminisztrátor, a hallgató és az előadó. Ezeket nem kell implementálni, csak a
viselkedésüket kell föltérképezni, megérteni és figyelembe venni. A feladat kezdeti megfogalmazásából kitűnik, hogy az adminisztrátor veszi föl a tárgyak listáját, a hallgató jelentkezik, az előadó pedig lekérdezi a jelentkezők névsorát. Az igényfelmérés egy nagyon egyszerű, általában jól használható (de távolról sem csalhatatlan!) módszere, ha megpróbálunk a feladat megfogalmazásában szereplő igékből használati eseteket gyártani, a főnevekből pedig osztályokat (esetünkben aktorokat) képezni. Minden esetben célszerű az aktorok meghatározásával kezdeni a sort. A józan ész ezt diktálja, és többnyire a feladat megfogalmazásában is könnyebb megtalálni az aktorokat, mint a használati eseteket. Megjegyzendő, hogy az UML az aktorokat is osztályoknak tekinti, továbbá, hogy az aktorok nem szükségképpen emberek, hanem bármilyen külső, autonóm rendszert jelképezhetnek, amellyel a rendszerünk kapcsolatban áll. Bár az
ábránkból ez nem nyilvánvaló, egy aktor több használati esethez is tartozhat (vagyis lehet többféle igénye is), és egy használati eset több aktorhoz is kapcsolódhat (vagyis többféle aktor is rendelkezhet azonos igényekkel). Ha meghatároztuk az aktorokat és használati eseteket, minden használati esethez tartozó eseménysorozatról egy-egy forgatókönyvet készítünk, melyben felsoroljuk, mit is kell csinálnia a rendszernek az aktor szemszögéből nézve. A forgatókönyvben célszerű módon ki kell térnünk a használati eset kezdő- és végeseményére, az események normális menetére, és a lehetséges változatokra is, egyes pontokon ugyanis a felhasználó meggondolhatja magát, hibázhat vagy egyszerűen félbehagyhatja, amit elkezdett. A használati eset diagramok eddigiekben ismertetett elemei az aktorok és használati esetek közötti kapcsolatok szemléltetésére alkalmasak. Előfordulhat azonban, hogy a használati esetek dokumentációja
során (vagy esetleg még ez előtt) fölfedezzük, hogy bizonyos használati esetek részben közös viselkedésformákat mutatnak. Az ilyen viselkedésformákat célszerű egy-egy külön használati esetbe kiemelni, mely gyakran nincs is közvetlen viszonyban egyetlen aktorral sem, hanem csupán másik használati esetekhez kapcsolódik. A fenti ábrán a kiinduló használati eset modellünket a jelszó ellenőrzése használati esettel bővítettük, mely mindhárom használati eset részét képezi. Az UML a használati eset modellekben alapvetően háromféle típusú kapcsolat használatát teszi lehetővé, melyeket a kapcsolaton megjelenő kettős csúcsos zárójelpár (<< >>) között elhelyezkedő speciális címke, ún. sztereotípia jelöl A sztereotípiák az UML metamodellben definiált speciális osztályok, melyek meghatározott viselkedést jelölnek. A használati eset modellekben alkalmazott Sztereotípia Admnisztrátor Tárgylista kezelése
<<uses>> Hallgató Jelentkezés Általánosítás <<uses>> <<uses>> Elõadó Névsor lekérdezése Jelszó ellenõrzése <<extends>> Nemlétezõ tárgy Az előző használati eset modell bővítése használati esetek közötti kapcsolatokkal kapcsolatokhoz háromféle sztereotípia tartozhat: <<communicates>>: Az aktorok és használati esetek közötti társítás. Ez az alapértelmezés, ezért ezt külön sosem jelöljük. <<uses>>: Két használati eset közötti általánosítás. A hivatkozott használati eset a hivatkozó használati eset által nyújtott viselkedés szerves része. <<extends>>: Két használati eset közötti specializáció. A hivatkozó használati eset a hivatkozott használati eset speciális esete, mely bizonyos feltételek mellett bővíti a hivatkozott használati eset által produkált viselkedést. A fenti három eset közül a
<<communicates>> egyértelmű, a másik két eset közötti különbség a fenti, szűkszavú definíció alapján azonban távolról sem nyilvánvaló, ezért ezek használatát bővebben kifejtjük. A megértés kulcsa az általánosítás reláció (a diagramokon háromszögfejű nyíl jelöli) jelentésének megértése. A <<uses>> kapcsolat a használati esetek egyfajta strukturális dekompozícióját jelenti, ahol a hivatkozott használati eset a hivatkozó használati eset szerves részét képezi. A hivatkozott használati eset a hivatkozó eset szuperosztályaként is fölfogható, a használati esetek azonban nem osztályok, így ez a szóhasználat csak a megértést segítő analógia. A példánkban a jelszó ellenőrzése általánosabb tevékenység, mint a másik három használati eset által meghatározott műveletek. Az <<extends>> kapcsolat olyan speciális esetet jelöl, mely egy használati eset részeként áll elő, de nem
minden esetben következik be. Ezáltal a hivatkozó eset olykor (de nem mindig) a hivatkozott eset viselkedését bővíti. Ilyen módon lehet modellezni a hibaállapotok kezelését, a felhasználói visszalépéseket, megszakításokat és egyéb különleges résztevékenység sorozatokat. Érdemes megjegyezni, hogy az általánosítás iránya az <<extends>> és <<uses>> esetben fordított, ami egyébként logikus, mert a <<uses>> esetben a hivatkozó, az <<extends>> esetben pedig a hivatkozott reláció az általánosabb. 3.52 Osztálydiagramok Az osztálydiagramok a legalapvetőbb objektumorientált modellező eszközök, melyekkel a rendszert fölépítő objektumokat és a közöttük lévő statikus kapcsolatokat írhatjuk le. Az osztálydiagramok ezért a statikus struktúradiagramok közé tartoznak. Az osztálydiagramokon alkalmazott alapvető jelöléseket a következő ábra mutatja. Általánosítás Felhasználó
név jelszó Navigálhatóság iránya * * Adminisztrátor Hallgató 1 3.10 Jelentkezéskezelõ Elõadó 1 tárgy felvétele(cím, kredit) tárgy törlése(cím) 1 szakirány beosztás Tárgylista kezelõ 1 Multiplicitás hallgató felvétele(tárgy, hallgató adatai) Csoport 0.4 1 4 * Névrekesz Meghirdetés Tulajdonságrekesz terem idõpont megnyitás() hallgató felvétele(hallgató adatai) 1.* 1 1.* Tárgy cím kredit Módszerrekesz Osztály Osztálydiagramokban alkalmazott főbb UML jelölések. Bár ez nem része az UML definíciójának, az UML lehetőséget ad nem teljesen specifikált modellek megalkotására is. Ily módon a tervezési folyamat különböző szakaszaiban is lehetővé válik ugyanazoknak a modelleknek a használata, és azokat csak megfelelő részletekkel bővítenünk kell. Az ábrán szereplő modell például nem tartalmazza az osztályok tulajdonságainak és a módszerek paramétereinek típusát, és nem mutatja az egyes
elemek láthatóságát. Az osztályokat és objektumokat is olyan téglalapok jelölik, melyek többnyire három rekeszre vannak osztva. A felső (név) rekeszben jelenik meg az objektum neve. A névrekeszben megjelenő egyszerű szimbólum osztályt jelöl (ez a leggyakoribb eset), az <objektum>:<osztály> szintaxis pedig az <osztály> osztályba tartozó <objektum> nevű objektumot. Objektumok megadásánál az objektum és osztály nevét alá kell húzni. A középső (tulajdonság) rekeszben kell megadni a tulajdonságok nevét A tulajdonságokhoz a néven kívül típus, kezdőérték és a láthatóságot jelző információ is rendelhető a következő szintaxis szerint: <láthatóság><név>:<típus>=<kezdőérték>. A láthatóság a + (public - nyilvános), - (protected - védett) vagy # (private belső) szimbólumok egyikével jelölhető. Ha a láthatóságot az ábrán nem jelöljük, ez nem jelent semmiféle
alapértelmezés szerinti beállítást, pusztán csak azt, hogy az adott pillanatban nem akarunk vele foglalkozni. Tárgy + cím : String - kredit : int = 1 Az ábrán a tárgy osztály látható tulajdonságainak részletes leírásával. A műveletek az alsó (módszer) rekeszben definiálhatók a következő szintaxis szerint: <láthatóság> <név> ( <paraméterek> ) : <visszaadott érték típusa>. Az osztályok jelölése ezen a szinten elég könnyen érthető, a társítások azonban látszólagos egyszerűségük ellenére ennél bonyolultabban írhatók le és jellemezhetők. A társítások osztályok vagy objektumok kapcsolatát fejezik ki. A társításokhoz nevet rendelhetünk, melyet címkeként a társítást reprezentáló vonal közelébe írhatunk. Erre az ábrán az Előadó és Meghirdetés osztályok közötti társítás mutat példát, melyhez a Csoport nevet rendeltük. Minden társításhoz irányától függően két szerep
rendelhető. Az ábránkon például a Csoport társítás alapján kereshetjük egy adott előadóhoz tartozó meghirdetett tárgyak halmazát (ebben az esetben a szerep előadó tárgyai), vagy kereshetjük, hogy egy tárgyat ki ad elő (ebben az esetben a szerep tárgy előadói). A szerep kiinduló osztályát szokás forrásosztálynak, ahová pedig a szerep mutat, célosztálynak nevezni. A szerepeknek nevet adhatunk, mely mindig a társítás forrásosztály felőli végéhez kötődik, és szükség szerint ott is jelenik meg. A szerepekhez multiplicitás is rendelhető, mely meghatározza, hány objektum vehet részt az adott relációban. A multiplicitás a szerep forrásosztálya mellett megjelenő jelzés, melynek szintaxisa a következő. Egy szám adott számú elem meglétét követeli Így a példánkon minden meghirdetett tárgyhoz kötelezően egy és csak egy előadó tartozhat. A tartományt ij jelöli, ahol i a tartomány alsó határa, j pedig a fölső határ. A
* egyenrangú a 0.∞ tartománnyal A példánkon tehát egy hallgató pontosan 4 meghirdetett tárgyra jelentkezhet, egy tárgyra legalább 3 de legföljebb 10 hallgató jelentkezhet, továbbá minden tárgyat legalább egyszer meg kell hirdetni, de elvileg akárhányszor meghirdethető 41. Az elemzés és tervezés során a társítások szerepe egyértelmű: két osztály illetve objektum közötti viszonyt fejezik ki (ilyen az ábrán látható osztálydiagram is). Ha a társítások implementációja is szóba kerül (ami egyébként a fejlesztési folyamat későbbi, implementációs jellegű lépései során várható), a társítások megvalósításának lehetősége is érdekessé válik. A társítások az általuk összekötött osztályok közül legalább az egyikben megjelenő tulajdonságok, esetleg módszerek formájában valósíthatók meg. Ha a társításokat tulajdonságként valósítjuk meg, tehetjük ezt valamilyen objektumreferencia (pl. a hivatkozott
objektumra mutató pointer) vagy tartalmazás formájában (ilyenkor a hivatkozó objektum munkaterülete tartalmazza magát a hivatkozott objektumot is, azaz a társítást implementáló tulajdonság nem objektumreferenciát, hanem egy objektumot jelöl). A társítások további jellemzője a navigálhatóság iránya, melyet a társítást jelölő vonal egyik végén elhelyezett nyíllal jelölünk. Elvi szinten ez azt fejezi ki, hogy például a jelentkezéskezelő információkkal kell rendelkezzen a tárgyak listájáról, e nélkül ugyanis nem leszünk képesek az egyes hallgatókat a választott tárgyakhoz hozzárendelni, ezzel szemben a tárgyak vizsgálatához nincsen szükségünk a jelentkezéskezelő létére utaló információra. Implementációs szinten a navigációs irány megadása azt eredményezi, hogy a forrásobjektumban megjelenik majd a társítást megvalósító tulajdonság vagy módszer, a célobjektumban azonban nem. A kapcsolatok alapértelmezés
szerint kétirányúak, ezért a mindkét végén nyíllal rendelkező kapcsolat - bár az UML szintaxis megengedi csak az ábrát bonyolítja, éppen ezért felesleges. A tulajdonságok sok szempontból a társításokhoz hasonlóan viselkednek, olyannyira, hogy - bár jóllehet ez a módszer nem igazán célszerű - a társítások helyett közvetlenül fölvehetünk megfelelő objektumokra hivatkozó tulajdonságokat is. Az UML osztálydiagramok a példában bemutatott alapvető elemeken kívül további fogalmak jelölését és kezelését is lehetővé teszik. Ezeket foglalják össze a következő bekezdések 41 Ez a módszer talán a mi egyetemünkön is beválna, a kérdés megválaszolása azonban kívül esik a szoftvertechnológia témakörén. Összetétel és kompozíció Az UML lehetőséget ad összetett objektumok definiálására és kezelésére. Összetett objektum kétféleképpen keletkezhet. Ha az összetételt választjuk, a tároló objektum és az azt
fölépítő részobjektumok fizikailag elkülönülnek. Kompozíció esetén a tároló objektum a részobjektumokat fizikailag tartalmazza, azaz együtt keletkeznek és szűnnek meg. Az összetétel jelölése az ábrának megfelelően a társításnak a tároló objektum felőli végén elhelyezett üres rombusz, a kompozícióé pedig a teli rombusz. Megjegyzendő, hogy a kétféle eset közül bármelyiket is választjuk, a társításnak a tároló objektum felőli végén (ahol az aggregátumot jelöljük) a multiplicitás kötelezően egy, a másik végén pedig lehet egy vagy több is. Az összetétel egyébként az UML-ben úgy is jelölhető, hogy a részobjektumokat a tároló objektumok belsejébe rajzoljuk. Sokszög Kompozíció 1 3.* 1 Összetétel Pont xpoz ypoz 1 Jellemzõk elõtérszín háttérszín vonalvastagság Összetétel és kompozíció jelölése Kvalifikált társítás Osztály A kvalifikált társítás egy tulajdonságot használ föl kulcs
(qualifier) gyanánt, melynek segítségével a társítás megfelelő végéhez tartozó objektumok csoportosíthatók, és a megfelelő kulcsértékek alapján az adott csoportok elemei lekérdezhetők. Az ábra a kvalifikált társítás jelrendszerét mutatja. A kulcsként használt tulajdonság neve a társítás megfelelő végén megjelenő dobozban helyezkedik el. A doboz egyik éle közös a megfelelő osztályt jelölő téglalapéval. Implementációs szinten a kvalifikált társítás többnyire a célosztály objektumaiból (a példánkon a Dolgozó) képzett indexelhető tömb vagy valamiféle más, kulcs szerinti keresésre alkalmas összetett adatstruktúra (pl. hashtábla) megjelenését eredményezi a forrásosztályban (esetünkben Osztály). 1 1.* Osztály# Dolgozó Kvalifikált társítás Társításosztály (association class) A társításosztályok segítségével társításokhoz tulajdonságokat és műveleteket rendelhetünk. Erre például olyankor van
szükség, ha két osztály egyedei között több-több értelmű leképezést akarunk megvalósítani, de az Versenyzõ Verseny egymáshoz rendelt párokhoz még olyan * 1.* további információkat is rendelnénk, amelyek igazából egyik osztályhoz sem Társításosztály rendelhetők kizárólagos módon hozzá. Erre Nevezõk mutat példát a mellékelt ábra, ahol egy helyezés versenyekkel kapcsolatos nevezési listák kezelésére alkalmas osztálystruktúrát láthatunk. A példa akkor értelmes, ha van Példa társításosztályra legalább egy verseny, amelyre nevezni lehet. Minden versenyre elvileg akárhány versenyző benevezhet (a gyakorlatban persze ez sokkal bonyolultabb, de a példánk szempontjából ez most közömbös). A vesenyzők és versenyek egymáshoz rendelését a Nevezők osztály valósítja meg, mely ezen kívül még azt is tartalmazza, hogy az egyes versenyzők az egyes versenyeken milyen helyezést értek el. A társításosztályt pontosan úgy
kell leírni, mint egy normál osztályt, majd az osztályt jelölő téglalapot szaggatott vonallal kell a társítás vonalához kötni. A társítás vonala, az osztályszimbólum és a társítást az osztállyal összekötő szaggatott vonal egyetlen objektumot jelölnek. Így például a társítás és a hozzá tartozó osztály külön elnevezése redundáns, és ha nem egyforma nevet kapnak, helytelen is. Leszármaztatott elemek Az UML lehetővé teszi számított elemek felvételét a modellünkbe. A mellékelt ábrán egy egyszerű osztály látható, ahol egy személy életkorát születési dátumának és a mai dátumnak a különbségeként számítjuk ki. A számított tulajdonságokat a tulajdonság neve előtt megjelenő / jellel jelöljük. A számításhoz használt kifejezést kényszerként (constraint) írhatjuk le. A kényszerek az UML definíció részét képezõ OCL (Object Constraint Language) nyelven megfogalmazható kifejezések, melyek ezután az UML
modellek szabványos részei. Dolgozó születésnap / életkor {életkor = maiDátum - születésnap} Számított elemek kezelése A kényszereket kapcsos zárójelek ( {.} ) közé kell írni Az OCL definiál bizonyos általa kezelt típusokat és az ezeken elvégezhető műveleteket. A típusok köre az egyszerű típusoktól (pl Integer, String, Boolean) az összetett típusokig terjed (pl. Collection, melynek altípusai a Set (halmaz), Bag (halmaz, mely többször is tartalmazhat azonos elemet), Sequence (rendezett Bag)). Ezeknek megfelelően az elvégezhető műveletek közé tartoznak az egyszerű aritmetikai utasítások (melyre egyébként a mi ábránkon látható kényszer is példa), és az összetett típusokkal végzett komplex műveletek is (pl. halmazokkal (Set típus) kapcsolatban metszet, unió, tartalmazásvizsgálat, számosság meghatározása és mások). Az OCL nyelvről hely (és idő) hiányában itt ennél részletesebben nem szólhatunk Részletes
specifikációja a http://www.rationalcom/ címen található az Object Constraint Language Specification című dokumentációban. Absztrakt osztályok és felületek Ahogy ezt a Java ismertetése során láttuk, az interface konstrukció segítségével definiálhatunk osztályt úgy is, hogy nem adjuk meg az implementációját, csak azokat az elemeket (a nevét és tipikusan a módszereinek a lenyomatát), melyek segítségével az osztály szolgáltatásai felhasználhatók. Bár a legtöbb nyelvben a felület és az implementáció együttesen jelenik meg az osztálydefiníciók szintjén, és egymástól nem választható el (még akkor sem, ha az implementációt az osztály felhasználói elől elrejtjük), a tervezés során a felületek definiálása nyelvtől függetlenül jó startégia. A felületek az UML-ben absztrakt osztályokkal egyenértékűek. Az ábrán egy operációs rendszertől független felülettel rendelkező, több platformon is működő editor Windows
Ablak tervének egy részletét látjuk. Az Megvalósítás editor ablakozó környezetben műkömegnyit() Függőség dik, így szüksége van egy olyan lezár() ablak megnyitására, amelyben a <<Interface>> szövegszerkesztési műveletek Motif Ablak Ablak elvégezhetők, majd a munka Editor befejeztével az ablakot le kell zárni. megnyit() megnyit() lezár() Ehhez definiálunk egy általános lezár() (rendszerfüggetlen) felületet, melyet Macintosh Ablak Ablaknak nevezünk. A felület műveleteket biztosít, amelyeket az megnyit() egyes operációs rendszerek alatti lezár() Példa felület használatára ablakozó könyvtárakat használó konkrét osztályok implementálnak. A megvalósítást a háromszögfejű, szaggatott nyíl jelzi, míg az egyszerű szaggatott nyíl függőséget jelöl. A függőség mindig azt fejezi ki, hogy ha a hivatkozott elem megváltozik, ez a hivatkozó elem változását vonhatja maga után. Azaz, ha a példánkban
megváltoztatjuk az Ablak felület elemeit, minden bizonnyal az Editor osztályon is változtatnunk kell. Ebből eredően célszerű a terveinket úgy konstruálni, hogy a függőségek számát minimalizáljuk. Személy A felületekre az UML másfajta jelöléseket is megenged. További részleteket az UML Notation Guide tartalmaz (http://www.rationalcom//) 3.53 Csomagdiagramok A csomagok (package) olyan eszközök, melyekkel funkcionálisan összetartozó modellelemek egyetlen magasabb szintű egységbe foghatók, így a rendszerünk statikus struktúrája kezelhetővé válik. Még bonyolult rendszerekben is, ahol az osztályok száma elérheti a több ezret, ügyes szervezéssel megoldható, hogy a csomagok száma ne legyen több néhány tíznél. Jelentkezések Tárgyak A hallgatói adminisztrációs rendszer egy lehetséges csomagdiagramja Bár az UML lehetővé teszi tetszőleges modellelemek csomagokba rendezését, a csomagokban mégis alapvetően osztályok helyezkednek
el, az osztályok között pedig, mint tudjuk, függőségek létezhetnek. Ha egymástól függő osztályok különböző csomagokba kerülnek (ami általánosságban elkerülhetetlen), ez a csomagok között is függőségek kialakulásához vezet. A csomagdiagramok tehát a csomagokat jelölő szimbólumokon kívül a csomagok közötti függőségek jelölésére is alkalmasak az ábrának megfelelő módon. A csomagok szükség szerint egymásba ágyazhatók, így a teljes rendszer leírható egyetlen nagy csomagként, melynek részei az egyes részrendszereket megvalósító csomagok, amelyek szintén további beágyazott részcsomagokra dekomponálhatók. A csomagok meghatározzák a bennük megjelenő modellelemek láthatóságát, és más csomagokból való felhasználhatóságát, tehát birtokolják a hozzájuk rendelt modellelemeket. Ezáltal fontos szerepet játszanak a fizikai konfiguráció, tárkiosztás és hozzáférés ellenőrzés szabályozásában. A csomagokat
az ábra szerint bal felső sarkukban kis “fülecskével” ellátott téglalapok jelölik. A csomag neve a téglalap belsejében, vagy ha a csomag tartalmát is meg akarjuk mutatni, a fülecskében jelenik meg. Ez utóbbi esetben a csomag tartalma a téglalapban látható Az ábrán megjelenő egyes csomagok egyébként a következő osztályokat és a közöttük lévő kapcsolatokat tartalmazzák: - Személy: Felhasználó, Adminisztrátor, Előadó, Hallgató, Jelszó ellenőrző. Tárgyak: Tárgylista kezelő, Tárgy, Meghirdetés. Jelentkezések: Jelentkezéskezelő. 3.54 Kölcsönhatásdiagramok (interaction diagrams) A kölcsönhatásdiagramok objektumokból és a közöttük lezajló üzenetváltásokból épülnek föl, így segítségükkel leírható, hogyan valósulnak meg a használati esetek objektumcsoportok együttes tevékenységsorozatai nyomán. A kölcsönhatásdiagramok két típusa a szekvencia diagram (sequence diagram), mely objektumok közötti
üzenetváltások időbeli sorrendjének leírására szolgál, illetve az együttműködési diagram (collaboration diagram), mely az objektumok kölcsönhatásait egymáshoz való kapcsolataik alapján adja meg. A kétfajta kölcsönhatásdiagram inkább egymás alternatívái, mint kiegészítői, így ízlés dolga, illetve a pillanatnyi feladat jellegéből fakad, melyiket részesítjük előnyben. Példaként a tárgylista kezelése használati eset következő forgatókönyvéhez szerkesztünk szekvenciaés együttműködési diagramot: - - Az adminisztrátor jelszavának begépelésével bejelentkezik a hallgatói adminisztrációs rendszerbe. A rendszer ellenőrzi a jelszót (T1) és megkéri az adminisztrátort az aktuális félév vagy egy következő félév kiválasztására (T2). Az adminisztrátor kiválasztja a kívánt félévet. A rendszer megkéri az adminisztrátort a megfelelő tevékenység (bevitel, törlés, lekérdezés vagy kilépés) kiválasztására.
Bevitel esetén a tárgy hozzáadása résztevékenység sorozat hajtódik végre (R1) Törlés esetén a tárgy törlése résztevékenység hajtódik végre (R2) Lekérdezés esetén a tárgyak lekérdezése résztevékenység hajtódik végre (R3) Ha a kiválasztott tevékenység a kilépés, a használati eset véget ér. Szekvencia diagram A következő ábra a tárgylista kezelése használati eset szekvencia diagramját mutatja. A szekvencia diagram az egyes objektumok közötti üzenetváltásokat, és azok időbeli lefolyásának sorrendjét írja le. Vízszintes tengelyéhez a tevékenységek végrehajtásában részt vevő objektumok, függőleges tengelyéhez pedig az idő tartozik. (Szükség esetén a két tengely szerepe fölcserélhető) A szekvencia diagramon az objektumokat az osztálydiagramokon szokásos módon az <objektum>:<osztály> szintaxissal jelöljük. Az osztály neve vagy az objektum neve elhagyható, a kettőspont és az aláhúzás azonban
mindig kötelező. Az egyes objektumok időbeli viselkedését (“életpályáját”) az objektumot jelölő dobozból kiinduló szaggatott függőleges vonal jelöli. Az objektumok mindig a hozzájuk tartozó életpálya legfelső pontján helyezkednek el. Az objektumok egymással üzeneteket váltanak. Az üzeneteket vizszintes, folytonos nyilak jelölik, melyekre címkeként rá kell írni az üzenet nevét, mely gyakran az üzenetváltásban részt vevő objektumok egy módszerére utal. A nyíl iránya értelemszerűen megadja az üzenet küldőjét és fogadóját. Az üzenetek különböző fajtáinak jelölésére az UML a következő lehetőségeket biztosítja: - Hivás-visszatérés: a híváshoz tartozó nyíl feje tömör; a visszatéréshez tartozó nyíl szára szaggatott. - Konkurens rendszerben a tömör nyílfej szinkron üzenetváltást, a fél tömör nyílfej pedig aszinkron üzenetváltást jelöl, vagyis az első esetben az üzenet küldője köteles
kivárni a választ, a második esetben viszont nem. - Feltételes elágazás: Ha egy pontból több eseményt indítunk, és minden eseményhez egy-egy őrfeltételt (guard condition) rendelünk, akkor kokurens vagy feltételes ágakat indíthatunk. Az utóbbi helyzet akkor áll elő, ha az őrfeltételek diszjunkt eseteket határoznak meg. Az őrfeltétel szintaxisa: [<OCL logikai kifejezés>]. Az egyes objektumok életpályájuk során lehetnek aktív vagy passzív állapotban. Az aktív objektum birtokolja a vezérlést, azaz éppen tevékenységeket hajt végre (“fut”), a passzív objektum pedig nem. Ha e két állapotot a szekvencia diagramon is meg akarjuk különböztetni egymástól, a passzív állapotot a szaggatott vonal, az aktív állapotot pedig megfelelő magasságú, a szaggatott vonalat eltakaró téglalappal jelölhetjük. Az UML egyébként megengedi pusztán a szaggatott vonal használatát is Feltételes üzenetváltások esetén egy objektum
életpályája többfelé ágazhat, majd egy adott ponton pwd : Jelszó ellenõrzõ a : Adminisztrátor t:Tárgylista kezelõ Azonosításkérés Név, jelszó magadása Életpálya [ellenõriz(név,jelszó) = TRUE] Jelszó rendben [ellenõriz(név,jelszó) = FALSE] Jelszó hibás Üzenet Feltételes elágazás Aktív állapot Művelet kiválasztása [bevitel] tárgy felvétele(cím, kredit) [törlés] tárgy törlése(cím) [lekérdezés] tárgyak lekérdezése(feltételek) Visszatérés [kilépés] A tárgylista kezelése használati eset szekvencia diagramja egyesülhet. Objektumok a szekvencia diagramban jelölt események hatására létrejöhetnek (ekkor az illető objektum nem a diagram felső szélén, hanem az őt létrehozó üzenettel egy magasságban jelenik meg), illetve meg is szűnhetnek (ekkor az objektum életpályáját egy nagy X zárja le, és onnantól az életpálya nem folytatódik tovább). Az üzenetek időzítését a megfelelő objektum
életpályája mellé írt címkékkel, és a címkékre hivatkozó OCL kényszerekkel lehet megadni. Ha maga az üzenetküldés időbe telik, az UML megengedi, hogy az üzenetet jelölő vonal ferdén lefelé haladjon a küldőtől a fogadó irányába. Együttműködési diagram A kölcsönhatásdiagramok második formája az együttműködési diagram. Az együttműködési diagramok az objektumokat a szekvencia diagramokban látott módon téglalapként ábrázolják, melynek belsejében föltüntetjük az objektum nevét. Az egyes objektumokat társítások kapcsolják egymáshoz, és ezek mellett jelennek meg az üzeneteket jelképező nyilak, melyeken címkeként föltüntetjük az üzenet nevét. Ha egy objektum az együttműködési diagramhoz kötődő üzenetek hatására keletkezik, ezt a neve mellé írt {new} címke jelzi. Hasonló módon, ha az objektum megszűnik, a {destroyed}, ha pedig keletkezik, majd megszűnik, a 1: tárgy felvétele(cím, kredit) {transient}
címkével jelölhető. : Tárgylista Az együttműködési diagramok kezelõ szemléletesen mutatják az adott tevékenységben részt vevő : Adminisztrátor objektumok egymáshoz képesti 2: Új tárgy létrehozása strukturális elhelyezkedését, azonban itt az üzenetek időbeli sorrendjének {new} : Tárgy leírására külön jelölést kell alkalmazni. Ennek legegyszerűbb módja az üzeneteknek bekövetkezési Egyszerű együttműködési diagram sorrendjük szerinti számozása. Konkurens működésű objektumok esetén azonban ez a módszer nem kielégítő, ezért az UML ilyenkor e helyett a decimális szorszámozást használja, amely tükrözi az üzenetek egymáshoz viszonyított ok-okozati sorrendjét is. Ha egy M üzenet elindít egy eseménysorozatot, M sorszáma 1. Minden olyan üzenet, melyet M váltott ki, az 1i sorszámot kapja, ahol i pozitív egész szám. Így például az 1235 sorszámú üzenet az ok-okozati lánc negyedik szintjén helyezkedik el, és az
1.23 sorszámú üzenet által kiváltott üzenetek közül sorrendben az ötödik Az együttműködési diagramokon is használhatunk feltételes üzeneteket (a feltételek megadásának szintaxisa megegyezik a szekvencia diagramok esetén elmondottakkal), de ez a diagram típus jellegzetességeiből fakadóan (egyszerűen hely hiányában) nemigen teszi lehetővé két objektum közötti bonyolult üzenetváltások leírását. A szekvencia diagramok esetén láttuk, hogyan lehet feltételes végrehajtási ágakat modellezni. Az ábránk még így is áttekinthető maradt, de ennek csak az volt az oka, hogy nagyon egyszrű volt a modellezett részprobléma. Könnyen előállhat olyan helyzet, mikor a feltételes működéssel kapcsolatos esetek már nem férnek el egyetlen diagramon. Ekkor minden részműködéshez egy-egy külön szekvencia diagramot vagy együttműködési diagramot rendelünk. 3.55 Állapotdiagramok és aktivitás diagramok Az eddigiekben bemutatott
diagramtípusokkal képesek vagyunk modellezni a megoldandó feladat specifikációját (használati eset diagramok), a megoldásban részt vevő osztályok és objektumok belső struktúráját és egymáshoz való viszonyát (osztálydiagramok) és a rendszerben végbemenő üzenetváltási szekvenciákat (kölcsönhatás diagramok). Mindez arra elég, hogy a tervezett rendszer struktúráját leírjuk, és a kölcsönhatásdiagramokban megjelenő üzenetek alapján meghatározzuk az egyes osztályokhoz tartozó módszereket. Az állapotdiagramok és aktivitás diagramok segítségével a rendszerünk procedurális részeit (vagyis a módszerek törzsét) tudjuk megtervezni. Mint tudjuk, bizonyos osztályok belső állapottal rendelkezhetnek, mely üzenetek hatására megváltozhat. Az állapotdiagram egy osztály belső állapotainak egymástól és különböző üzenetektől való függését írja le. Az aktivitás diagramok segítségével a rendszerben megjelenő
tevékenységek (pl. módszerek) végrehajtásának módja írható le meglehetősen precízen, lépésről lépésre, durva hasonlattal élve valahgy úgy, mint egy folyamatábrán. A következő oldalon látható ábra egy viszonylag egyszerű állapotdiagramot mutat. Az állapotdiagram kötelezően legalább egy kiinduló állapotot tartalmaz (teli kör), tartalmazhat egy vagy több végállapotot (üres körben teli kör), valamint tetszőleges számú belső állapotot (lekerekített téglalapok). Az egyes állapotok állapotátmenetek hatására változnak (nyilak) A kiindulóállapotnak csak kimenő, a végállapotnak csak bemenő, míg a belső állapotoknak ki- és bemenő állapotátmenetei is lehetnek. Az állapotátmenetekhez címke rendelhető, melynek szintaxisa: <név> [<őrfeltétel>] / <tevékenység> ahol <név> az állapotátmenetet kiváltó esemény neve, <őrfeltétel> egy alkalmas logikai kifejezés, melynek az állapotátmenet
végrehajtásához igaznak kell lennie, és <tevékenység> egy olyan akció, melyet az állapotátmenet során végre kell hajtani. A tevékenység itt és az állapotdiagramokban minden esetben oszthatatlanok, azaz végrehajtásuk elemi utasításként fogható föl. Az állapotátmenet vezethet az aktuális állapotba is (azaz az állapotdiagram szintjén nem szükségképpen jár Kezdőállapot Új hallgató [hallgatók < 10] Inicializálás Új hallgató Megnyitva do / tárgy inicializálása Törlés entry/ Hallgató felvétele Állapotátmenet exit/ hallgatók++ Törlés Lezárás Állapot Névrekesz Törölve Törlés Lezárva do / Jelentkezõk értesítése Belső átmenet rekesz do / tárgy véglegesítése Végállapot A jelentkezők listájának kezelését leíró állapotdiagram legfelső szintje állapotváltozással). A belső állapotok további lényeges vonásai, hogy névvel láthatók el. Minden névtelen belső állapot különböző,
egyébként egy állapotdiagramon belül két azonos nevű állapot ne szerepeljen42. A belső átmenet rekeszben (az állapotok alsó rekesze) olyan tevékenységeket lehet megadni, melyeket az objektum különböző üzenetek hatására végrehajt, de közben az állapota nem változik. A belső átmenetek leírásának szintaxisa: <esemény> <argumentumok> [<őrfeltétel>] / <tevékenység> ahol <esemény> az állapotátmenetet kiváltó esemény neve, <argumentumok> az esemény paraméterei, <őrfeltétel> egy alkalmas logikai kifejezés, melynek az állapotátmenet végrehajtásához igaznak kell lennie, és <tevékenység> egy olyan akció, melyet az állapotátmenet során végre kell hajtani. Az UML háromféle előre definiált belső állapotátmenet típust is megad, melyek nem használhatók felhasználói események neveiként, nincsenek argumentumaik, és nem rendelhető hozzájuk őrfeltétel. Az események, definíciójuk
szintaxisa és hatásuk a következő: entry / <tevékenység> Az állapotba való belépéskor végrehajtandó elemi utasítást definiálja. exit / <tevékenység> Az állapot elhagyásakor végrehajtandó elemi utasítást definiálja. do / <diagramnév> (<argumentumok>) Az adott állpotdigramba ágyazott állapotdiagramot definiál, mely automatikusan végrehajtódik az adott állapotba való belépéskor. A <diagramnév> állapotdiagramnak kell kezdő és végállapottal is rendelkeznie. Egy állapoton belül további alállapotok hozhatók létre, melyek az állapot belsejében jelennek meg. Az alállapotok lehetnek szekvenciálisak vagy párhuzamosak, de a kétféle módszer egyidejű alkalmazása nem lehetséges. A továbbiakban az aktivitás diagramokkal foglalkozunk, melyre a következő ábrán láthatunk egy példát. Megjegyezzük, hogy a mintaproblémaként felvetett hallgatói adminisztrációs rendszer alapján csak túlságosan egyszerű
aktivitásdiagramok adódnak példaként, így ez esetben egy absztrakt példát használunk. Egy hétköznapi tevékenységsorozat modellezéséről van szó, nevezetesen a "colával-vagykávéval-mérgezzük-magunkat" dilemma eldöntésének egy lehetséges algoritmusáról Az aktivitásdiagramok aktivitásállapotokat (félkör oldalú "téglalapok"), állapotátmeneteket (nyilak), feltételes elágazásokat (rombuszok) illetve szinkronizációs pontokat tartalmaznak (vastag, vízszintes vonalak). Az aktivitásdiagramok is rendelkeznek kezdőállapottal, és tartalmazhatnak egy vagy több végállapotot is. Ez utóbbiak jelölése megyezik az állapotdiagramokon alkalmazott jelölésekkel 42 Ne szerepeljen, de azért az UML a dolgot megengedi. Ez egyike az UML jelenlegi számos, apró, elvarratlan szálának, amelyek olykor gondot okozhatnak. Persze az UML fejlesztőinek is élni kell valamiből az elkövetkezendő években. Az aktivitásállapot belső
tevékenységet magába foglaló állapotot jelöl, melyhez legalább egy bemeneti, és legalább egy kimeneti állapotátmenet kapcsolódik. [van cola] Keress italt Kezdőállapot Feltételes elágazás [van kávé] Az állapotátmenetekhez az ábrának megfelelően a szokásos szintaxisú őrfeltételek kapcsolhatók, melyeket az állapotátmenetet jelölő nyíl címkéjeként tüntetünk föl. Érdemes kiemelni a potenciálisan párhuzamos végrehajtási ágak kezelését. Ha a példánkban kávéfőzésre adjuk a fejünket, három kezdeti tevékenységet kell végrehajtanunk (kávéfőző feltöltése vízzel és kávéval illetve pohár beszerzése), azonban ezek sorrendje kötetlen. Mivel a végeredmény szempontjából közömbös, hogy milyen sorrendben hajtjuk őket végre, akár meg is köthetnénk a sorrendet, de ezzel a későbbiekben, az aktivitások tényleges végrehajtása során szinte biztosan hatékonytalanabb megoldáshoz jutnánk, ahol nem tudjuk jól
kihasználni az erőforrásainkat. [nincs cola] [nincs kávé] Állapotátmenet Tegyél kávét a gépbe Tegyél vizet a gépbe Szerezz poharat Aktivitásállapot Kapcsold be a gépet Fő a kávé Vegyél egy üveg Colát Szinkronizációs pont Öntsd a kávét a pohárba Nyisd ki Idd meg Végállapot Párhuzamos elágazások és csatlakozások szinkronizációs Aktivitás diagram pontok formájában írhatók le. A szinkronizációs pont az aktivitásdiagram olyan pontja, melyhez bemenő és kimenő állapotátmenetek kapcsolódnak. A kimenő állapotátmenetek mindegyike párhuzamosan végrehajtódik, ha az utolsó bemenő állapotátmenet is végrehajtódott. Így például a kávéfőzőt csak akkor szabad bekapcsolni, ha előzőleg már tettünk bele vizet és kávét is (hogy melyik történt előbb, az persze közömbös). Az aktivitásdiagramok fenti alapelemein kívül feloszthatjuk őket (tipikusan függőleges) életpályákra. Egy-egy életpálya a
szekvenciadiagramokhoz hasonlóan egy-egy objektumhoz tartozik, ily módon megmutathatjuk, hogy az aktivitásdiagram egyes aktivitásállapotait melyik objektumok hajtják végre. Mindezeken kívül az UML tartalmaz még néhány speciális jelölést, mellyel különböző események küldését és fogadását lehet szimbolizálni, ezeket azonban nem részletezzük. 3.56 Implementációs diagramok Az implementációs diagramok a megvalósított rendszer egyes kódrészleteinek kapcsolatát, és ezen kódrészletek futásidejű elhelyezkedését írják le. Két fajtájuk a komponensdiagramok (melyek a szoftver forráskódú, tárgykódú illetve futtatható moduljait és ezek kapcsolatát definiálják), és a feladatkiosztási diagramok, melyek arról adnak információt, hogy az egyes szoftvermodulok milyen Admin courses.exe Course Komponens Person Register.exe Kapcsolat A hallgatói adminisztrációs rendszer egy lehetséges komponensdiagramja hardver etőforrásokon
hajtódnak végre. 3.561 Komponensdiagramok A komponensdiagramok szoftvermodulok közötti kapcsolatokat reprezentálnak. Komponensekből és a köztük lévő kapcsolatokból épülnek föl, ahol egy komponens egy szoftvermodul-implementáció osztályt definiál (azaz a komponensekből egyedek nem hozhatók létre). A komponensek egymáshoz függőségeken keresztül kapcsolódnak, illetve megengedett az egymásba ágyazott komponensek közvetlen szemléltetése is. Az egyes implementációk az ábrán látható általános komponens jelölésen kívül további jelöléseket alkalmazhatnak olyan speciális komponensek jelölésére, mint például az eljárás vagy függvény, a csomagfej és csomagtörzs (implementáció szinten) vagy a párhuzamosan futó folyamatok feje és törzse. 3.562 Feladatkiosztási diagramok (deployment diagram) A feladatkiosztási diagramok a működő rendszerünket alkotó szoftver és hardverkomponensek közötti fizikai kapcsolatokat írják le. A
feladatkiosztási diagramok csomópontjai a számítógépes rendszerünk fizikai erőforrásait reprezentálják, azaz legtöbbször hardver komponenseket, melyek komplexitása a legegyszerűbb perifériától a legbonyolultabb számítógépes rendszerig tetszőleges lehet. A csomópontok közötti kapcsolatok az egyes elemek közötti kommunikációs csatornákat definiálják. A komponensek szoftvermodulok fizikai kódját testesítik meg, melyeknek megfeleltethetőknek kell lenniük a csomagdiagramokban megjelenő egyes csomagokkal. A komponensek közötti függőségek szintén meg kell egyezzenek a megfelelő csomagok közötti függőségekkel. A függőségek az egyes komponensek közötti kömmunikáció tényét jelzik, a függőségek iránya pedig a kommunikációs folyamat kezdeményezőjére (az aktív elemre) utal. Kommunikációs kapcsolat TCP/IP Adminisztrációs munkaállomás Adatbáziskezelõ szerver Admin courses.exe Course Csomópont Person
Jelentkezõ/lekérdezõ munkaállomás Register.exe TCP/IP Feladatkiosztási diagram A feladatkiosztási diagramok jelentősége egygépes környezetben elhanyagolható, azonban ahogy a rendszerünk elosztottá válik (pl. hálózati környezetben vagy sokprocesszoros architektúrán fut), egyre fontosabb szerepet kap szoftverünk architektúrájának a fizikai architektúrárára való optimális leképezése, melyet a feladatkiosztási diagramok hatékonyan támogathatnak. Az ábránk például egy viszonylag egyszerű kliens-szerver architektúrát definiál. Irodalom [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] G. Booch: Object Oriented Analysis and Design Addison-Wesley 1994 Tilly K, Tóth E.: Számítógépek Jegyzet, BME MMT 1992. J. Rumbaugh, M Blaha, W Premerlani, F Eddy, W Lorensen: Object Oriented Modeling and Design Prentice-Hall International Editions 1991 E. Rich, K Knight: Artificial Intelligence McGraw-Hill 1994 K. Arnold, J Gosling: The Java
Programming Language Addison-Wesley 1996 G. Gardarin, P Valduriez: Relational Databases and Knowledge Bases Addison-Wesley 1989 W. Clocksin, C Mellish: Programming in Prolog Springer 1984 Oracle 7 Server SQL Reference Oracle Coporation 1997 Oracle Power Objects Users Guide Oracle Corporation 1995. What is Objectory? Rational Software Corporation 1997 http://www.rationalcom/demos/o process/Ory41Demo/online/ory/intro/intro3htm M. Fowler, K Scott: UML Distilled: Applying the Standard Object Modelling Language Addison-Wesley 1997 UML Notation Guide V1.1 Rational Software Corporation 1997, http://www.rationalcom/docs/uml Object Constraint Language Specification Rational Software Corporation 1997, http://www.rationalcom/ docs/uml