Content extract
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) 12 relációnak is nevezni. 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('