Programozási technológiák – Jegyzet Kollár, Lajos Sterbinszky, Nóra
Created by XMLmind XSL-FO Converter.
Programozási technológiák – Jegyzet Kollár, Lajos Sterbinszky, Nóra Publication date 2014 Szerzői jog © 2014 Kollár Lajos, Sterbinszky Nóra Copyright 2014
Created by XMLmind XSL-FO Converter.
Tartalom 1. Bevezetés ........................................................................................................................................ 2 1. Objektumorientált tervezési alapelvek .................................................................................. 2 1.1. Ne ismételd önmagad (Don't Repeat Yourself, DRY) .............................................. 3 1.2. Kerüljük a felesleges bonyodalmakat (Keep It Simple Stupid, KISS) ..................... 3 1.3. Demeter törvénye (Law of Demeter) ........................................................................ 3 1.4. Vonatkozások szétválasztása (Separation of concerns) ............................................ 3 1.5. A felelősségek hozzárendelésének általános mintái (General Responsibility Assignment Software Patterns, GRASP) ............................................................................................. 4 1.6. GoF alapelvek ........................................................................................................... 5 1.6.1. Interfészre programozzunk, ne pedig implementációra! .............................. 5 1.6.2. Használjunk objektum-összetételt öröklődés helyett, ha lehet! .................... 6 1.7. SOLID alapelvek ...................................................................................................... 7 2. Haladó programnyelvi eszközök .................................................................................................. 12 1. Kivételkezelési ökölszabályok ............................................................................................ 12 1.1. A kivételek csak kivételes helyzetekre valók ......................................................... 12 1.2. Ellenőrzött és futásidejű kivételek használata ........................................................ 12 1.3. Kerüljük az ellenőrzött kivételek szükségtelen használatát .................................... 13 1.4. Favorizáljuk a szabványos kivételeket ................................................................... 14 1.5. Az absztrakciónak megfelelő kivételt dobjuk ......................................................... 15 1.6. Minden metódus minden kivételét dokumentáljuk ................................................. 21 1.7. A hiba lényegére koncentráló hibaüzenet-szövegeket írjunk .................................. 21 1.8. Törekedjünk az elemi hibaszint megőrzésére ......................................................... 21 1.9. Ne hagyjunk figyelmen kívül kivételeket ............................................................... 22 2. Állítások (assertions) ........................................................................................................... 22 3. Annotációk .......................................................................................................................... 24 3.1. Metaannotációk ...................................................................................................... 25 4. Szerződés alapú tervezés ..................................................................................................... 27 4.1. Szerződések és az öröklődés ................................................................................... 28 4.2. Contracts for Java (cofoja) ...................................................................................... 29 4.2.1. Eclipse és cofoja ......................................................................................... 30 4.2.2. Példa: verem megvalósítása szerződésekkel .............................................. 33 3. Szoftvertesztelés ........................................................................................................................... 38 1. Belövés ................................................................................................................................ 39 1.1. Belövés Eclipse-ben ............................................................................................... 39 1.1.1. Breakpoints nézet ....................................................................................... 42 1.1.2. Variables nézet ........................................................................................... 43 1.1.3. Expressions nézet ....................................................................................... 44 1.1.4. Töréspontok tulajdonságai ......................................................................... 45 1.1.5. A végrehajtás felfüggesztésének további lehetőségei ................................ 45 2. Egységtesztelés JUnit segítségével ..................................................................................... 46 2.1. Parametrizált tesztek ............................................................................................... 49 2.2. Kivételek tesztelése ................................................................................................ 50 2.3. Tesztkészletek létrehozása ...................................................................................... 51 2.4. JUnit antiminták ..................................................................................................... 51 2.4.1. Rosszul kezelt állítások .............................................................................. 52 2.4.2. Felszínes tesztlefedettség ........................................................................... 53 2.4.3. Túlbonyolított tesztek ................................................................................. 53 2.4.4. Külső függőségek ....................................................................................... 54 2.4.5. Nem várt kivételek elkapása ...................................................................... 54 2.4.6. Az éles és a tesztkód keveredése ................................................................ 55 2.4.7. Nem létező egységtesztek .......................................................................... 56 4. Tervezési minták ........................................................................................................................... 57 1. A tervezési minták leírása ................................................................................................... 57 2. GoF tervezési minták katalógusa ........................................................................................ 57 2.1. Tervezési minták rendszerezése és kapcsolataik .................................................... 59 2.2. Hogyan válasszunk tervezési mintát? ..................................................................... 60
iii Created by XMLmind XSL-FO Converter.
Programozási technológiák – Jegyzet
2.3. Hogyan használjuk a tervezési mintákat? ............................................................... 63 3. Tervezési minták alkalmazása a gyakorlatban .................................................................... 64 3.1. Létrehozási minták ................................................................................................. 64 3.1.1. Egyke (Singleton) ...................................................................................... 64 3.1.2. Gyártó minták ............................................................................................ 67 3.1.3. Építő (Builder) ........................................................................................... 69 3.1.4. Prototípus (Prototype) ................................................................................ 69 3.2. Szerkezeti minták ................................................................................................... 71 3.2.1. Illesztő (Adapter) ....................................................................................... 71 3.2.2. Összetétel (Composite) .............................................................................. 71 3.2.3. Helyettes (Proxy) ....................................................................................... 72 3.2.4. Pehelysúlyú (Flyweight) ............................................................................ 74 3.2.5. Homlokzat (Façade) ................................................................................... 74 3.2.6. Híd (Bridge) ............................................................................................... 75 3.2.7. Díszítő (Decorator) ..................................................................................... 77 3.3. Viselkedési minták ................................................................................................. 77 3.3.1. Sablonfüggvény (Template method) .......................................................... 78 3.3.2. Közvetítő (Mediator) .................................................................................. 78 3.3.3. Felelősséglánc (Chain of responsibility) .................................................... 79 3.3.4. Megfigyelő (Observer) ............................................................................... 81 3.3.5. Stratégia (Strategy) ..................................................................................... 86 3.3.6. Parancs (Command) ................................................................................... 87 3.3.7. Állapot (State) ............................................................................................ 87 3.3.8. Látogató (Visitor) ....................................................................................... 89 3.3.9. Értelmező (Interpreter) ............................................................................... 91 3.3.10. Bejáró (Iterator) ........................................................................................ 93 3.3.11. Emlékeztető (Memento) ........................................................................... 94 5. Kódújraszervezés .......................................................................................................................... 96 1. Tesztvezérelt fejlesztés ........................................................................................................ 97 2. Kódújraszervezési technikák ............................................................................................. 100 3. Kódújraszervezési eszköztámogatás ................................................................................. 101 6. Adatkezelés ................................................................................................................................. 103 1. XML dokumentumok kezelése ......................................................................................... 103 1.1. Áttekintés .............................................................................................................. 103 1.1.1. SAX .......................................................................................................... 104 1.1.2. DOM ........................................................................................................ 106 1.1.3. StAX ........................................................................................................ 109 1.1.4. A három API összehasonlítása táblázatos formában: ............................... 109 1.2. A SAX API használata ......................................................................................... 110 1.2.1. SAX feldolgozás ...................................................................................... 110 1.2.2. Az XML dokumentum érvényességének ellenőrzése (validáció) ............ 114 1.3. A DOM API használata ........................................................................................ 115 1.3.1. DOM feldolgozás ..................................................................................... 115 1.3.2. Validáció .................................................................................................. 119 1.3.3. XML dokumentum létrehozása DOM API segítségével .......................... 120 1.4. A StAX API használata ........................................................................................ 122 1.4.1. StAX feldolgozás ..................................................................................... 122 1.4.2. Validálás .................................................................................................. 125 1.4.3. XML dokumentum létrehozása StAX API segítségével .......................... 125 2. Adatbázis-kapcsolatok kezelése ........................................................................................ 127 2.1. A JDBC felépítése ................................................................................................ 128 2.2. Meghajtóprogramok ............................................................................................. 129 2.3. A JDBC API főbb elemei ..................................................................................... 131 2.4. A JDBC API használata ........................................................................................ 134 2.4.1. Kapcsolat létrehozása ............................................................................... 136 2.4.2. SQL-utasítások létrehozása ...................................................................... 138 2.4.3. Tranzakciók kezelése ............................................................................... 143 2.4.4. Lekérdezések végrehajtása ....................................................................... 146 2.4.5. Statement objektumok használata kötegelt feldolgozás esetén ................ 149 2.4.6. A kapcsolat lezárása ................................................................................. 150 iv Created by XMLmind XSL-FO Converter.
Programozási technológiák – Jegyzet
2.4.7. Kivételek kezelése .................................................................................... 7. Grafikus felhasználói felületek készítése .................................................................................... 1. Swing felületek felépítése ................................................................................................. 1.1. Komponens hozzáadása a tartalompanelhez ......................................................... 1.1.1. A JComponent osztály ............................................................................. 1.2. Szöveges komponensek ........................................................................................ 1.3. Listák és legördülő listák ...................................................................................... 1.3.1. A kiválasztási modell ............................................................................... 1.3.2. Eseménykezelők ....................................................................................... 1.4. Táblázatok ............................................................................................................ 1.4.1. Kiválasztás ............................................................................................... 1.4.2. Modellek .................................................................................................. 1.4.3. Események kezelése ................................................................................. 1.5. Elrendezéskezelők (Layout menedzserek) ............................................................ 1.5.1. A megfelelő elhelyezési stratégia kiválasztása ......................................... 1.5.2. Elrendezéskezelők működése ................................................................... 2. Tervezési minták a Swing keretrendszerben ..................................................................... Irodalomjegyzék .............................................................................................................................
v Created by XMLmind XSL-FO Converter.
150 152 152 153 154 155 155 156 157 160 161 161 163 164 166 167 167 169
Az ábrák listája 2.1. Annotációfeldolgozás beállítása ................................................................................................ 31 2.2. Annotációfeldolgozót tartalmazó jar beállítása .......................................................................... 32 2.3. Hibák a szerződésekben ............................................................................................................. 32 2.4. Futásidejű szerződésellenőrzés .................................................................................................. 33 3.1. Töréspont beállítása ................................................................................................................... 39 3.2. A beállított töréspont ................................................................................................................. 40 3.3. Belövés indítása ......................................................................................................................... 40 3.4. Debug perspektíva ..................................................................................................................... 41 3.5. A Debug nézet gyorsbillentyűi .................................................................................................. 42 3.6. Hívási lánc a Debug nézetben .................................................................................................... 42 3.7. Breakpoints nézet ...................................................................................................................... 43 3.8. Variables nézet .......................................................................................................................... 43 3.9. Változóérték megváltoztatása .................................................................................................... 43 3.10. Részletes formázó hozzáadása ................................................................................................. 44 3.11. Expressions nézet .................................................................................................................... 44 3.12. Törésponthoz tartozó feltétel megadása ................................................................................... 45 4.1. Tervezésiminta-kapcsolatok GOF2004 ..................................................................................... 60 4.2. Az egyke minta megvalósításának osztálydiagramja ................................................................. 64 4.3. A gyártó minták általános megvalósításának osztálydiagramja [OODesign] ............................ 67 4.4. A gyártófüggvény minta megvalósításának osztálydiagramja [OODesign] .............................. 67 4.5. Az elvont gyár minta megvalósításának osztálydiagramja [OODesign] ................................... 68 4.6. Az építő minta megvalósításának osztálydiagramja [OODesign] ............................................. 69 4.7. A prototípus minta megvalósításának osztálydiagramja [OODesign] ....................................... 70 4.8. Az illesztő minta megvalósításának osztálydiagramja [OODesign] .......................................... 71 4.9. Az összetétel minta megvalósításának osztálydiagramja [OODesign] ...................................... 71 4.10. A helyettes minta megvalósításának osztálydiagramja [OODesign] ....................................... 72 4.11. A pehelysúlyú minta megvalósításának osztálydiagramja [OODesign] .................................. 74 4.12. A homlokzat mintát megvalósító JFileChooser ....................................................................... 75 4.13. A híd minta megvalósításának osztálydiagramja [OODesign] ................................................ 75 4.14. A díszítő minta megvalósításának osztálydiagramja [OODesign] ........................................... 77 4.15. A sablonfüggvény minta megvalósításának osztálydiagramja [OODesign] ............................ 78 4.16. A közvetítő minta megvalósításának osztálydiagramja [OODesign] ...................................... 78 4.17. A felelősséglánc minta megvalósításának osztálydiagramja [OODesign] ............................... 80 4.18. A megfigyelő minta megvalósításának osztálydiagramja [OODesign] ................................... 81 4.19. A stratégia minta megvalósításának osztálydiagramja [OODesign] ........................................ 86 4.20. A parancs minta megvalósításának osztálydiagramja [OODesign] ......................................... 87 4.21. Az Állapot minta megvalósításának osztálydiagramja [Sourcemaking] .................................. 87 4.22. A látogató minta megvalósításának osztálydiagramja [OODesign] ........................................ 89 4.23. Az értelmező minta megvalósításának osztálydiagramja [OODesign] .................................... 91 4.24. A bejáró megvalósításának osztálydiagramja [OODesign] ..................................................... 93 4.25. Az emlékeztető minta megvalósításának osztálydiagramja [OODesign] ................................ 94 5.1. Egy bowling játék eredménye .................................................................................................... 98 5.2. A tesztvezérelt fejlesztés ritmusa [KACZANOWSKI2013] ...................................................... 99 5.3. A tesztvezérelt fejlesztés ritmusa részletezettebben [KACZANOWSKI2013] ......................... 99 5.4. Az Eclipse Refactor menüje .................................................................................................... 101 6.1. Az XML dokumentumok SAX stílusú feldolgozásának sematikus modellje .......................... 104 6.2. A SAX API elemei [JAXPTutorial] ........................................................................................ 105 6.3. DOM modulok ......................................................................................................................... 107 6.4. DOM API interfészei ............................................................................................................... 107 6.5. Az XML dokumentumok DOM stílusú feldolgozásának sematikus modellje ......................... 108 6.6. A DOM API elemei [JAXPTutorial] ....................................................................................... 108 6.7. Az XSLT API elemei [JAXPTutorial] ..................................................................................... 120 6.8. Kapcsolódás különféle adatforrásokhoz .................................................................................. 128 6.9. Kétrétegű feldolgozási modell [JDBCTutorial] ....................................................................... 128 6.10. Háromrétegű feldolgozási modell [JDBCTutorial] ................................................................ 128 6.11. 4-es típusú JDBC-driver ........................................................................................................ 130
vi Created by XMLmind XSL-FO Converter.
Programozási technológiák – Jegyzet
6.12. A fő JDBC osztályok és interfészek és üzeneteik .................................................................. 6.13. A java.sql csomag főbb típusai a közöttük lévő kapcsolatokkal ............................................ 6.14. A JDBC API főbb interfészei és használatának lépései ......................................................... 6.15. Kapcsolat létrehozása DataSource segítségével ................................................................... 6.16. Connection pooling ................................................................................................................ 6.17. Elosztott tranzakciók támogatása ........................................................................................... 6.18. Exploits of a mom ................................................................................................................. 7.1. Példa komponenshierarchiára [SwingTutorial] ....................................................................... 7.2. Az Eclipse WindowBuilder GUI programozást segítő palettája .............................................. 7.3. Szöveges komponensek osztályozása [SwingTutorial] ........................................................... 7.4. Egyszeres kiválasztás ............................................................................................................... 7.5. Egyszeres intervallum kiválasztás ........................................................................................... 7.6. Többszörös intervallum kiválasztás ......................................................................................... 7.7. Eseménykezelők [SwingTutorial] ............................................................................................ 7.8. Táblázat beágyazott ComboBox objektumokkal ..................................................................... 7.9. Táblázat és táblamodelljének kapcsolata [SwingTutorial] ...................................................... 7.10. BorderLayout[SwingTutorial] ............................................................................................. 7.11. BoxLayout[SwingTutorial] ................................................................................................... 7.12. CardLayout [SwingTutorial] ................................................................................................. 7.13. FlowLayout[SwingTutorial] ................................................................................................. 7.14. GridLayout[SwingTutorial] ................................................................................................. 7.15. GridBagLayout[SwingTutorial] ........................................................................................... 7.16. GroupLayout[SwingTutorial] ............................................................................................... 7.17. SpringLayout[SwingTutorial] ............................................................................................. 7.18. Információt megjelenítő JOptionPane komponens ..............................................................
vii Created by XMLmind XSL-FO Converter.
131 134 134 136 137 137 139 152 153 155 156 156 157 157 160 161 164 164 164 165 165 165 166 166 167
A táblázatok listája 1.1. Felelősségek hozzárendelésének általános mintái ....................................................................... 4 2.1. A cofoja annotációi .................................................................................................................... 29 2.2. A cofoja pszeudováltozói .......................................................................................................... 29 2.3. Az annotációfeldolgozó számára beállítandó kulcs–érték párok ............................................... 31 3.1. A Debug perspektíva gyorsbillentyűi ........................................................................................ 41 3.2. JUnit annotációk ........................................................................................................................ 47 3.3. Az Assert osztály metódusai .................................................................................................... 49 4.1. Tervezési minták leírására szolgáló sablon elemei .................................................................... 57 4.2. A GoF 23 tervezési mintájának katalógusa ............................................................................... 58 4.3. Tervezési minták osztályozása ................................................................................................... 59 4.4. A tervezési minták által megengedett változtatható elemek ...................................................... 62 6.1. XML-feldolgozási modellek jellemzői .................................................................................... 110 6.2. XML dokumentumok csomópontjai ........................................................................................ 115 6.3. Adatbázis-kezelő rendszerek JDBC-drivereinek elérhetősége ................................................ 130 6.4. Tárolt alprogramok paraméterátadási módjai .......................................................................... 142 7.1. Komponensek és figyelőik [SwingTutorial] ............................................................................ 159
viii Created by XMLmind XSL-FO Converter.
A példák listája 2.1. Jelölőannotáció-típus megadása ................................................................................................ 25 2.2. Egyelemű annotációtípus megadása .......................................................................................... 25 2.3. Többelemű annotációtípus megadása tömbtípusú elemmel és alapértelmezett értékekkel ........ 25 2.4. Összetett annotációtípus megadása ............................................................................................ 25 2.5. Egyelemű annotációtípus kiegészítése metaannotációkkal ........................................................ 26 2.6. Többelemű annotációtípus kiegészítése metaannotációkkal ...................................................... 26 6.1. Címek adatainak adatbázisból Address-listába olvasása ........................................................ 135 6.2. Az SQL-befecskendezéses támadás kivédése .......................................................................... 140 6.3. Paraméter néküli tárolt függvény meghívása ........................................................................... 143 6.4. Egy IN és egy OUT paraméterrel rendelkező kétparaméteres tárolt eljárás meghívása .......... 143 6.5. Két IN és egy INOUT paraméterrel rendelkező tárolt eljárás meghívása ................................ 143 6.6. Kötegelt adatbázis-műveletek végrehajtása ............................................................................. 149
ix Created by XMLmind XSL-FO Converter.
Végszó
1 Created by XMLmind XSL-FO Converter.
1. fejezet - Bevezetés Mitől válhat egy kezdő programozó jó programozóvá? Attól, hogy fejből fújja egy adott programozási nyelv szintaktikai szabályait? Aligha. Attól, hogy nagy részletességgel ismeri különböző programkönyvtárak alkalmazásprogramozói interfészének (vagyis API-jának) az interfészeit, osztályait, metódusait? Nem valószínű, hogy mindez elegendő volna a jó programozóvá váláshoz. Egy (természetesen nagyon fontos) dolog ugyanis egy programozási nyelv szintakszisának az elsajátítása, de csak attól még, hogy lefordítható programokat ír valaki, nem válik automatikusan jó programozóvá. Ahhoz, hogy a jó programozó válás útján elinduljon valaki, mindenképpen szükség van némi elhivatottságra, hogy jó programozóvá akarjon válni az illető! Ez talán a legfontosabb összetevő. Ha ez megvan, már „csak” rengeteg gyakorlásra és tanulásra van szükség, de hát egy elhivatott ember számára ez persze nem okoz gondot. A tanulási folyamatot már gyerekkorban is minta alapon végezzük: szüleinktől, környezetünktől ellessük a legjobb(nak vélt) fogásokat azért, hogy a későbbiekben ezt a megszerzett tudást újrahasznosítva a legkülönbözőbb élethelyzetek leküzdésében segítségünkre legyenek. A mintákon keresztül mintegy szemléletmódot is tanulunk, amelyet aztán sokszor mélyen és hosszú távon magunkkal hordozunk.. A programozó tanulási folyamata szintén nagymértékben minta alapú: az évek során felhalmazódott tudást és legjobb gyakorlatokat követve készítjük programjainkat.Egy kezdő, de eléggé elhivatott programozó számára persze nagyon fontos, hogy megismerje ezeket a mintákat. E jegyzet elsősorban a Debreceni Egyetem Informatikai Karának másodéves programtervező informatikus alapszakos hallgatói számára íródott, akik ekkorra már remélhetőleg megismerkedtek legalább két programozási nyelv alapelemeivel, amelyek közül az egyik a Java. Egy bevezető programozási kurzus célja általában a nyelvi alapelemek megismertetése és begyakoroltatása, az alapvető vezérlési szerkezetek és algoritmusok, valamint az API legfontosabb elemeinek a bemutatása, azonban ennél több nem nagyon fér a szűkös időkeretbe. Holott, mint említettük, fontos a legjobb gyakorlatok, a minták, a megfelelő szemléletmód kialakítása. Talán fontosabb is, mint a konkrét eszközrendszer. Éppen ezért a jegyzet olvasója vissza-visszatérően különféle alapelvekbe és mintákba fog botlani, amelyek megismerése, megértése és alkalmazása lehetőleg segítségére lesz az úton. Jó utat!
1. Objektumorientált tervezési alapelvek Az objektumorientált tervezés alapelvei (object-oriented design principles) a későbbiekben tárgyalásra kerülő tervezési mintáknál magasabb absztrakciós szinten írják le, milyen a „jó” program. A tervezési minták ezeket az alapelveket valósítják meg szintén még egy elég magas absztrakciós szinten (éppen ezért a későbbiekben ezen elvek egyikére-másikára vissza is fogunk utalni). A tervezési mintákat megvalósító programokat az alapelvek manifesztálódásaként tekinthetjük. Az ebben a szakaszban leírt alapelveket természetesen úgy is alkalmazthatjuk, hogy nem ismerjük (vagy csak egyszerűen nem alkalmazzuk) a tervezési mintákat. Az objektumorientált tervezési alapelvek abban nyújtanak segítséget, hogy több, általában egyenértékű programozói eszköz (például öröklődés és objektum-összetétel) közül kiválasszuk azt, amely jobb kódot eredményez. A jóság természetesen relatív fogalom, azonban az elmúlt évtizedekben kialakultak olyan általános jellemzők, amelyek alapján egyik-másik megoldásra rámondható, hogy jobb a többinél. Ilyen általános jósági jellemző, ha a kód rugalmasan bővíthető, újrafelhasználható komponensekből áll és könnyen érthető más programozók számára is. A tervezési alapelvek abban segítenek, hogy ne essünk például abba a hibába, hogy egy osztályba kódolunk mindent, hogy élvezzük a mezők, mint globális változók programozást gyorsító hatását. A tapasztalat az, hogy lehet programozni ezen alapelvek ismerete nélkül, vagy akár tudatos megszegésével, csak nem érdemes. Ha rugalmatlan, nehezen változtatható, karbantartható programot írunk, akkor a jövőbeli énünk (és kollégáink) életét keserítjük meg, hiszen ha egy változtatást kell elvégezni, az ezáltal nehézkessé válhat. Inkább érdemes a jelenben több időt rászánni a fejlesztésre, és biztosítani, hogy a jövőben könnyebb legyen a változások kezelése. Ezt biztosítja számunkra az alapelvek betartása. A további alszakaszokban néhány széles körben ismert és elterjedt programozási, programtervezési alapelvet mutatunk röviden be. Ezek között természetesen vannak egymásra hasonlító elvek is, de hát egy jó alapelv jó alapelv marad. 2 Created by XMLmind XSL-FO Converter.
Bevezetés
1.1. Ne ismételd önmagad (Don't Repeat Yourself, DRY) A „Ne ismételd önmagad!” alapelv először Andy Hunt és Dave Thomas [PRAGPROG1999] könyvében jelent meg, ahol elég széles körben alkalmazandó irányelvként határozták meg. Alkalmazandó nem csak a programkódra, de az adatbázissémákra, teszttervekre, sőt, még a dokumentációra is. Az alapelv röviden úgy fogalmazható meg, hogy „egy rendszeren belül a tudás minden darabkájának egyetlen, egyértelmű és megbízható reprezentációval kell rendelkeznie”.1A DRY alapelv sikeres alkalmazása esetén a rendszer egy elemének a módosítása nem igényli a rendszer más, a módosított elemmel kapcsolatan nem lévő részek megváltoztatását. Fontos, hogy fel tudjuk ismerni az ismétlődő részeket, és valamilyen (az ismétlődés jellegétől függő) absztrakció alkalmazásával szüntessük meg azokat. A legegyszerűbb programozási példa erre az ismétlődő kódrészletek önálló metódusba történő kiemelése (procedurális absztrakció), majd az ismétlődő kódrészek metódushívásra történő lecserélése. A DRY alapelv megsértését angol betűszóval gyakran WET-nek („Write Everything Twice”, vagy „We Enjoy Typing”) nevezik.
1.2. Kerüljük a felesleges bonyodalmakat (Keep It Simple Stupid, KISS) Ez az irányelv az 1960-as években az amerikai haditengerészetnél született meg, és lényege, hogy tiszta, könnyen érthető megoldásokra törekedjünk. Albert Einstein szavaival élve: „egyszerűsítsük a dolgokat, amennyire csak lehet, de ne jobban”. 2 A KISS alapelvet programozói tevékenységre értve azt mondhatnánk, hogy tartsd a kódodat pofonegyszerű állapotban, vagyis, éppen annyit valósítsunk meg, amennyire szükség van, és ne bonyolítsuk el feleslegesen a dolgokat.
1.3. Demeter törvénye (Law of Demeter) Demeter törvénye röviden úgy fogalmazható meg, hogy „ne beszélgess idegenekkel”! Ennek a törvénynek a betartásával könnyebben karbantartható és adaptálható szoftverhez jutunk, hiszen az objektumok kevésbé függnek más objektumok belső felépítésétől, ezért az objektumok felépítése sokkal könnyebben módosítható, akár a hívó szerkezetének módosítása nélkül is. Tegyük fel, hogy az A objektum igénybe veheti a B objektum egy szolgáltatását (meghívja egy metódusát), de az A objektum nem érheti el a B objektumon keresztül egy C objektum szolgáltatásait. Ez azt jelentené, hogy az A objektumnak implicit módon a szükségesnél jobban kell ismernie a B objektum belső felépítését. A megoldás a B objektum felépítésének módosítása oly módon, hogy az A objektum közvetlenül hívja B objektumot, és a B objektum intézi a szükséges hívásokat a megfelelő alkomponensekhez. Ha a törvényt követjük, kizárólag B objektum ismeri saját belső felépítését. Formálisan ezt azt jelenti, hogy a törvény betartása esetén egy o objektum egy m metódusa csak az alábbi objektumok metódusait hívhatja: • magáét o-ét, • m paramétereiét, • bármely, m-en belül létrehozott/példányosított objektumét, • o közvetlen kompnensobjektumaiét, illetve • az o által az m hatáskörében hozzáférhető globális változóiét. Másképpen ezt egypontszabályként is nevezhetnénk, hiszen amíg az o.m() hívás megfelel a törvénynek, az o.a.p() vagy éppen az o.m().p() nem (hiszen ezek az o szempontjából idegenek). Ezt a szemléletmódot fejezi ki a kutyasétáltatás analógiája is: ha sétáltatni vinnénk a kutyát, nem közvetlenül a lábainak mondjuk meg, hogy sétáljanak, hanem magának a kutyának, amely a saját felépítésének ismeretében utasítja erre a lábait. Vagyis itt egy delegáció történik.
1.4. Vonatkozások szétválasztása (Separation of concerns) 1 2
„Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” „Make things as simple as possible, but not simpler.”
3 Created by XMLmind XSL-FO Converter.
Bevezetés
A vonatkozások szétválasztásánk alapelve szerint egy programot lehetőleg úgy bontsunk fel különféle részekre, hogy az egyes részek külön-külön vonatkozásokat fedjenek le. Egy vonatkozás (concern) olyan információk összessége, amelyek befolyásolják a programkódot. Ez alatt olyan felelősségi köröket értünk, amelyek akár az alkalmazás funkciójához is kötődhetnek, de attól függetlenek is lehetnek. Utóbbira példa lehet egy gyorsítótár kezelése, vagy akár a naplózás, amelyre, mint feladatra, különféle funkciókat megvalósító programelemeknek is szüksége van, de az, hogy maga a naplózás miként, és legfőképpen hová történjen, teljesen független a funkciótól (ezért egy önálló vonatkozást alkot). A vonatkozások megfelelő szétválasztásával szoftverelemeinket úgy tudjuk kialakítani, hogy a lehető legkisebb átfedés alakuljon ki közöttük. Ez kapcsolatba hozható a DRY alapelvvel is, hiszen ezáltal az egyes vonatkozások elemei különállóan kezelhetőek, és például a kívánt naplózási szint beállítása is egy helyen elvégezhető az egész alkalmazás vonatkozásában, ahelyett, hogy az egyes funkciók naplózásánál kelljen azt rendre szabályozni. Ráadásul, ha egy darab kódnak nincs világosan meghatározott feladata, akkor nehéz lesz megérteni, használni és adott esetben javítani vagy bővíteni, ezért ennek az elvnek az alkalmazása segíthet egy letisztult gondolkodás kialakításában is. Az aspektusorientált programozás is a vonatkozások különválasztásának elvén épül fel. Az úgynevezett keresztező vonatkozásokat (vagyis a több osztályt is érintő vonatkozásokat, mint amilyen a fenti példában a naplózás), kiemeljük egy úgynevezett aspektusba, amely bármely osztályhoz hozzákapcsolható.
1.5. A felelősségek hozzárendelésének általános mintái (General Responsibility Assignment Software Patterns, GRASP) A GRASP alapelvek (ahogyan azt az angol nyelvű elnevezés is mutatja) a felelősségek hozzárendelésének általános mintáit határozzák meg. Először leírásra Craig Larman könyvében [LARMAN2004] került, és tulajdonképpen minták egy gyűjteményéről van szó. Az ide tartozó mintákat és rövid leírásukat az alábbi táblázat foglalja össze:
1.1. táblázat - Felelősségek hozzárendelésének általános mintái Minta
Leírás
Információs szakértő (Information expert)
A felelősségeket mindig az információs szakértőhöz rendeljük,vagyis ahhoz az osztályhoz, amely birtokában van a felelősség megvalósításához szükséges információknak.
Létrehozó (Creator)
Az A osztály egy példányának létrehozását bízzuk a B osztályra, ha az alábbiak valamelyike igaz: 1. B tartalmazza A-t 2. B aggregálja A-t 3. B tartalmazza az A inicializálásához szükséges adatokat (vagyis B az A létrehozása szempontjából információs szakértő) 4. B nyilvántartja A példányait 5. B szorosan használja A-t Természetesen a létrehozás szempontjából a későbbiekben tárgyalásra kerülő létrehozási minták is jó alternatívát biztosítanak.
Vezérlő (Controller)
A rendszer eseményeinek kezelésének felelősségét egy olyan osztályhoz rendeljük, amely vagy a teljes rendszert/alrendszert/eszközt reprezentálja (ez tulajdonképpen a később tárgyalandó Homlokzat 4 Created by XMLmind XSL-FO Converter.
Bevezetés
Leírás
Minta
(Façade) tervezési minta alkalmazása), vagy egy olyan forgatókönyvet reprezentál, amelyben az esemény bekövetkezett. Laza csatolás (Low/loose coupling)
A csatolás annak mértéke, hogy az egyes komponensek mennyire szorosan kötődnek más komponensekhez, illetve mennyi információ birtokában vannak más komponensekről. A felelősségeket úgy rendeljük az objektumainkhoz, hogy továbbra is lazán csatoltak maradjanak.
Nagyfokú kohézió (High cohesion)
A kohézió annak mértéke, hogy egyetlen komponens felelősségei mennyire szorosan kapcsolódnak egymáshoz. A felelősségeket úgy rendeljük az objektumainkhoz, hogy fenntartsuk a magas kohéziót.
Polimorfizmus (Polymorphism)
Amennyiben összetartozó viselkedések típustól (vagyis osztálytól) függően változnak, a viselkedést leíró felelősséget polimorf műveletek segítségével rendeljük azon típusokhoz, amelyeknél a viselkedés változik.
Pusztán gyártás (Pure fabrication)
Néha az információs szakértő alkalmazása a kohézió csökkenését, illetve a kapcsolódás szorosabbá válását vonja magával, ami nem szerencsés. Ilyenkor hozzunk létre egy mesterséges osztályt, amely nem a problématér valamely fogalmát tükrözi, hanem a célja csupán a magas kohézió és a laza kapcsolódás fenntartása és az újrafelhasználhatóság elősegítése.
Indirekció (Indirection)
A kapcsolódáson lazítani úgy tudunk, hogy két (túlságosan) erősen kapcsolódó objektum közé bevezetünk egy köztes objektumot, amely indirekció segítségével az objektumok között mediátor szerepet tölt be, így azok nem közvetlenül állnak majd kapcsolatban egymással.
Védett változatok (Protected variations)
Azonosítsuk az előre látható változások által érintett elemeket, és csomagoljuk be őket egy stabil interfész mögé, így a polimorfizmus alkalmazásával az interfészhez különféle implementációkat társíthatunk.
1.6. GoF alapelvek A GoF alapelvek a nevüket annak a könyvnek a szerzőiről kapták, amelyben először leírták őket. Ezt a könyvet [GOF2004] a Gang Of Four (GoF) néven elhíresült szerzőnégyes írta, és sok úgynevezett tervezési minta mellett két alapvető objektorientált tervezési alapelvet is megfogalmaztak.
1.6.1. Interfészre programozzunk, ne pedig implementációra! Az osztályok közötti öröklődés alapjában véve csak egy módszer, amivel a szülőosztály szolgáltatásait kibővítjük. Segítségével gyorsan hozhatunk létre új objektumokat egy régi alapján. Különösebb munka nélkül kaphatunk új megvalósításokat, egyszerűen leszármaztatva a létező osztályokból, amire szükségünk van. Mindazonáltal az implementáció újrafelhasználása még nem minden. Az öröklődés azon tulajdonsága, hogy az azonos interfésszel rendelkező objektumok egy családját képes meghatározni (általában egy absztrakt osztályból örökölve, vagy egy interfészt implementálva) szintén fontos, hiszen ez a polimorf működés alapja. Öröklődés során minden, az absztrakt műveletek konkretizálását végző osztály osztozik az interfészen, vagyis az interfészen változtatni az alosztályok illetve implementációs osztályok nem tudnak (még elrejteni sem tudják az öröklött/kapott műveleteket!). A konkrét osztályok „mindössze” új műveleteket adhatnak hozzá, illetve a meglévőket felüldefiniálhatják. Így viszont minden alosztály tud majd válaszolni az interfésznek megfelelő 5 Created by XMLmind XSL-FO Converter.
Bevezetés
kérelmekre, amik lehetővé teszik, hogy a kliensek függetlenek legyenek az általuk felhasznált objektumok tényleges típusától (nem is kell őket ismerniük, így ez a lazán csatolás irányába tett nagy lépésként is értelmezhető), amíg az interfész megfelel a kliens által vártnak. A kliens így csak az interfésztől függ, és nem az implementációtól, vagyis anélkül cserélhetőek le a szolgáltatást nyújtó konkrét osztályok az interfész változatlanul hagyása mellett, hogy az a kliensekre bármiféle kihatással lenne.
1.6.2. Használjunk objektum-összetételt öröklődés helyett, ha lehet! Az objektumorientált rendszerekben az újrafelhasználás két leggyakrabban használt módszere az öröklődés és az objektum-összetétel (objektumkompozíció).
Megjegyzés Az objektumkompozíció egy olyan viszony, amely két objektum között egy nagyon szoros rész–egész kapcsolatot ír le. Az egész lesz a felelős a rész létrehozásáért (lásd a Létrehozó GRASP mintát) és megszüntetéséért is, ugyanis egy kompozíciós kapcsolatban a rész élettartama függ az egészétől: ha az egész megszűnik létezni, törlődik a rész is. Példa erre egy számla és számlatételeinek viszonya: egy számla számlatételeket tartalmaz (rész–egész viszony), azonban egy számlatétel csak egy számlához kötődően létezik, vagyis ha egy számla törlésre kerül, a részeit alkotó számlatételek szintén megszűnnek létezni. Ahogy azt már említettük, az öröklődés arra ad módot, hogy egy osztály megvalósítását egy másik osztály segítségével határozzuk meg. Az alosztályokon keresztül történő újrafelhasználást fehérdobozos újrafelhasználásnak nevezzük. A „fehér doboz” itt a láthatóságra utal: az öröklődéssel az alosztályok gyakran látják a szülőosztály belső részeit. Az objektum-összetétel az öröklődés alternatívája. Itt az új szolgáltatások úgy jönnek létre, hogy kisebb részekből építünk fel objektumokat, hogy több szolgáltatással rendelkezzenek. Az objektum-összetételnél az összeépített objektumoknak jól meghatározott interfésszel kell rendelkezniük. Az ilyen újrafelhasználást feketedobozos újrafelhasználásnak nevezzük, mert az objektumok belső részei láthatatlanok. Az objektumok „fekete dobozokként” jelennek meg. Az öröklődésnek és az összetételnek egyaránt megvannak a maga előnyei és hátrányai. Az öröklődés statikusan, fordítási időben történik, és használata egyértelmű, mivel közvetlenül a programnyelv támogatja; továbbá az öröklődés könnyebbé teszi az újrahasznosított megvalósítás módosítását is. Ha egy alosztály felülírja a műveletek némelyikét, de nem mindet, akkor a leszármazottak műveleteit is megváltoztathatja, feltéve, hogy azok a felüldefiniált műveleteket hívják. De az öröklődésnek vannak hátrányai is. Először is, a szülőosztályoktól örökölt implementációt futásidőben nem változtathatjuk meg, mivel az öröklődés már fordításkor eldől. Másodszor – és ez sokkal rosszabb –, a szülőosztályok gyakran alosztályaik fizikai megjelenését is meghatározzák, legalább részben. Mivel az öröklődés megengedi, hogy egy alosztály betekintést nyerjen szülője megvalósításába, gyakran mondják, hogy „az öröklődés megszegi az egységbe zárás szabályát”. Az alosztály megvalósítása annyira kötődik a szülőosztály megvalósításához (szoros kapcsolat van lazán csatoltság helyett), hogy a szülő megvalósításában a legkisebb változtatás is az alosztály változását vonja maga után. Az implementációs függőségek gondot okozhatnak az alosztályok újrafelhasználásánál. Ha az örökölt megvalósítás bármely szempontból nem felel meg az új feladatnak, arra kényszerülünk, hogy újraírjuk, vagy valami megfelelőbbel helyettesítsük a szülőosztályt. Ez a függőség korlátozza a rugalmasságot, és végül az újrafelhasználhatóságot. Ezt úgy orvosolhatjuk, ha csak absztrakt osztályoktól öröklünk, mivel azok általában egyáltalán nem tartalmaznak megvalósításra vonatkozó részeket (vagy ha mégis, akkor csak keveset). Az objektum-összetétel dinamikusan, futásidőben történik, olyan objektumokon keresztül, amelyek hivatkozásokat szereznek más objektumokra. Az összetételhez szükséges, hogy az objektumok figyelembe vegyék egymás interfészét, amihez gondosan megtervezett interfészekre van szükség, amelyek lehetővé teszik, hogy az objektumokat sok másikkal együtt használjuk. A módszer előnye viszont, hogy mivel az objektumokat csak interfészükön keresztül érhetjük el, nem szegjük meg az egységbe zárás elvét. Bármely objektumot lecserélhetünk egy másikra futásidőben, amíg a típusaik egyeznek. Továbbá, mivel az objektumok megvalósítása interfészek segítségével épül fel, sokkal kevesebb lesz a megvalósítási függőség. Az objektum-összetételnek még egy hatása van a rendszer szerkezetére: az öröklődéssel szemben segít az osztályok egységbe zárásában és abban, hogy azok egy feladatra összpontosíthassanak (egyszeres felelősség 6 Created by XMLmind XSL-FO Converter.
Bevezetés
elve). Az osztályok és osztályhierarchiák kicsik maradnak, és kevésbé valószínű, hogy kezelhetetlen szörnyekké duzzadnak. Másrészről az objektum-összetételen alapuló tervezés alkalmazása során több objektumunk lesz (még ha osztályunk kevesebb is), és a rendszer viselkedése ezek kapcsolataitól függ majd, nem pedig egyetlen osztály határozza meg.
1.7. SOLID alapelvek A SOLID alapelvek egy angol nyelvű betűszó nyomán kapták a nevüket. Öt alapelvről van itt szó: • Egyszeres felelősség elve (Single Responsibility Principle, SRP) Az egyszeres felelősség elve azt mondja ki, hogy minden osztálynak egyetlen felelősséget kell lefednie, de azt teljes egészében. Eredeti angol megfogalmazása: “A class should have only one reason to change”, azaz „egy osztálynak csak egy oka legyen a változásra”. Ha egy kódnak több oka is van arra, hogy megváltozzon, az arra utal, hogy a felelősségek és vonatkozások szétválasztása nem megfelelően történt meg. Ilyenkor addig kell alakítanunk az osztályunkat – azonosítva és kimozgatva belőle a „felesleges” felelősségeket, hozzárendelve azokat más osztályokhoz, amelyeket talán épp emiatt hozunk létre –, amíg el nem érjük, hogy csak egyetlen felelősséget tartalmazzon. A vonatkozások szétválasztásának elve és az egyszeres felelősség elve szorosan összefügg. Így a felelősségek befoglaló halmazát alkotják a vonatkozások. Ideális esetben minden vonatkozás egy felelősségből áll, mégpedig a fő funkció felelősségéből. Azonban egy felelősségben gyakran több vonatkozás is keveredik. A vonatkozások szétválasztásának elve azt nem mondja ki, hogy egy felelősség csak egy vonatkozásból állhat, hanem csak annyit követel meg, hogy a vonatkozásokat el kell különíteni egymástól, vagyis tisztán felismerhetőnek kell lennie, ha több vonatkozás is jelen van. Az egyszeres felelősség elvének megfelelő alkalmazásával olyan kódhoz jutunk, amelyet tesztelni is könnyebb, ráadásul a hibakeresés is egyszerűbbé válik. • Nyitva zárt alapelv (Open-Closed Principle, OCP) A nyitva zárt elvet eredetileg Bertrand Meyer fogalmazta meg, kimondva, hogy a program forráskódja legyen nyitott a bővítésre, de zárt a módosításra. Eredeti angol megfogalmazása: “Classes should be open for extension, but closed for modification”. Egy kicsit szűkebb értelmezésben úgy fogalmazhatnánk, hogy az osztályhierarchiánk legyen nyitott a bővítésre, de zárt a módosításra. Ez az jelenti, hogy új alosztályt vagy egy új metódust nyugodtan felvehetünk, de meglévőt nem írhatunk felül. Ennek azért van értelme, mert ha már van egy működő, letesztelt, kiforrott metódusunk és azt megváltoztatjuk, akkor több hátrányos dolog is történhet: a változás miatt az eddig működő ágak hibássá válhatnak, illetve a változás miatt a tőle implementációs függőségben lévő kódrészek megváltoztatására is szükség lehet. Kódunkban az if ... else if szerkezet jelenléte gyakran arra utalhat, hogy nem tartottuk be ezt az elvet, ezért a változtatást úgy vezettük be a kódunkba, hogy újabb ágat adtunk a meglévők mellé (vagyis megsértettük a módosításra vonatkozó zártság követelményét). Ez például egy árak számítását végző program esetében fordulhat elő, ahol különféle feltételektől függően eltérő árképzési stratégiára van szükség. Ha új árszámítási módszert kell megvalósítanunk, akkor egy újabb ág helyett a Védett változatok nevű GRASP minta alkalmazásával, absztrakt osztály segítségével, egy interfészt hozhatnánk létre az árképzés miatt, és különböző alosztályok segítségével a polimorf viselkedést kihasználva implementálhatóak a konkrét árképzési stratégiák. • Liskov-féle helyettesíthetőségi alapelv (Liskov's Substitution Principle, LSP) A Liskov-féle helyettesíthetőségi alapelv (nevét kidolgozója, Barbara Liskov nyomán kapta) azt írja elő, hogy a leszármazott osztályok példányainak úgy kell viselkedniük, mint az ősosztály példányainak, vagyis a program viselkedése nem változhat meg attól, hogy az ősosztály egy példánya helyett a jövőben valamelyik gyermekosztályának egy példányát használjuk. Ez elsőre meglehetősen banálisan hangzik. A kivételek példáján keresztül azonban rögtön érthetővé válik, milyen problémák léphetnek fel, ha ezt az elvet megsértjük. Amennyiben az ősosztály egy metódusának végrehajtásakor nem vált ki kivételt, akkor az összes alosztálynak is tartania kell magát ehhez a szabályhoz. Amennyiben az egyik alosztály eljárása mégis kivételt váltana ki, akkor ez gondot okozna minden olyan helyen, ahol egy ősosztály típusú objektumot használunk, mert ott a kliens nincs erre felkészülve.
7 Created by XMLmind XSL-FO Converter.
Bevezetés
Általánosabban úgy is ki lehetne fejezni ezt az elvet, hogy az alosztálynak csak kibővítenie szabad az ős funkcionalitását, de korlátoznia nem. Amennyiben például egy metódus az ősosztályban egy adott értéktartományon operál, akkor az altípus öröklött metódusa legalább ezen az értéktartományon kell, hogy működjön. Az értéktartomány kibővítése engedélyezett, de semmiképpen sem szabad korlátozni azt! A Liskov-féle helyettesíthetőségi alapelv tehát elsősorban arra hívja fel a figyelmünket, hogy alaposan gondoljuk át az öröklődést. Hacsak lehet, érdemes lehet a kompozíciót előtérbe helyezni az öröklődéssel szemben (lásd a megfelelő GoF alapelvet is). Az öröklődésnél tehát mindenképpen el kell gondolkodni a viselkedésről is, nem csak a struktúráról, vagyis amikor arról döntünk, hogy két osztály között fennáll-e az öröklődési viszony, azt is vizsgáljuk meg, hogy a szerkezeten túlmenően a viselkedésről is minden esetben elmondható-e, hogy az alosztály példánya szuperosztályának példánya helyett helyt tud állni. • Interfészek szétválasztásának elve (Interface Segregation Principle, ISP) Az interfészek szétválasztásának elve azt mondja ki, hogy egy sok szolgáltatást nyújtó osztály fölé el kell helyezni interfészeket, hogy minden kliens, amely használja az osztály szolgáltatásait, csak azokat a metódusokat lássa, amelyeket ténylegesen használ. Eredeti angol megfogalmazása: “No client should be forced to depend on methods it does not use”, azaz „Egyetlen kliens se legyen rákényszerítve arra, hogy olyan metódusoktól függjön, amelyeket nem is használ”. Minél kevesebb dolog található az interfészben, annál lazább a csatolás (coupling) a két komponens között. Gondoljunk csak bele, mit tennénk, ha egy olyan dugaszt kellene terveznünk, amelyikkel egy monitort egy számítógépre lehet csatlakoztatni. Például úgy dönthetnénk, hogy minden jelet, amely egy számítógépben felléphet, egy dugaszon keresztül rendelkezésre bocsátunk. Ennek ugyan lesz pár száz lába, de maximálisan rugalmas lesz. Sajnálatos módon ezzel a csatolás is maximálissá válik (ugyanis egy jelfajta megjelenésekor az egész dugaszt újratervezhetjük, még akkor is, ha a monitor ilyen típusú jelet nem is bocsát ki). A dugasz példáján keresztül nyilvánvaló, hogy egy monitor-összeköttetésnek csak azokat a jeleket kell tartalmaznia, amelyek egy kép ábrázolásához szükségesek. Ugyanez a helyzet a szoftverinterfészeknél is. Ezeknek is a lehető legkisebbnek kellene lenniük, hogy elkerüljük a felesleges csatolást. Ráadásul, a monitordugaszhoz hasonlóan az interfésznek kohézívnek kell lennie. Csak olyan dolgokat kellene tartalmaznia, amelyek szorosan összefüggnek. Ha meg is változik például az egér jeleinek átvitelére szolgáló csatoló, az csak azokat a klienseket érinti, amelyek ezt használják (jelen esetben csak az egeret). • Függőséginverzió alapelve (Dependency Inversion Principle, DIP) A függőséginverzió elve azt mondja ki, hogy a magas szintű komponensek ne függjenek alacsony szintű implementációs részleteket kidolgozó osztályoktól, hanem épp fordítva, a magas absztrakciós szinten álló komponensektől függjenek az alacsony absztrakciós szinten álló modulok. Eredeti angol megfogalmazása: “High-level modules should not depend on low-level modules. Both should depend on abstractions”. Azaz: „a magas szintű modulok ne függjenek az alacsony szintű moduloktól. Mindkettő absztrakcióktól függjön”. Amennyiben egy magas szintű osztály közvetlenül használ fel egy alacsony szintűt, akkor kettejük közt egy erős csatolás jön létre. Legkésőbb akkor ütközünk nehézségekbe, amikor megpróbáljuk tesztelni a magas szintű osztályt. Emiatt a magas szintű osztálynak egy interfésztől kellene függenie, amit aztán az alacsony szintű osztály implementál. Az alacsony szintű komponensek újrafelhasználása az úgynevezett osztálykönyvtárak (library) segítségével jól megoldott. Azokat a metódusokat illetve osztályokat szokás osztálykönyvtárba gyűjteni, amelyekre gyakran szükségünk van. A rendszer logikáját leíró magas szintű komponensek azonban általában nehézkesen újrafelhasználhatók, mert sok függőséggel rendelkeznek. Ezen segít a függőség megfordítása. Tekintsük a következő kódot, amely a szabványos bemeneten olvasott karaktersorozatot egy kimenő szöveges állományba írja: import java.io.FileWriter; import java.io.IOException; class CopyCharacters { private FileWriter writer; public void copy() throws IOException { writer = new FileWriter("out.txt"); int c;
8 Created by XMLmind XSL-FO Converter.
Bevezetés
while ((c = System.in.read()) != -1) { writer.append((char) c); } writer.close(); } } public class Main { public static void main(String[] args) throws IOException { CopyCharacters cc = new CopyCharacters(); cc.copy(); } }
Itt a copy metódus függ a System.in.read és a FileWriter.append metódustól. A copy metódus fontos logikát ír le, a forrásból a célra kell másolni karaktereket állományvégjelig. Ezt a logikát elviekben sok helyen fel lehetne használni, hiszen a forrás és a cél bármi lehet, ami karaktereket tud beolvasni, illetve kiírni. Ez a kód azonban konkrét (alacsony szintű) megvalósításoktól függ ( System.in, FileWriter). Ha ezt a kódot szeretnénk újrafelhasználni, akkor vagy if ... else if szerkezet segítségével kell megállapítani, hogy éppen aktuálisan melyik forrásra, illetve célra van szükség. Ekkor például a szabványos bemenet helyett egy állományból olvashatnánk, vagy akár egy sztringből, esetleg karaktertömbből, stb. Ez persze nagyon csúnya, nehezen átlátható és módosítható kódot eredményezne, épp ezért az if szerkezet használata helyett azt kellene biztosítanunk, hogy az alacsony szintű konstrukció helyett magas szintű absztrakcióktól függjünk. Ennek egyik módja, hogy a forrás és a cél referenciáját kívülről adjuk meg úgynevezett függőséginjekció (dependency injection) segítségével. A függőséginjekciónak több fajtája is létezik: 1. Függőséginjekció konstruktor segítségével: Ebben az esetben az osztály a konstruktorán keresztül kapja meg azokat a referenciákat, amelyeken keresztül a neki hasznos szolgáltatásokat meg tudja hívni. Ezt más néven objektum-összetételnek is nevezzük és a leggyakrabban épp így programozzuk le. import import import import import
java.io.FileWriter; java.io.IOException; java.io.InputStreamReader; java.io.Reader; java.io.Writer;
class CopyCharacters { private Reader reader; private Writer writer; public CopyCharacters(Reader r, Writer w) { reader = r; writer = w; } public void copy() throws IOException { int c; while ((c = reader.read()) != -1) { writer.append((char) c); } } } public class Main { public static void main(String[] args) throws IOException { try (FileWriter fw = new FileWriter("out.txt")) { CopyCharacters cc = new CopyCharacters(new InputStreamReader( System.in), fw); cc.copy(); } } }
Látható, hogy a copy metódus törzse megváltozott, az egész CopyCharacters osztály függetlenné vált a forrástól és a céltól is, most már absztrakcióktól (a java.io.Reader és java.io.Writer interfészektől) függ csupán. Természetesen a hívó is változott, hiszen immár az ő felelőssége, hogy azon objektumokat előállítsa, amelyek a konkrét forrást és célt megvalósítják.
9 Created by XMLmind XSL-FO Converter.
Bevezetés
2. Függőséginjekció beállító (setter) metódusokkal: Ebben az esetben az osztály beállító metódusokon keresztül kapja meg azokat a referenciákat, amikre szüksége van a működéséhez. Általában ezt csak akkor használjuk, ha opcionális működés megvalósításához kell objektum-összetételt alkalmaznunk. Ez alapjában véve nem nagyon különbözik a konstruktor segítségével végzett függőséginjekciótól, leszámítva, hogy a függőségek dinamikusan változtathatók, hiszen nem rögzülnek az objektum példányosításakor. import import import import import import import
java.io.FileWriter; java.io.IOException; java.io.InputStreamReader; java.io.OutputStreamWriter; java.io.Reader; java.io.StringReader; java.io.Writer;
class CopyCharacters { private Reader reader; private Writer writer; public void setReader(Reader reader) { this.reader = reader; } public void setWriter(Writer writer) { this.writer = writer; } public void copy() throws IOException { if (reader == null) throw new IllegalStateException("Source not set."); if (writer == null) throw new IllegalStateException("Destination not set."); int c; while ((c = reader.read()) != -1) { writer.append((char) c); } } } public class Main { public static void main(String[] args) throws IOException { try (FileWriter fw = new FileWriter("out.txt")) { CopyCharacters cc = new CopyCharacters(); cc.setReader(new InputStreamReader(System.in)); cc.setWriter(fw); cc.copy(); cc.setReader(new StringReader("Test string.")); cc.setWriter(new OutputStreamWriter(System.out)); cc.copy(); } } }
Figyeljük meg, hogy amennyiben nem opcionális működést valósítunk meg (mint példánkban), akkor a copy metódus törzse újfent változik, hiszen – szemben a konstruktor segítségével végzett függőséginjekcióval – ez esetben nem tudjuk kikényszeríteni, hogy minden függőség még azelőtt beinjektálásra kerüljön, mielőtt felhasználásra kerülne (például ha a Main-ben a copy-t úgy hívnák meg, hogy a setReader vagy setWriter hívás elmarad). Ezért elképzelhető, hogy a művelet nem hajtható végre, mert a CopyCharacters objektum nincs megfelelő állapotban az injekció hiánya miatt. Azt is láthatjuk a főprogramban, hogy a hívó sokkal rugalmasabban injektálhat, mint az előző esetben, hiszen a függőségek nem rögzülnek a CopyCharacters objektum példányosításakor, hanem dinamikusan, futásidőben újabb függőségek megadására is lehetőség van, két copy metódushívás között. 3. Függőséginjekció interfész megvalósításával: Ez a megoldás az injekció céljaira létrehozott interfész használatát takarja. Először egy interfészt kell készítenünk, amelyen keresztül a függőséginjekciót elvégezzük majd.
10 Created by XMLmind XSL-FO Converter.
Bevezetés
import java.io.Reader; import java.io.Writer; public interface SourceDestination { void setSource(Reader in); void setDestination(Writer out); }
Ezt követően az interfészt használjuk a függőségek beinjektálására: import import import import import
java.io.FileWriter; java.io.IOException; java.io.InputStreamReader; java.io.Reader; java.io.Writer;
class CopyCharacters implements SourceDestination { private Reader reader; private Writer writer; @Override public void setSource(Reader in) { reader = in; } @Override public void setDestination(Writer out) { writer = out; } public void copy() throws IOException { if (reader == null) throw new IllegalStateException("Source not set."); if (writer == null) throw new IllegalStateException("Destination not set."); int c; while ((c = reader.read()) != -1) { writer.append((char) c); } } } public class Main { public static void main(String[] args) throws IOException { try (FileWriter fw = new FileWriter("out.txt")) { CopyCharacters cc = new CopyCharacters(); cc.setSource(new InputStreamReader(System.in)); cc.setDestination(fw); cc.copy(); } } }
Az interfészt általában maga a magas szintű komponens valósítja meg, de lehetőség van arra is, hogy az előző módszerek valamelyikével (konstruktor vagy beállító metódus segítségével) paramétereként adjuk át a függőségeket meghatározó interfészt. 4. Függőséginjekció elnevezési konvenció alapján: Ez általában keretrendszerekre jellemző, amelyek zömében egy (XML) konfigurációs állománnyal szabályozzák, hogy mely objektumhoz jöjjön létre a függőség. Ezt a megoldást elsősorban csak nagyon tapasztalt programozóknak ajánljuk, mert nyomkövetéssel nem lehet megtalálni, hogy honnan jön a példány és ez nagyban megnehezíti a hibakeresést, de úgy általában, a megértést is.
11 Created by XMLmind XSL-FO Converter.
2. fejezet - Haladó programnyelvi eszközök Ebben a fejezetben a Java nyelven történő programozás néhány olyan aspektusát mutatjuk be, amely talán nem kapott helyett a bevezető programozási kurzusban. Biztosan tanult az olvasó például arról, hogy milyen módon lehet a Java nyelvben a kivételeket kezelni, de egyáltalán nem biztos, hogy arról is szó esett, hogy hogyan érdemes egy programban a kivételkezelési stratégiát kialakítani. Erről is szó lesz ebben a fejezetben, mindamellett pedig olyan eszközök használatában is elmélyedhet az Olvasó, mint az annotációk használata, vagy éppen az állítások (assertion-ök) használata, amelyeken alapulva a szerződés alapú API-tervezés rejtelmeivel is megismerkedhet az érdeklődő.
1. Kivételkezelési ökölszabályok Ebben a szakaszban [BLOCH2008EN és BLOCH2008HU] alapján a kivételek kezelésének mintáit tekintjük át. A kivételkezelési mechanizmus minden olyan nyelvben, amelyben beépítetten jelen van, fontos programozói eszközt jelent. Helyesen használva őket, nagyban javíthatják a program olvashatóságát, megbízhatóságát és karbantarthatóságát. Amennyiben viszont rosszul használjuk őket, épp ellenkező hatást érhetünk el, ezért fontos, hogy tisztában legyünk azzal, hogy hogyan érdemes hatékonyan használni ezt az eszközt.
1.1. A kivételek csak kivételes helyzetekre valók A kivételeket sose használjuk normál programvezérlésre, csakis kivételes helyzetek esetén! Az alábbi példa egy meglehetősen rossz gyakorlatot mutat be: 1 // Sose használjuk ezt a kódot tömb bejárására! try { int i = 0; while(true) 5 products[i++].printDetails(); } catch(ArrayIndexOutOfBoundsException e) { }
Itt azt látjuk, hogy az ArrayIndexOutOfBoundsException-t arra használták, hogy egy termékeket tartalmazó tömb elemeinek bejárása során jelezzék, hogy a bejárás befejeződött, azonban ez több okból is aggályos: egyrészt nehezíti a megértést, hiszen nehezebben átlátható kódot eredményez annál, mint ha az alábbi megoldást választanánk: for (Product p : products) p.printDetails();
Ennél is nagyobb problémát jelenthet, hogy amennyiben a printDetails() metódusban esetleg egy teljesen másik tömb feldolgozása során tömbindex-túlhivatkozás történik, a fenti (rossz) megoldás esetén nincs információnk arról, hogy a products tömb összes elemét feldolgoztuk-e, avagy sem. Ezt az elvet természetesen az API-tervezés során is be kell tartanunk: ha egy olyan programkomponenst készítünk, amelyet majd vélhetően más komponensek újrafelhasználnak, nem kényszeríthetjük a leendő klienseket arra, hogy a kivételeket normál programvezérlésre használják. Minden olyan osztálynak, amely állapotfüggő metódussal rendelkezik – vagyis amelyet csak bizonyos feltételek teljesülése esetén lehet meghívni, mint amilyen például az Iterator interfész next() metódusa –, rendelkeznie kell egy állapotjelző metódussal is, amely azt jelzi, hogy megfelelő időpontban történik-e az állapotfüggő metódus meghívása. Ilyen állapotjelző metódus az IteratorhasNext() metódusa, amely alapján a kliens ellenőrizheti, hogy szabad-e, illetve – mivel programozástechnikai értelemben ezt semmi nem tiltja, inkább – érdemes-e meghívnia a bejárás során következő elemet visszaadó next() metódust.
1.2. Ellenőrzött és futásidejű kivételek használata A Java nyelvben a kivételes események kezelésére a Throwable osztály, illetve annak leszármazottai, az Error és az Exception osztályok szolgálnak. Az Exception osztály RuntimeException alosztálya kiemelendően fontos, hiszen így alapvetően háromféle kivételes eseményt értelmezünk: a hibákat (Error és alosztályai), a 12 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
futásidejű (más néven nem ellenőrzött) kivételeket (RuntimeException és alosztályai) és az ellenőrzött kivételeket (az ExceptionRuntimeException hierarchián kívül eső alosztályai). Sok esetben talán nem nyilvánvaló, hogy milyen esetekben melyiket célszerű használni. Az ellenőrzött és nem ellenőrzött kivételek közötti választás során akkor döntsünk az ellenőrzöttek mellett, ha a bekövetkező hibát a hívó (kliens) jó eséllyel helyrehozhatja, hiszen egy ellenőrzött kivétel kiváltásával arra kényszerítjük a hívót, hogy kezelje (vagy dobja tovább) a kivételt, hiszen ellenkező esetben a kódja le sem fordul (ezt jelenti ugyebár az ellenőrzöttség: a fordító ellenőrzi, hogy a hívó megfelelően foglalkozik-e azokkal a kivételekkel, amelyek az adott ponton bekövetkezhetnek). Vagyis egy metódus specifikációjában elhelyezett ellenőrzött kivételek olyan jelzést adnak a az API-t használók felé, hogy a kivételek bekövetkezése is a metódus lehetséges kimenetei közé tartoznak. A fordítói ellenőrzés nélküli visszajelzésnek két fajtája van: a futásidejű kivételek és a hibák. Ezeket nem kötelező (és sokszor nem is érdemes) elkapni, általában nem helyrehozható hibát tükröznek. Az Error-t és alosztályait közmegegyezés alapján a virtuális gép (JVM) számára tartjuk fent, vagyis nem szerencsés programozóként ilyet létrehoznunk. Éppen ezért, ha nem ellenőrzött kivételes eseményeket szeretnénk megvalósítani, akkor a RuntimeException-ből (vagy annak valamely alosztályából) képezzünk új alosztályt! Az ilyen futásidejű kivételeket használjuk programozási hibák jelzésére! A leggyakrabban ilyen hibákkal akkor találkozunk, ha egy metódus szerződésének előfeltétele sérült (bővebb információkért lásd a 4. szakasz Szerződés alapú tervezés szakaszt). Fontos megjegyezni, hogy az ellenőrzött és nem ellenőrzött kivételek funkcionális szempontból teljesen egyenértékűek: minden, amit ellenőrzött kivételekkel elvégezhetünk, megtehetjük nem ellenőrzöttekkel is, és viszont. Az API-tervezők azonban gyakran elfeledkeznek arról, hogy a kivételek teljes értékű objektumok: állapotuk és metódusaik lehetnek, amelyek segítségével a hiba forrása további információkat biztosíthat a kivételt elkapó programrész számára. Rossz gyakorlat, ha egy kivétel sztringreprezentációjából kell kinyernie a hiba körülményeire vonatkozó információkat, ráadásul ez nehezen hordozhatóvá teszi a kódot. Például egy bankkártyás fizetés fedezethiány miatti sikertelenségét jelző ellenőrzött kivétel közölheti a hívóval a kivételobjektum állapota és egy lekérdező metódus segítségével, hogy mennyi híja volt a sikeres tranzakciónak, és így a kivételt elkapó klienskód jelezheti ezt a felhasználónak.
1.3. Kerüljük az ellenőrzött kivételek szükségtelen használatát Egy metódus által dobott minden ellenőrzött kivétel terheket ró a kliens programozójára, hiszen rákényszeríti arra, hogy foglalkozzon a kivételes helyzetekkel. Ráadásul, amennyiben több ellenőrzött kivételt is dob egy metódus, akkor mindegyiket kezelni kell, ami a kezelést végző catch ágak nagy száma miatt átláthatatlan kódot eredményezhet. Ha a metódusunkban valamilyen kivételes esemény bekövetkezése várható, és döntenünk kell, hogy ellenőrzött vagy nem ellenőrzött kivételt dobjunk-e, akkor érdemes feltennünk magunknak a kérdést: mit fog ezzel a kivétellel kezdeni az a programozó, aki meghívja ezt a metódust? Mert ha csak ennyit: } catch (TheCheckedException tce) { e.printStackTrace(); System.exit(1); }
akkor jobb a nem ellenőrzött kivétel használata, hiszen a programozó semmit nem tud vagy nem akar tenni a kivétel kezelése érdekében. Ha egyetlen ellenőrzött kivételt dob csak a metódus, akkor emiatt az egyetlen kivétel miatt kell a hívás helyén egy try blokkot bevezetni, ami ront a kód olvashatóságán. Ilyen esetben akár át is szervezhetjük az API-nkat annak érdekében, hogy ne legyen ilyen probléma. Például az alábbi példa obj objektumának osztályába az action metódus mellé felvehetünk egy actionPermitted metódust, amely azt ellenőrzi, hogy az action műveletet szabad-e végrehajtani. Ekkor a hívás helyén // Hívás ellenőrzött kivétellel rendelkező action metódus esetén try { obj.action(args); } catch(TheCheckedException tce) { // Kivétel kezelése }
helyett a kényelmesebben használható (bár nem feltétlenül szebb megoldást nyújtó) // Állapotellenőrző metódus használata if (obj.actionPermitted(args)) {
13 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
obj.action(args); } else { // Hibás eset kezelése (nem kivételkezelés!) }
kódhoz juthatunk. Ez tulajdonképpen a korábban már látott állapotjelző metódus megjelenését jelenti, azonban ez az átalakítás nem mindig végezhető el (például ha az objektum több szálon is elérhető, mert akkor az actionPermitted és az action hívások között is megváltozhatna az állapota). Ezzel is rákényszerítjük a kliens programozóját arra, hogy foglalkozzon a kivételes helyzettel, még ha arról nem is kivétel formájában értesül. Újabb lehetőség, hogy csak egy nem ellenőrzött kivételt váltunk ki hiba esetén, ekkor a kliensnek elegendő egy obj.action(args);
hívást elvégeznie, ez azonban a végrehajtási szál végét is jelentheti egy kivétel bekövetkezése esetén.
1.4. Favorizáljuk a szabványos kivételeket A tapasztalt és tapasztalatlan programozókat egymástól többek között az is megkülönbözteti, hogy előbbiek mindent megtesznek az egyszer már bevált kódrészletek újrafelhasználása érdekében, és ezt általában magas szintre is emelik. Ez alól a kivételek sem kivételek, és mivel a Java szabványos API-jában számos olyan nem ellenőrzött kivétel szerepel, amelyek sok kivételdobási igényt kielégítenek, ezért, ha csak lehet, érdemes ezeket használni. Mindennek több oka is van: az API könnyebben tanulható, mivel ismerős konvenciókra támaszkodhatunk, ráadásul nincs szükség a saját kivételosztályok átvitelére abba a rendszerbe, ahonnan az újrafelhasználás megtörténik. Szintén fontos érv szól a kód olvashatósága mellett: az olyan kódot könnyebb olvasni, amely nincs teletűzdelve sosem látott kivételekkel. Végül (és egyben utolsósorban) kevesebb kivételosztály kisebb memóriafogyasztást és kevesebb osztálybetöltést igényel, ezért jobb teljesítményt is eredményez. A leggyakrabban újrafelhasznált kivétel az IllegalArgumentException, amelyet akkor dobunk, ha a hívótól nem megfelelő értékű paramétert kaptunk. Például, ha egy tevékenység ismétlési darabszámára várva egy negatív értéket kapunk. Egy másik gyakran újrafelhasznált kivétel az IllegalStateException, amit általában akkor váltunk ki, ha a hívás a fogadó objektum állapota miatt érvénytelen. Ilyet érdemes az előző alszakasz végén említett obj.action(args) hívás során kiváltani, ha a tevékenység nem végrehajtható. Persze csaknem minden rossz metódushívásra rá lehetne fogni, hogy vagy rossz paraméterekkel, vagy rossz állapotban érkezett, de vannak olyan beépített nem ellenőrzött kivételek is, amelyek ezen érvénytelen paramétereket illetve állapotokat tovább részletezik. Például ha egy olyan paraméter értéke null, amelyé nem lehetne az, akkor IllegalArgumentException helyett inkább NullPointerException-t dobjunk. Hasonlóként, ha egy engedélyezett értéktartományon kívüli indexhivatkozásról van szó (például egy ötelemű lista tízes indexű elemére hivatkoznak), akkor IllegalArgumentException helyett alkalmazzunk IndexOutOfBoundsException-t. Egy másik olyan kivétel, amiről nem árt tudni, a ConcurrentModificationException, ami olyan esetekre lett tervezve, amikor egy nem párhuzamos működésre tervezett objektum konkurens módosítása történik vagy éppen történt meg. Az utolsó, említésre méltó kivétel az UnsupportedOperationException, amit akkor szokás kiváltani, ha nem támogatott műveletet próbálnak meg végrehajtani egy objektumon. Ilyen kivétel váltódik ki például a Java Collections Framework (JCF) bizonyos osztályainak opcionális műveletei esetén, ha egy osztály nem valósítja azokat meg. Az újrafelhasználható kivételek közül történő választás nem mindig egyértelmű. Példaként tekintsünk egy kártyapartit reprezentáló objektumot, és annak valahány lap leemelésére szolgáló metódusát, amely a leemelendő lapok számát paraméterként kapja. Ha a kártyalapok számánál kisebb vagy nagyobb számú lapot próbálnak leemelni, akkor ez lehet IllegalArgumentException (hiszen a paraméterül kapott érték nem megfelelő), vagy akár IllegalStateException is (mivel a leemelést követően érvénytelen állapotú paklihoz jutnánk). Mindkét megoldás indokolható, azonban érezhető, hogy ebben az esetben az IllegalArgumentException némileg megfelelőbb, hiszen az esetleges érvénytelen állapot csakis a rossz paraméternek köszönhető. Ez az eset mindenesetre rámutat arra, hogy a helyzet nem fekete-fehér. 14 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
1.5. Az absztrakciónak megfelelő kivételt dobjuk Ez talán a kivételkezeléssel kapcsolatos legfontosabb szabály. Nagyon rossz ugyanis olyan kivétellel szembesülni, amely látszólag semmi köze ahhoz a művelethez, amelynek során keletkezett. Ilyen rossz megoldásra példaként tekintsük a következő, fix elemszámú tömbbel megvalósított verem adatszerkezetet megvalósító osztályt: 1 public class FixedLengthStack<E> { private E[] elements; private int noOfElements; 5
@SuppressWarnings("unchecked") public FixedLengthStack(int capacity) { elements = (E[])new Object[capacity]; noOfElements = 0; }
10 public boolean empty() { return noOfElements == 0; } 15
20
25
public E push(E item) { return elements[noOfElements++] = item; } public E pop() { return elements[--noOfElements]; } public E peek() { return elements[noOfElements - 1]; } public int getCapacity() { return elements.length; }
30 }
Generikusok a Java nyelvben A triviálisnál akár csak kicsit is összetettebb szoftverprojektek óhatatlan velejárója a hiba (bug). A tervezési, programozási és tesztelési folyamatok gondos elvégzésével valamelyest csökkenthető persze annak kiterjedését, de végső soron valahogyan mégis mindig utat találnak maguknak. Ez különösen akkor jelenik meg, ha új funkciók bevezetése miatt a kód mérete és bonyolultsága megnő. Szerencsére azonban nem minden hibát egyformán nehéz megtalálni: a fordítási hibák felderítése sokkal egyszerűbb (és jóval korábban is megtehető) mint a futásidejű hibák megtalálása. Utóbbiak nem feltétlenül fedik fel magukat azonnal, a hiba látszólagos és tényleges oka viszonylag messze kerülhet egymástól. A generikusok úgy adnak némi stabiliztást a kódunkhoz, hogy lehetővé teszik számos hiba fordítási időben történő felderítését. Egy generikus típus egy típussal parametrizált általánosított osztály vagy interfész lehet. Generikus típusokkal bővebben a Java 7 generikusokról szóló tutoriál http://docs.oracle.com/javase/tutorial/java/generics/ ad információt. Az alábbi videók rövidke betekintést nyújtanak a generikusok használatába.
Készítsünk egy háromelemű, egész értékek tárolására alkalmas vermet, amelybe próbáljunk meg beszúrni öt elemet! 1 public class FixedLengthStackExample { public static void main(String... args) { FixedLengthStack
s = new FixedLengthStack(3);
15 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
for (int i = 0; i < 5; i++) { System.out.printf("Invoking push(%d)...\n", i); s.push(i); }
5 } }
A program végrehajtása során kapott kimenet az alábbi: Invoking push(0)... Invoking push(1)... Invoking push(2)... Invoking push(3)... Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3 at FixedLengthStack.push(FixedLengthStack.java:16) at FixedLengthStackExample.main(FixedLengthStackExample.java:6)
Azt láthatjuk, hogy egy ArrayIndexOutOfBoundsException következett be (akkor, amikor a negyedik elemet megpróbáltuk a verembe tenni). Ez a kivétel azonban a hívás helyén nehezen értelmezhető: a hívó egy verembe beszúrást végző push művelet végrehajtásakor aligha számíthat arra, hogy ilyen hibával találkozik! Annál is inkább, mivel egy jól megvalósított programban a verem absztrakt adattípus, amely bezárja az implementáció részleteit. Ez azonban ebben az esetben pont ezen kivétel miatt nem valósult meg, hiszen ezzel a hívó olyan információhoz jutott (lévén ilyen kivétel csak tömbök használata során következik be), hogy a megvalósítás tömb segítségével történt, holott ez sem nem érdekelte, sem nem lett volna szabad ilyen információt kapnia. A kivételkezelésünket úgy kell alakítani, hogy a hívó mindig megfelelő szakterületi fogalmak segítségével megfogalmazott kivételekkel találkozzon. Ezt az irányelvet szem előtt tartva úgy tudjuk átalakítani a verem megvalósítását, hogy az adott hívási környezetben semmitmondó (sőt, még inkább zavaró) kivétel helyett egy megfelelő absztrakciós szinten lévő FullStackException-t váltsunk ki. Természetesen a szimmetria miatt (hiszen az üres – egyetlen elemet sem tartalmazó – veremre alkalmazott pop művelet is ArrayIndexOutOfBoundsException-t dob) egy EmptyStackException-re is szükségünk lesz. Ezek a kivételek a hívó számára – szemben a korábbi ArrayIndexOutOfBoundsException-nel – valódi jelentéssel bírnak, hiszen egy fix méretű verembe történő beszúráskor, illetve abból történő törléskor joggal számíthat a hívó olyan hibákra, amelyek a verem telítettségét illetve ürességét, mint a sikertelen műveletvégzés okát jelölik meg. A kérdés csupán az, hogy ezen kivételek ellenőrzöttek vagy nem ellenőrzöttek legyenek-e? A korábban tárgyalt irányelvek alapján könnyen megválaszolhatjuk ezt a kérdést: ellenőrzött kivételre lesz szükségünk, hiszen ez a hiba egyáltalán nem elháríthatatlan, hiszen a kliensnek több lehetősége is van, hogy reagáljon. Nyilván lehetősége van arra, hogy a veremműveletet valamilyen más tevékenységgel helyettesítse (ez persze problémafüggő, ezért nem általánosan alkalmazható megoldás, attól függ, milyen célból haználja a kliens a vermet), de akár egy nagyobb méretű verem felhasználásával a meglévő veremben lévő elemeket átmásolva folytathatja munkáját. Ez utóbbi tevékenységhez persze nagy segítséget nyújthatna az alábbi konstruktor hozzáadása a FixedLengthStack osztályhoz: @SuppressWarnings("unchecked") public FixedLengthStack(FixedLengthStack<E> base, int capacity) { elements = (E[])new Object[capacity]; System.arraycopy(base.elements, 0, elements, 0, base.noOfElements); noOfElements = base.noOfElements; }
Ez a konstruktor tulajdonképpen létrehoz egy, a második paramétereként megadott kapacitással rendelkező fix méretű vermet, és az első paraméterében lévő verem elemeit átmásolja az új verembe (vagyis az új verem így ugyanazon elemeket tartalmazza, mint az eredeti, csak a kapacitás változik). Hozzuk létre a FullStackException nevű ellenőrzött kivételt: public class FullStackException extends Exception { }
Ezt követően a FixedLengthStack osztály push metódusában ki kell váltanunk a kivételt, ha a verem már tele van (vagyis a tényleges elemek száma megegyezik a tömb számára lefoglalt elemek darabszámával): public E push(E item) throws FullStackException { if (noOfElements == elements.length)
16 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
throw new FullStackException(); return elements[noOfElements++] = item; }
Ezzel ekvivalens megoldást nyújt, azonban egy Java programozási idiómát, méghozzá a kivételfordítást (exception translation) mutatja be az alábbi megoldás: 1 public E push(E item) throws FullStackException { try { elements[noOfElements] = item; noOfElements++; 5 return item; } catch (ArrayIndexOutOfBoundsException e) { throw new FullStackException(); } }
Fontos Figyeljük meg, hogy ebben a megvalósításban az előző változat kompakt megoldása (elements[noOfElements++] = item;) helyett jóval konzervatívabb megoldást választottunk. Ez nem véletlen, ugyanis ha a try-blokkon belül is ugyanezt helyeznénk el, előfordulhatna, hogy olyankor, amikor bekövetkezik az ArrayIndexOutOfBoundsException, a ++ operátor növelő hatása érvényesül, ezáltal pedig megváltozik a veremobjektum állapota oly módon, hogy az elemszám eggyel nő, annak ellenére, hogy a tényleges beszúrás az index-túlhivatkozás miatt nem történik meg. (Ez a helyzet az első megoldás során nem fordulhatott elő, hiszen a return-t tartalmazó kompakt sor végrehajtására csak akkor került sor, ha nem volt tele a verem, de ekkor a művelet gond nélkül végrehajtható, nem okoz problémát.) Mindez persze arra is rávilágít, hogy a tömör kód nem minden esetben szerencsés, sőt. Ez a megoldás foglalja össze a teendőinket annak érdekében, hogy a megfelelő absztrakciós szintű kivételt dobhassuk el: a magasabb szintű rétegeknek el kell kapniuk az alacsonyabb szintű kivételeket, és helyettük olyan kivételt kell dobniuk, amely a magasabb szintű absztrakciónak megfelelően magyarázza el a hibát. Itt pont ezt látjuk: az alacsonyabb szintű ArrayIndexOutOfBoundsException helyett (amely a hívónak úgysem mondana sokat) a kivételt lefordítjuk egy, az adott szakterületnek megfelelő fogalomra. A magasabb absztrakciós szinten a tömb túlhivatkozásának kivétele azzal egyenértékű, hogy a verem tele van, ezért a FullStackException nevű kifejező kivételt dobjuk helyette. A kivételfordítás speciális esete a kivételláncolás (exception chaining), amit akkor használunk, ha úgy ítéljük meg, hogy az alacsonyabb szintű kivétel is informatív lehet, például belövési (debug) célokból. Ehhez arra van szükség, hogy a magasabb szinten lévő kivételosztály rendelkezzen Throwable paraméterű konstruktorral, hiszen ennek segítségével lehet az alacsonyabb szintű kivételobjektumot a magasabb szintűbe csomagolni. A fenti példában ez nem kivitelezhető, hiszen a FullStackException osztály csak az alapértemezett konstruktorral rendelkezik. Ha kivételfordítás helyett láncolni szeretnénk, akkor FullStackException kódja az alábbiak szerint módosul: public class FullStackException extends Exception { public FullStackException(Throwable cause) { super(cause); } }
Ezt követően a push művelet 5. sorában a következőt kell írnunk a kivételláncolás érdekében: throw new FullStackException(e);, ahol e az alacsonyabb szintű kivétel objektuma. Egy ilyen kivételt elkapó kliens a kivételláncon szereplő objektumokat az elkapott kivételobjektum getCause() metódusának iteratív módon történő hívásával érheti el. Ahogyan azt már megállapítottuk, ebben a konkrét példában a kivételláncolásnak nincs értelme, hiszen a kliens számára a magas szintű kivétel okaként fellépő alacsony szintű ( ArrayIndexOutOfBoundsException) kivétel semmitmondó, nem segíti a megértést. Épp ezért a példa további részében a kivételláncolástól eltekintünk.
17 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
A push műveletet meghívó főprogram is változtatásra szorul, hiszen ebben a formájában már nem fordítható, mivel a fordító az ellenőrzött kivétel bekövetkezését észlelve hiányolja a kivétel kezelését avagy továbbdobását. Utóbbira példa: public class FixedLengthStackExample { public static void main(String... args) throws FullStackException { FixedLengthStack s = new FixedLengthStack(3); for (int i = 0; i < 5; i++) { System.out.printf("Invoking push(%d)...\n", i); s.push(i); } // További tevékenységek, amelyek a FullStackException kivétel // bekövetkezése esetén nem kerülnek végrehajtásra. } }
Ha minden hívás esetén így járnánk el, az egyben azt is jelentené, hogy a kivétel lehetett volna futásidejű is, hiszen ilyenkor pont az elenőrzött kivételek használatának legfőbb előnyét, nevezetesen a hiba helyreállíthatóságát hagyjuk veszendőbe. Természetesen kliense válogatja, hogy mit teszünk a kivétellel, van, amikor a továbbdobás, van, amikor a valamilyen módon történő kezelés lesz a megfelelő reakció. Ebben az esetben is több lehetőség közül választhatunk, attól függően, hogy miképpen kell reagálni. Elképzelhető, hogy a veremműveleteket befejezve szeretnénk továbbhaladni a program végrehajtásában. Ekkor a try-catch blokk a for ciklust öleli körül: public class FixedLengthStackExample { public static void main(String... args) { FixedLengthStack s = new FixedLengthStack(3); try { for (int i = 0; i < 5; i++) { System.out.printf("Invoking push(%d)...\n", i); s.push(i); } } catch (FullStackException fse) { // A kivétel kezelése } // További tevékenységek, amiket a kivétel bekövetkezése esetén végre szeretnénk hajtani. } }
Abban az esetben, ha a kivételt okozó műveletet újfent el szeretnénk végezni, miután megnöveljük a verem méretét, akkor további módosításokra van szükség. Ebben az esetben a ciklus törzsében kell majd a kivétel bekövetkezését figyelni, hogy még a következő iteráció előtt újra elvégezhessük a műveletet. Ehhez azonban szükség volna arra az elemre, amelynek a beszúrása során a hiba bekövetkezett! Ezt a legjobb volna, ha a kivételobjektum zárná be, hiszen a push művelet – ahol a kivétel eldobásra kerül – természetesen ismeri ezt az értéket. Mivel a verem generikus, ezért a FixedLengthStack osztály fordítási idejében nem tudjuk, milyen elemek kerülnek bele, azt viszont tudhatjuk, hogy a kivételobjektumba is pont ugyanolyan típusú elemet kellene tenni. Első gondolatunk az lehet, hogy tegyük a kivételosztályt is generikussá: // HIBÁS KÓD! public class FullStackException<E> extends Exception { private E erroneousElement; public FullStackException(E e) { erroneousElement = e; } public E getErroneousElement() { return erroneousElement; } }
A fordításkor viszont az alábbi hibaüzenettel szembesülünk: FullStackException.java:1: error: a generic class may not extend java.lang.Throwable public class FullStackException<E> extends Exception {
18 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
^ 1 error
Vagyis a Throwable osztály nem rendelkezhet generikus leszármazott osztállyal. Így hát nem maradt más választásunk – az általánosság megtartása érdekében –, mint hogy a kivételosztály által becsomagolt objektum Object típusú legyen: public class FullStackException extends Exception { private Object erroneousElement; public FullStackException(Object element) { erroneousElement = element; } public Object getErroneousElement() { return erroneousElement; } }
A push művelet az alábbiak szerint módosul annak érdekében, hogy eltárolásra kerüljön a hibát okozó elem: public E push(E item) throws FullStackException { try { elements[noOfElements] = item; noOfElements++; return item; } catch (ArrayIndexOutOfBoundsException e) { throw new FullStackException(item); } }
A verem tartalmának könnyebb nyomonkövetése érdekében adjuk hozzá a FixedLengthStack osztályhoz az alábbi toString metódust, amelynek segítségével karakteresen tudjuk írhatjuk a képernyőre a verem aktuális állapotát! public String toString() { StringBuilder sb = new StringBuilder(); for (int i = elements.length - 1; i >= 0; i--) sb.append("| ").append(i > noOfElements - 1 ? " " : elements[i]).append(" |\n"); sb.append("-----"); return sb.toString(); }
Megjegyzés Ez a megoldás mesze nem elég általános egy ilyen generikus osztály számára (például már kétszámjegyű egész elemek esetén is szétcsúszik a kimenet, nem beszélve arról, ha nem egész értékeket tárolnánk), de példánk szempontjából elegendő lesz. A hívás helyén pedig a következőt írjuk: 1 public class FixedLengthStackExample { public static void main(String... args) { FixedLengthStack s = new FixedLengthStack(3); System.out.println(s); 5 for (int i = 0; i < 5; i++) { try { System.out.printf("Invoking push(%d)...\n", i); s.push(i); System.out.println(s); 10 } catch (FullStackException fse) { System.out.println("Increasing stack size."); s = new FixedLengthStack(s, s.getCapacity((1)) + 1); System.out.printf("Invoking push(%d) again...\n", (Integer)fse.getErroneousElement()); try { 15 s.push((Integer)fse.getErroneousElement()); (2) System.out.println(s); } catch (FullStackException e) {
19 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
throw new RuntimeException(e);
(3)
} 20
} } } }
1
A kivétel bekövetkezése esetén eggyel megnöveljük a verem méretét.
Megjegyzés
2
3
Természetesen ilyet, hogy egy fix méretű adatszerkezet méretét egyesével növeljük, a valóságban sose tegyünk! Ez számos hátránnyal jár, elsősorban a teljesítmény vonatkozásában. Azonban ez a példa egyszerű eszközökkel bemutatja, hogy hogyan állíthatjuk helyre a program állapotát egy kivétel bekövetkezése esetén, ezért kerül mégis bemutatásra. A push művelet ismételt végrehajtásának érdekében lekérjük a kivételobjektumtól azt az elemet, amelynek beszúrása során a kivétel bekövetkezett. Mivel azonban a push művelet végrehajtása során a FullStackException nevű ellenőrzött kivétel bekövetkezhet, ezért ezt szintén kezelni kell. Ha második nekifutásra sem sikerült a verembe beszúrás, akkor nincs értelme tovább próbálkozni, ezért ebben az esetben ezt a kivételt becsomagoljuk egy futásidejű kivételbe, és útjára engedjük. Ez a helyzet persze akkor, ha megfelelően növeltük a verem méretét, elvileg elő sem fordulhat, épp ezért ez jól mutatja az ellenőrzött kivételek használatának egyik hátulütőjét: hiába tudja a programozó, hogy egy adott ponton egy adott kivétel nem következhet be, formálisan mégis el kell végezni a kivétel lekezelését, ami a kód áttekinthetőségét is rontja.
Habár a kivételfordítás mindenképpen jobb megoldás az alsó rétegekből érkező kivételek ész nélkül történő továbbdobásánál, azonban ezt sem érdemes túlzásba vinni. Az alacsonyabb rétegekben előálló kivételekkel szemben a legjobb stratégia az, ha elkerüljük őket azáltal, hogy biztosítjuk az alacsony szintű metódus sikeres lefutását. Sokszor ehhez elegendő a mgasabb szintű metódus paramétereinek ellenőrzését elvégezni, mielőtt az alacsonyabb szintre továbbadjuk őket. Ha nem tudjuk megakadályozni, hogy az alacsonyabb rétegekből kivételek jöjjenek, akkor a legjobb, amit tehetünk, hogy úgy alakítjuk ki a magasabb réteget, hogy az szép csendben elszigetelje a magasabb szintű rétegek hívóját az alacsonyabb szintű réteg problémáitól. A verem további műveleteit górcső alá véve azt állapíthatjuk meg, hogy a verem legfelső elemét törlő pop() és a legfelső eleméhez törlés nélkül hozzáférést biztosító peek() esetében is bekövetkezhet ArrayIndexOutOfBoundsException, például ha megpróbáljuk törölni vagy elérni egy üres verem legfelső elemét. Az egyik megoldás már csak a szimmetria miatt is az lehetne, ha létrehoznánk egy EmptyStackException kivételosztályt, amellyel ezt az esetet kezelhetjük. Egy másik lehetséges megoldás, ha észrevesszük, hogy a verem empty() művelete valójában egy állapotjelző metódus, amely pont a pop() és peek() állapotfüggő műveletek előfeltétel-ellenőrzésére haználható, például: if (!empty()) pop();
Ebben az esetben érdemes azonban a pop és peek metódusok törzsét egy szabványos kivétel kiváltásával kiegészíteni, hogy abban az esetben, ha esetleg a kliens nem hívná meg az állapotjelző metódust (csak rögtön az állapotfüggő műveletet), akkor se a semmitmondó ArrayIndexOutOfBoundsException-nel szembesüljön. Az IllegalStateException némileg kifejezőbb, lévén az üresveremből törlés valóban érvénytelen veremállapothoz vezetne. public E pop() { if (noOfElements < 1) throw new IllegalStateException(); return elements[--noOfElements]; } public E peek() { if (noOfElements < 1) throw new IllegalStateException(); return elements[noOfElements - 1]; }
20 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
1.6. Minden metódus minden kivételét dokumentáljuk Mivel az ellenőrzött kivételek a metódusok specifikációjának részét képezik, ezért roppant fontos, hogy megfelelően dokumentáljuk azokat. Az ellenőrzött kivételeket deklaráljuk egyesével, vagyis ne essünk abba a hibába, hogy néhány kivételosztály közös ősosztályát választjuk ki, mint az eldobandó kivétel osztálya.
Fontos SOHA ne tegyünk olyat, hogy egy metódus fejlécét throws Exception-nel (vagy ami talán még ennél is rosszabb, throws Throwable-lel fejezzük be! Ez ugyanis arra kényszerítené a hívót, hogy Exception-re (illetve Throwable-re) hallgató kivételkezelőt készítsen a hívás helyén, ami pedig az összes többi hívásból származó kivételek kezelését nagyban megnehezítené. Az ellenőrzött kivételeket a javadoc dokumentációs megjegyzéseiben használható @throws taggel jelöljük meg, pontosan leírva mindazon feltételeket, amelyek bekövetkezése esetén a kivétel bekövetkezik. Hasonlóan járjunk el a metódus által kiváltható nem ellenőrzött kivételek esetén is (a @throws dokumentációs megjegyzés ekkor is szerepeljen!), a különbség mindössze annyi, hogy a nem ellenőrzött kivételek természetesen a metódus specifikációjában nem jelennek meg. Ha az osztályban ugyanolyan okból több metódus is dobhatja ugyanazt a kivételt, akkor ahelyett, hogy minden metódusnál dokumentáljuk, megtehetjük, hogy az osztály dokumentációjának részévé tesszük a kivétel leírását. Ilyen például gyakran a NullPointerException: leírhatjuk az osztály dokumentációjában, hogy „az osztály minden metódusa NullPointerException kivételt dob, ha bármely paraméter nullreferenciát kap”.
1.7. A hiba lényegére koncentráló hibaüzenet-szövegeket írjunk Ha egy program egy el nem kapott kivétel miatt leáll, alapértelmezés szerint a hívási lánc verme a képernyőre kerül. Itt találkozhatunk a kivétel sztringreprezentációjával, amely a kivétel osztályából és egy részletes üzenetből áll. A hiba megfelelő módon történő megragadásának érdekében a kivétel részletes üzenetébe minden olyan paraméter és adattag értékét írjuk bele, amelyek szerepet játszhattak a hiba létrejöttében
1.8. Törekedjünk az elemi hibaszint megőrzésére Általában elmondható, hogy minden, kivétel bekövetkezése miatt meghiúsult metódushívásnak az objektumot olyan állapotban kell hagynia, mint amilyenben a metódushívás előtt volt. Emlékezzünk vissza a kivételfordítás bemutatásakor átalakított push metódustörzsre! Ott pont amiatt kellett a korábbi egysoros törzset lecserélni, mert abban a ++ operátor mellékhatásként megváltoztatta az objektum állapotát (a verem elemszámának megnövelésével), még akkor is, ha egyébként kivétel váltódott ki a végrehajtás során. Ez nem kívánatos viselkedés. Ennek a problémának a megoldására többféle eszköz áll rendelkezésünkre. Hacsak lehetséges, használjunk megváltoztathatatlan (immutable) objektumokat, és akkor ez a kérdés meg van oldva. Egy másik módszer szerint – amennyiben elkerülhetetlen, hogy változó objektumokon dolgozzunk – a paraméterek értékét ellenőrizzük még a művelet elvégzése előtt. Ez a példánkban a push metódus eredeti megvalósítását jelentené, amikor előbb egy if utasítás segítségével levizsgáltuk, hogy tele van-e a verem, és ha igen, akkor kiváltottuk a megfelelő kivételt, majd magát a műveletet csak akkor végeztük el, ha a verem nem volt üres. Újabb megoldást jelenthet, ha „tranzakciókká alakítjuk” a számításainkat, vagyis úgy rendezzük sorba a számításokat, hogy a meghiúsulásra hajlamos részfeladatok azelőtt helyezkedjenek el, mielőtt bármely művelet módosítani kezdené az objektumot. Lehetőség van még úgynevezett helyreállító kód írására is, amely a művelet közben bekövetkezett hibát érzékelve visszaállítja az objektumot a műveletvégzés előtti állapotába. Ezt a megoldást leginkább lemezen tárolt adatszerkezetek esetén alkalmazzuk. Szintén jó megoldás, ha a megváltoztatandó objektumról készítünk egy másolatot, ezen hajtjuk végre a műveletet, majd ha sikeresen véget ért, akkor írjuk felül az eredeti objektumot az újjal (hasonló elven működnek az adatbázis-kezelő rendszerek adatblokkgyorsítói is). Ez egyébként a későbbiekben bemutatásra kerülő Emlékeztető (Memento) tervezési minta alkalmazását jelenti. 21 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
1.9. Ne hagyjunk figyelmen kívül kivételeket Ne hagyjuk üresen a catch ágat, mert az aláássa a kivételdobás célját, vagyis, hogy lehetősége legyen a hívónak a kivételes körülmények megfelelő lekezelésére. Ez kicsit olyan, mint ha kikapcsolnánk a tűzjelző hangját: ebben az esetben esetleg senki nem vesz majd tudomást a tűzről. Ha mégis okkal teszünk ilyet, legalább megjegyzéssel dokumentáljuk. Ilyen helyzet tradicionálisan tipikusan az erőforrások (például egy állomány, adatbázis-kapcsolat, stb.) lezárásakor fordult elő, hiszen a close metódus is kiválthat például IOException-t.
2. Állítások (assertions) A Java nyelv 1.4-es verziójában jelentek meg először az állítások (assertions). Az assert utasítás segítségével fogalmazhatunk meg programunk egy pontján egy olyan logikai állítást, amelynek igaznak kell(ene) lennie. Állapotuk lehet engedélyezett vagy letiltott. Alaphelyzetben letiltott állapotban vannak. A letiltott állapotú assert utasítások a szemantikát és a teljesítményt illetően is az üres utasítással ekvivalensek. Az engedélyezett állítások esetén, ha az állítás valóban igaz, az assert utasítás hatástalan, ellenkező esetben egy AssertionError hiba következik be. Az assert utasításnak két formája van: 1. assert kifejezés; Itt kifejezés egy logikai értékű kifejezés. Ha az állítások használata engedélyezett, akkor az utasítás végrehajtása ekvivalens az if (! kifejezés) { throw new AssertionError(); }
utasításéval. 2. assert kifejezés1 : kifejezés2; Itt kifejezés1 egy logikai értékű, míg kifejezés2 egy értékkel rendelkező (vagyis nem-void) kifejezés. Ha az állítások használata engedélyezett, akkor az utasítás végrehajtása ekvivalens az if (! kifejezés1) { throw new AssertionError(kifejezés2); }
utasításéval. Az állítások segítségével programunk megbízhatóságát tudjuk növelni azáltal, hogy bizonyos, a programvégrehajtással kapcsolatos feltelezéseinket megvizsgálhatjuk. Az állítások az alábbi esetekben használhatók: • Belső invariánsok Tekintsük az alábbi kódrészletet: if (salary > 200000) { ... } else if (salary > 100000 && salary <= 200000) { ... } else { // salary kisebb vagy egyenlő mint százezer }
Ebben az esetben csupán a megjegyzés állítja egy adott ponton, hogy egy változónk értékére valamilyen feltétel teljesül. Nyilván beletehetnének ezt is egy if-be, de hatékonysági szempontból nem szeretnénk állandóan egy plusz feltételvizsgálatot. Ha assert utasítást helyezünk el a megjegyzés helyett, akkor ez az éles kódban semmilyen gondot nem okoz, hiszen ott az alapértelmezés szerinti módon minden bizonnyal 22 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
kikapcsolva tartjuk az állítások figyelését, de a fejlesztői és tesztrendszeren engedélyezhetjük őket, és ekkor az if-else-if szerkezet feltételeinek átírása esetén is megbizonyosodhatunk róla, hogy az így tett állításunk még mindig helytálló. Ráadásul, az alább látható módon használva az esetleges feltételmegsértés esetén a keletkezett hibaobjektumba a hibát okozó salary érték is becsomagolásra kerül, megkönnyítve ezáltal a hiba okának felderítését. if (salary > 200000) { ... } else if (salary > 100000 && salary <= 200000) { ... } else { assert salary <= 100000 : salary; }
Ugyanezt hasonló megfontolásból a default ág nélküli switch utasításban is érdemes megtenni, mert így az ágak későbbi átrendezése nem okoz váratlan meglepetést. switch (num) { case 1: ...; break; case 2: ...; break; case 3: ...; break; ... default: assert false : num; break; }
• Vezérlésiszerkezet-invariánsok Olyan helyre helyezzük, ahová elvileg nem juthat el a vezérlés. Ha a későbbi programátalakítások miatt mégis eljut, a hibáról gyorsan tudomást szerzünk. void foo() { for (...) { if (...) return; } assert false; }
• Elő- és utófeltételek, osztályinvariánsok A metódusok elő- illetve utófeltételei alatt olyan logikai feltételeket értünk, amelynek a metódus lefutása előtt vagy után teljesülnie kell. Ez a megközelítés az alapja a 4. szakasz - Szerződés alapú tervezésnek, amelyről még rövidesen szót ejtünk. Az osztályinvariáns olyan logikai feltétel, amely egy adott osztály minden példányára minden időpillanatban igaz (kivéve akkor, amikor az objektum épp állapotot vált). Segítségével az osztály adattagjai közötti viszony leírására nyílik lehetőség, például egy rendelést leíró objektum esetén ha a bankkártyás fizetés jelző logikai értékű attribútum igaz, akkor a bankkártyaszám attribútum értéke nem lehet null, A Java nyelv (szemben a szerződés alapú tervezéssel) nem igazán biztosít eszközrendszert az osztályinvariánsok megfogalmazására, ezért leginkább úgy járhatunk el, hogy ezeket az osztály minden metódusának elején és végén elvégezzük.
Fontos Metódusok előfeltételének ellenőrzését csak nempublikus metódusok esetén végezzük! A publikus metódusok az API részei, és ezért az ellenőrzéseket mindig el kell végezni, az állítások engedélyezett vagy letiltott állapotától függetlenül! Ellenkező esetben ugyanis futásidejű kivétel bekövetkezését kockáztatjuk, ami nem szerencsés.
Tipp 23 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
Ilyenkor használjuk a megfelelő futásidejű kivételeket, mint amilyen például az IllegalArgumentException, az IllegalStateException, az IndexOutOfBoundsException vagy a NullPointerException! Az alkalmazás normál működéséhez szükséges műveleteket sose állításokban fogalmazzuk meg, hiszen ha azok le vannak tiltva, akkor nem futnak le. Például ha a names nevű kollekcióból ki szeretnénk törölni egy null értéket, akkor az assert names.remove(null);
helyett ezt írjuk: boolean nullsRemoved = names.remove(null); assert nullsRemoved;
mivel így a null értékű elem törlése akkor is megtörténik, ha az állítások le vannak tiltva. Ide kapcsolódik, hogy ügyeljünk rá, hogy az állításainkban szereplő kifejezések mellékhatásmentesek legyenek, hacsaknem egy másik állításból használt állapot módosítását végzik! Az állítások engedélyezését a virtuális gép -ea vagy -enableassertions kapcsolóival végezhetjük különböző szemcsézettségi szinteken (a letiltás kapcsolói a -da illetve a -disableassertions): java [ -ea | -enableassertions ] [ :csomagnév... | :osztálynév ]
Példa: engedélyezzük az állításokat hu.unideb.inf.prt.main csomag elemeire!
a
hu.unideb.inf.prt
csomagjára
és
alcsomagjaira,
kivéve
java -ea:hu.unideb.inf.prt... -da:hu.unideb.inf.prt.main
Ha azt szeretnénk, hogy egy osztályt az osztálybetöltő csak akkor tudjon sikeresen betölteni, ha az állítások bekapcsolt állapotban vannak, a következő – szándékos mellékhatást tartalmazó – statikus inicializátor blokkot helyezzük el az osztályban: static { boolean assertsEnabled = false; assert assertsEnabled = true; // Szándékos mellékhatás !!! if (!assertsEnabled) throw new RuntimeException("Asserts must be enabled!!!"); }
Ha nincsenek engedélyezve, a szándékos mellékhatást tartalmazó sor értékadása nem kerül végrehajtásra, ezért a futásidejű kivétel kiváltása történik meg (és persze a statikus inicializátorban bekövetkező kivétel miatt az osztálybetöltés is sikertelen lesz). Ha engedélyezve vannak, akkor a mellékhatás miatt a kivétel nem kerül kiváltásra.
3. Annotációk Az annotációk metaadatok, úgy szolgáltatnak információkat egy programról, hogy annak nem részei. Metaadatként az annotációk nincsenek közvetlen hatással annak a kódnak a működésére, amelyet annotálnak. Az annotációk felhasználási területe elég széles: többek között információkat nyújthatnak a fordítóprogram számára, amelyeket az a hibák felderítésében vagy a megjegyzések elnyomásában hasznosíthat; különféle szoftverek az annotációk fordítási illetve telepítési idejű feldolgozásával kódot, XML-dokumentumokat, stb. tudnak generálni; bizonyos annotációk pedig futásidőben is elérhetőek. Az annotációk a Java nyelv 5-ös verziójában kerültek be a nyelvbe. Egy annotációtípus tulajdonképpen egy speciális interfésztípus, amelynek a nevét felhasználásuk során egy @ jel előzi meg. Egy meglehetősen ismert példa az @Override annotáció, amelynek megadása esetén a fordítónak ellenőriznie kell, hogy a metódus, amelyre előírták, felüldefiniálja-e a szuperosztály valamely metódusát, és ha nem, akkor fordítási hibát kell jeleznie. @Override public String toString() {
24 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
return "Name: " + name; }
Az annotációk mint interfészek metódusokkal (egész pontosan azok specifikációjával persze) is rendelkezhetnek, ezekre azonban speciális szabályok érvényesek: nem lehetnek generikusak, nem rendelkezhetnek paraméterrel, nem lehet throws-utasításrészük, a visszatérési érték típusa pedig csupán az alábbiak valamelyike lehet: • primitív típus (például int), • String, • Class vagy példányosítása, • felsorolásos típus (enum), • annotációtípus, vagy • ezek valamelyikéből képzett egydimenziós tömb. Terminológia: az annotációtípusok metódusait az annotáció elemeinek nevezzük. Vannak annotációk, amelyek nem rendelkeznek elemmel (ilyen a fenti példában szereplő @Override is), ezek az úgynevezett jelölőannotációk (vagy más néven marker annotációk). Ha egy annotációtípus csak egy elemmel rendelkezik, akkor azt konvenció szerint nevezzük value-nak. Az egyes elemek rendelkezhetnek alapértelmezett értékkel is. Egy annotációtípus defíníciójának szintakszisa:
2.1. példa - Jelölőannotáció-típus megadása public @interface Preliminary { }
2.2. példa - Egyelemű annotációtípus megadása public @interface InvokeMultiple { int value(); }
2.3. példa - Többelemű alapértelmezett értékekkel
annotációtípus
megadása
tömbtípusú
elemmel
és
public @interface ClassPreamble { String author(); String date(); int currentRevision() default 1; String lastModified() default "N/A"; String lastModifiedBy() default "N/A"; String[] reviewers(); }
2.4. példa - Összetett annotációtípus megadása public @interface Name { String firstName(); String lastName(); }
3.1. Metaannotációk A metaannotációk olyan annotációk, amelyekkel annotációtípusok annotálását végezhetjük. A metaannotációkat a java.lang.annotation csomag tartalmazza, a Java 7-ben négy ilyen van (a Java 8 ezt újabb kettővel megtoldotta): • @Documented: azt jelzi, hogy a javadoc és a hozzá hasonló eszközök dokumentálják-e a publikus API-ban az annotációtípust. 25 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
• @Target: azt adja meg, hogy az annotációt milyen Java elemekre szabad alkalmazni. Lehetséges értékei: • ElementType.ANNOTATION_TYPE: annotációtípusokra, • ElementType.CONSTRUCTOR: konstruktorokra. • ElementType.FIELD: adattagokra,. • ElementType.LOCAL_VARIABLE lokális változókra, • ElementType.METHOD metódusokra, • ElementType.PACKAGE csomagokra, • ElementType.PARAMETER formális paraméterekre, • ElementType.TYPE osztályra, interfészre (beleértve az annotációtípust is) vagy enumokra, • ElementType.TYPE_PARAMETER (Java 8 óta): típusparaméter-deklarációkra, • ElementType.TYPE_USE (Java 8 óta): típusfelhasználásokra. Ha egy annotációtípusra nem írjuk elő a @Target metaannotációt, akkor az annotációt típusparaméter kivételével a fentiek közül bármilyen Java elemre megadhatjuk. • @Inherited: azt jelzi, hogy az osztályokra előírt annotációtípus jelenlétére vonatkozó információt örököljéke az alosztályok. Ez alaphelyzetben nincs így, szóval ha egy osztályra előírunk egy annotációt, az alosztályai nem fognak ezzel rendelkezni, hacsaknem rájuk is explicit módon előírjuk. • @Retention: azt jelzi, hogy az ezzel az annotációtípus jelenlétéről szóló információkat mennyi ideig kell megőrizni. Három lehetséges értéke: • RetentionPolicy.SOURCE: forrásidejű megőrzés. A megjelölt annotációtípus csak a forráskód szintjén jelenik meg, a fordító már nem fordítja bele a bájtkódba. Ez tipikusan magának a fordítónak, illetve olyan forráskódelemző szoftvereknek szólnak, mint amilyenek az integrált fejlesztői keretrendszerek. A beépített annotációtípusok közül ebbe a csoportba tartozik a @Deprecated és a @SuppressWarnings. • RetentionPolicy.CLASS: bájtkódú megőrzés. Az annotáció jelenlétéről szóló információ bekerül a .class fájlba, de a virtuális gépnek nem kötelessége ezt betölteni. Ez az alapértelmezett megőrzési kategória. • RetentionPolicy.RUNTIME: futásidejű megőrzés. Nemcsak a .class fájlba kerül be az annotáció jelenlétéről szóló információ, de a virtuális gépnek is kötelessége betölteni, és futásidőben a reflection eszközrendszerén keresztül elérhetővé tenni. • @Native (Java 8 óta): jelzi, hogy az adott konstans értéket definiáló adattag értéke natív kódból is hivatkozható. • @Repeatable (Java 8 óta): azt jelzi, hogy az így jelölt annotációtípus ismételhető, vagyis ugyanarra a Java elemre akár többször is megadható. Példa saját annotációtípus létrehozására:
2.5. példa - Egyelemű annotációtípus kiegészítése metaannotációkkal @Documented @Target(ElementType.METHOD) @Retention{RetentionPolicy.RUNTIME} public @interface InvokeMultiple { int value(); }
2.6. példa - Többelemű annotációtípus kiegészítése metaannotációkkal 26 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
@Target(ElementType.TYPE) public @interface ClassPreamble { String author(); String date(); int currentRevision() default 1; String lastModified() default "N/A"; String lastModifiedBy() default "N/A"; String[] reviewers(); }
4. Szerződés alapú tervezés A szerződés alapú tervezést (design by contract), mint szoftvertervezési módszert Bertrand Meyer, az Eiffel programozási nyelv megalkotója fejlesztette ki a nyelv részeként az 1980-as közepén. Megközelítése szerint a szoftverek komponenseinek interfészeit formálisan leírt, részletes és ellenőrizhető specifikációkkal együtt kell megtervezni. Ilyen módon egy osztály interfésze kiegészül a metódusokhoz tartozó elő- és utófeltételekkel, valamint az osztályhoz tartozó invariánssal. Ezeket a specifikációkat „szerződéseknek” (contract) nevezi, az üzleti szerződésekben szereplő feltételek analógiájára. Meyer definíciója szerint egy szerződés „egy szoftverelem tulajdonságainak olyan specifikációja, amely befolyásolja, hogy potenciális kliensei hogyan használhatják”. A szerződést kötő két fél tehát az osztály és az azt használó kliens. Az osztály és kliensei között mintegy „ráutaló magatartásként” jön létre a szerződés, azáltal, hogy a kliens elkezdi használni az osztály által nyújtott szolgáltatásokat. Az osztály a klienseivel való kapcsolatok során vállalja, hogy tartja magát az interfészében definiált utófeltételekhez (és az osztályinvariánshoz), mindaddig, amíg a kliensek tartják magukat az előfeltételekhez. Ebből adódóan világosan elválnak egymástól a felek jogai és kötelességei. A szerződés tehát mindkét félre kötelezettségeket ró: a kliensnek be kell tartania az előfeltételt (az osztálynak pedig megvan a joga, hogy az előfeltételt be nem tartó kliensekkel ne foglalkozzon), és ekkor cserében joga van hasznot húzni az utófeltételből (amelynek a betartása ez esetben persze az osztály kötelessége).
Megjegyzés Ez a megközelítés hasonló ahhoz, mint amikor buszra szállunk, és jegyünket megváltva automatikusan elfogadjuk az utazási feltételeket, vagyis vállaljuk, hogy azokat betartva utazunk (ez tulajdonképpen előfeltétele a szolgáltatások igénybevételének). A tömegközlekedést szervező cég csak addig vállalja, hogy célba juttatja az utasokat, amíg azok betartják az előfeltételeket. Az előfeltételeket be nem tartó (például késő, vagy túlméretes csomagokkal érkező) utasokról a társaság nem állít semmit . A szerződés alapú tervezés igen jól használható szoftverkomponenseink alkalmazásprogramozói interfészének (API) tervezésekor, hiszen a szerződéses alapon meghatározott API-k esetében világos, hogy melyik félnek milyen kötelezettségei vannak. Fontos megjegyezni, hogy az elő- és utófeltételek illetve osztályinvariánsok a specifikáció részét alkotják!
A szerződés alapú tervezés alapelvei Előfeltétel alapelv Egy metódust meghívó kliensnek a hívás előtt meg kell bizonyosodnia arról, hogy az előfeltétel teljesül. Utófeltétel alapelv Egy metódusnak biztosítania kell, hogy amennyiben végrehajtásának kezdetén a hozzá társított előfeltétel teljesült, a végrehajtás végén a kapcsolódó utófeltétel is igaz legyen. Osztályinvariáns alapelv Az osztályinvariánsnak az objektum létrehozásától kezdve mindig igaznak kell lennie, minden, a kliensek számára elérhető metódus végrehajtása előtt és futtatása után.
Megjegyzés Az osztályinvariáns alapelv megfogalmazásából kitűnik, hogy a metódusok végrehajtása közben előfordulhat, hogy az invariáns nem teljesül. Ez természetes, hiszen a metódusokra úgy tekinthetünk,
27 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
mint amelyek az objektumot egyik állapotából egy másikba viszik át, és persze az átmenet során nem biztosítható mindig az invariáns teljesülése. Mindez persze nem okoz problémát, hiszen a kívülről megfigyelhető állapotokban (vagyis a metódushívások előtt illetve után) az invariánsok teljesülnek. Példaként tekintsünk egy kiegyensúlyozott bináris keresőfát megvalósító objektumot. Az ezt megvalósító osztály invariánsa (amelynek minden objektum minden megfigyelhető állapotában igaznak kell lennie) az lehet, hogy a fa minden elemének bal és jobb oldali részfáinak magasságkülönbsége legfeljebb 1. Ennek igaznak kell lennie minden műveletvégzés előtt illetve után, azonban a fába történő beszúrást, illetve az abból történő törlést megvalósító metódusok belsejében (törzsében) nem, hiszen ott ideiglenesen előállhat egy kiegyensúlyozatlan állapot. Ezt azonban a beszúrási illetve törlési algoritmus alapján még ezen metódusok belsejében különféle forgatások alkalmazásával ki kell egyensúlyozni annak érdekében, hogy mire a metódusok befejeződnek (és így az objektum újfent kívülről megfigyelhető állapotba kerül), az osztályinvariáns teljesüljön. Természetesen előfordulhat, hogy egy metódusnak nincs előfeltétele, például ha nincsenek elvárásai a kliensekkel szemben. Ebben az esetben implicit módon azonosan igaz (true) az előfeltétel (ezt értelemszerűen minden kliens mindig teljesíti). A szerződések leírásához szükséges elemek bizonyos programozási nyelvben a nyelv részét képezik (ilyen nyelv többek között az Eiffel, a D és az Ada 2012), más esetben ezek az eszközök valamilyen szabványos kiegészítésként jelennek meg (ilyen a .NET platformhoz kiadott Code Contracts nevű kiegészítés, amely többek között a C# nyelvű programok számára teszi elérhetővé az eszközöket), míg vannak olyan nyelvek (a C++ és a Java jelenleg ezek közé tartozik), amelyhez különféle, harmadik fél által készített programkönyvtárak biztosítják az elemeket. A Java nyelvhez készített ilyen eszközök közül kiemelendő a Java Modeling Language (JML) nevű interfészspecifikációs nyelv, amely nemcsak a szerződések leírására, de egyebek mellett formális verifikációra is alkalmas, illetve a JML-hez kapcsolódó OpenJML eszköz, azonban ennek bonyolultsága miatt részletesebb bemutatásra inkább egy, a Google által fejlesztett programkönyvtárat, a Contracts for Java-t választottuk, amelyben annotációk segítségével adhatjuk meg a szerződések elemeit. A szerződés alapú tervezés persze nem váltja ki a tesztelést, leginkább csak mint annak egyféle kiegészítését tekinthetjük, amelynek segítségével hamarabb deríthetjük fel a hibákat. Mindemellett a szerződések betartásának ellenőrzése nyilvánvaló többletterhelést ró egy rendszerre. Fontos azonban látnunk, hogy egy hibamentes program végrehajtása során a szerződések feltételei sosem sérülhetnek, éppen ezért éles környezetben nem is nagyon van szükség az ellenőrzések elvégzésére, elegendő csupán a tesztelés, illetve a belövés során alkalmazni a futásidejű ellenőrzéseket.
4.1. Szerződések és az öröklődés A szerződés alapú tervezés elméletének egyik fontos következménye, hogy jobb megértését biztosítja olyan fontos objektumorientált alapfogalmaknak, mint amilyen az öröklődés, a polimorfizmus, vagy éppen a dinamikus kötés. Meg kell vizsgálnunk, hogy egy szuperosztály–alosztály viszonyban milyen szerep jut az egyes szerződésfajtáknak. Az első szabály az invariánsokra vonatkozik. Mivel az öröklődés egy „is-egy” (angolul is-a) kapcsolat (például egy Hallgató is egy Személy), amely magával vonja a helyettesíthetőséget is (vagyis ahol a Személy osztály egy objektumára van szükség – például személyek egy kollekciójában –, használható a Hallgató osztály valamely objektuma is), ezért minden olyan megszorításnak, amelyet a szülőosztály példányaira előírtunk, a leszármazottakra is érvényesnek kell lennie. Vagyis egy osztály a szülőjétől nemcsak adattagjait és metódusait, de osztályinvariánsát is örökli! Annak vizsgálata, hogy mi történik az elő- és utófeltételekkel, a szoftverfejlesztés egy fontos szabályához vezet el. Nyilvánvaló, hogy a leszármazott (a szerződések szempontjából „alvállalkozó”) osztály egyetlen metódusának előfeltétele sem lehet erősebb a szuperosztálybéli metódusáénál, hiszen ha így lenne a helyettesíthetőség nem valósulhatna meg, mivel egy klienst, amely a szuperosztály előfeltételének betartására készül fel, váratlanul érne egy attól erősebb elvárás, amit nem is feltétlenül lenne képes betartani. Hasonló okból nem lehet az alszerződő metódusainak utófeltételét gyengíteni, hiszen a kliensek számítanak az eredeti utófeltétel teljesülésére. Ezek a gondolatok vezetnek el a második szabályhoz: egy felüldefiniált metódus megtarthatja, vagy gyengítheti az előfeltételt, és megtarthatja, vagy erősítheti az utófeltételt! A feltételek gyengítésére illetve erősítésére az alábbi két logikai törvény adja az elméleti megalapozást:
28 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök 1. p ⊃ (p ∨ q) 2. (p ∧ q) ⊃ p
4.2. Contracts for Java (cofoja) A cofoja elnevezésű ingyenes, nyílt forrású programkönyvtárat (honlapja: http://code.google.com/p/cofoja/) a Google fejlesztette ki, annak érdekében, hogy a szerződéselemeket könnyen olvasható (és könnyen írható) módon lehessen megadni. A cofoja négyféle szerződésfajtát támogat:
2.1. táblázat - A cofoja annotációi A szerződés típusa
Annotáció (com.google.java.contract.*)
Mikor kerül ellenőrzésre?
Örökl ődés
Invariáns ok
Invariant
Előfeltéte lek
Requires
Metódusba történő belépéskor
vagyol va
Utófeltéte lek
Ensures
Metódusból történő szabályos kilépés
éselve
Kivételes utófeltétel ek
ThrowEnsures
Metódusból történő szabálytalan kilépés (ha kivétel dobódik)
éselve
A publikus és csomag szintű metódusokba éselve történő belépéskor, illetve publikus és csomag szintű metódusokból és a konstruktorokból való kilépéskor
A szerződések megírását megkönnyítendő, két speciális változót is bevezet:
2.2. táblázat - A cofoja pszeudováltozói Kulcss zó
Mivel használható együtt?
Példa
Jelentés
old
Ensures, ThrowEnsures
old(size()) == size() + 1
A kifejezés a metódusba történő belépéskori állapota.
result
Ensures
result != null
A metódus által visszaadott érték. Csak nem-void metódusok esetén használható.
A szintakszis nagyon egyszerű, mindössze a megfelelő szerződést meghatározó annotációt kell alkalmaznunk: @SzerződésAnnotáció("kifejezés") SzerződésselEllátottDeklaráció ...
Ha a szerződésben több kifejezést is meg szeretnénk adni (ezek ilyenkor összeéselődnek): @SzerződésAnnotáció({ "kifejezés1", "kifejezés2", ... }) SzerződésselEllátottDeklaráció ...
Példa: import com.google.java.contract.*; @Invariant({ "osztályinvariáns 1", "osztályinvariáns 2"
29 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
}) class MyClass { @Requires("metódus előfeltétele") @Ensures("metódus utófeltétele") SomeType someMethod(...) { ... } @Requires({ "több részes", "előfeltétel" }) @ThrowEnsures({ "SomeException", "kivételes utófeltétel" }) AnotherType anotherMethod(...) { ... } }
4.2.1. Eclipse és cofoja Ahhoz, hogy Eclipse-ben élvezhessük a szerződés alapú fejlesztés előnyeit, néhány beállítást el kell végeznünk. Első lépésként biztosítanunk kell, hogy az Eclipse fejlesztőkörnyezetet egy JDK (ne pedig csak egy JRE) futtassa. Ezt az eclipse.ini konfigurációs állományban a -vm opció értékének kell a JDK bin könyvtárának elérési útját adni, vagyis az alábbi két sort kell megadni, a -vmargs opciót megelőzően: -vm
Példa: -vm c:/Program Files/Java/jdk1.7.0_51/bin -startup plugins/org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar --launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.200.v20140116-2212 -product org.eclipse.epp.package.java.product --launcher.defaultAction openFile --launcher.XXMaxPermSize 256M -showsplash org.eclipse.platform --launcher.XXMaxPermSize 256m --launcher.defaultAction openFile --launcher.appendVmargs -vmargs -Dosgi.requiredJavaVersion=1.6 -Xms40m -Xmx512m
A beállítást követően, amennyiben az Eclipse éppen fut, újra kell azt indítani. Emellett be kell szereznünk magát a Contracts for Java osztálykönyvtárat, amelyet a cofoja honlapjáról tölthetünk le (jelen sorok írásakor a legfrissebb változat a cofoja-1.1-r150.jar névre hallgat). Ezt mentsük el, majd adjuk ahhoz a projekthez, amelyben a szerződéseket használni szeretnénk (jobbklikk a projekt nevén → Properties → Java Build Path → Libraries fül → Add JARs... vagy Add External JARs..., attól függően, hogy a munkaterületünkre (workspace) mentettük-e a fenti jar fájlt, avagy sem)! Miután ily módon a classpath-hoz adtuk a fenti jar-t, a com.google.java.contract csomag elemei használhatóak. A cofoja használatát az alábbi egyszerű példán keresztül mutatjuk be: 1 package hu.unideb.inf.prt.contracts; import com.google.java.contract.Ensures; import com.google.java.contract.Requires; 5 public class Contracts {
30 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
public static void main(String[] args) { System.out.println(new Numbers().add(-10, 5)); } 10 } class Numbers { @Requires({ "c > 0", 15 "b > 0" }) @Ensures({ "result > a", "result > b" 20 }) int add(int a, int b) { return a - b; } }
Észrevehető, hogy a Numbers osztály add metódusának feltételei nem feltétlenül helyesek (például c változó sehol sem szerepel), de a bemutató szempontjából ez azért nem baj, mert így nyílik lehetőség annak bemutatására, hogy a fejlesztőkörnyezet hogyan hívja fel a figyelmet a hibákra. A projekt tulajdonságai között (Project Properties) a Java Compiler → Annotation Processing beállítások között mindhárom jelölőnégyzet beállítása után a New... gomb segítségével vigyük fel az alábbi kulcs–érték párokat, mint ahogyan az az Annotációfeldolgozás beállítása ábrán is látható:
2.3. táblázat - Az annotációfeldolgozó számára beállítandó kulcs–érték párok Kulcs
Érték
com.google.java.contract.classoutput
%PROJECT.DIR%/bin
com.google.java.contract.classpath
%PROJECT.DIR%/lib/cofoja-1.1-r150.jar
com.google.java.contract.sourcepath
%PROJECT.DIR%/src
Megjegyzés Értelemszerűen a fenti változók értékei rendre: a könyvtár, ahová a lefordított állományok kerülnek, a cofoja keretrendszert tartalmazó, korábban letöltött jar fájl, valamint a szerződéseket tartalmazó források könyvtára. A %PROJECT.DIR% változó segítségével mindezeket a projekt könyvtárához képest relatív módon is megadhatjuk.
2.1. ábra - Annotációfeldolgozás beállítása
31 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
Ugyanitt a Factory Path beállításai között szintén be kell állítani a letöltött jar-t, ahogyan az az Annotációfeldolgozót tartalmazó jar beállítása ábrán látható.
2.2. ábra - Annotációfeldolgozót tartalmazó jar beállítása
Egyből meg is kapjuk a c változóról, hogy problémás. Ha ez nem történne meg, akkor a Window menü Preferences menüpontjának kiválasztásával a General → Workspace oldalon be kell állítani a munkaterület automatikus frissítését. Hibák a szerződésekben ábrán látható.
2.3. ábra - Hibák a szerződésekben
32 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
Ez azonban csak arra szolgál, hogy az IDE statikusan megjelölje a felderített problémákat, ettől azonban a szerződések futásidejű betartatása nem történik meg, amit könnyen ellenőrizhetünk, ha lefuttatjuk az alkalmazást. Minden további nélküli lefut. A futásidejű ellenőrzéshez ugyanis még egy parancssori virtuálisgépargumentumot is meg kell adnunk a -javaagent:<jar fájl> opció segítségével. Ezt az Eclipse-ben a Run Configurations... ablak Arguments fülén, a VM arguments szövegmezőben adhatjuk meg. Példa: javaagent:${project_loc}/lib/cofoja-1.1-r150.jar. A Futásidejű szerződésellenőrzés ábrán látható, hogy a pop művelet üres veremre történő meghívása a size() >= 1 előfeltétel megsértése miatt a PreconditionError hibát okozza.
2.4. ábra - Futásidejű szerződésellenőrzés
4.2.2. Példa: verem megvalósítása szerződésekkel Kiinduló példának tekintsük egy verem meglehetősen egyszerű interfészét, amely két metóduból áll: az egyikkel a verem legfelső elemét ki lehet törölni, míg a másikkal egy új elemet lehet hozzáadni a veremhez. interface Stack { public T pop(); public void push(T obj); }
Erre az egyszerű interfészre azonban nem könnyű szerződéseket definiálni. Ez azért van, mert az interfész nem biztosít lekérdező metódust, amely anélkül tenné lehetővé az objektum állapotának vizsgálatát, hogy meg kellene változtatni azt. Annak érdekében, hogy egy objektum állapotára vonatkozóan szerződéseket készítsünk, először is hozzá kell férnünk ehhez az állapothoz. Egy ilyen egyszerű információ lehet a verem aktuális elemszáma. Bővítsük tehát interfészünket egy méretlekérdező metódussal:
33 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
interface Stack { public int size(); public T pop(); public void push(T obj); }
Bár a Java nyelvben az int típus előjeles egészt reprezentál, nyilvánvló, hogy egy veremben sosem lehet negatív számú elem. Elkészíthetjük hát első szerződésünket, egy osztályinvariánst, mivel ennek az állításnak az osztály minden objektumára minden időpillanatban igaznak kell lennie. import com.google.java.contract.Invariant; @Invariant("size() >= 0") interface Stack { public int size(); public T pop(); public void push(T obj); }
Ahogy a példából látható, magát a feltételt idézőjelek között (vagyis sztringként) kell megadni. A sztringbe viszont (néhány később ismertetendő kivétellel) érvényes Java kódot kell írni. Ennél azért többet is ki tudnánk fejezni az eszközrendszer segítségével. Például nyilvánvaló, hogy az üres veremből nem szabad törölni, vagyis a veremből törlés előfeltétele, hogy a veremben legalább egy elem legyen. 1 import com.google.java.contract.Invariant; import com.google.java.contract.Requires; @Invariant("size() >= 0") 5 interface Stack { public int size(); @Requires("size() >= 1") public T pop(); 10 public void push(T obj); }
Jól látható, hogy a szerződések segítségével hogyan valósul meg az API-tervezés és dokumentálás, ha arra gondolunk, hogy ez a megoldás egy igen konzervatív szemléletmódot tükröz: a klienseknek biztosítaniuk kell, hogy a verem nem üres, hiszen ez a feltétel a törlés előfeltétele. Ekkor a kliens kódja valahogy így nézhet ki (feltételezve, hogy s egy Stack típusú objektum): if (s.size() > 0) s.pop();
Amennyiben a kliens nem így jár el, és az előfeltétel vizsgálata nélkül végzi a pop metódus hívását, azt kockáztatja, hogy a futásidejű előfeltétel-ellenőrzésen a program elbukik. Azonban az objektum és kliensének viszonya ennél sokkal liberálisabb is lehet, például a pop abban az esetben, ha az üres veremből próbálna meg a kliens elemet kivenni, 1. null értéket is visszaadhatna, 2. kiválthatna egy kivételt. Ezekben az esetekben a kliens nem kényszerülne rá, hogy a pop művelet meghívását a fentebb látható módon őrfeltétellel lássa el. Bármikor meghívható volna a metódus, és a visszatérési érték alapján vagy az alapján, hogy bekövetkezett-e a kivétel, dönthetné el a kliens, hogy a műveletvégzés sikeres volt-e, avagy sem. Az 1. esetben a pop művelet szerződése a következő módon alakulna (a fenti osztály 8-9. sora): 8 9
@Ensures( "size() != 0 || result == null") public T pop();
Figyeljük meg, hogy az előfeltétel eltűnt, vagyis ennek a metódusnak ez esetben nincs előfeltétele, viszont megjelent egy utófeltétel, amely tulajdonképpen egy implikációt ír le: abból, hogy a méret 0 (vagyis a verem üres) következik, hogy a visszaadott értéknek (result) null-nak kell lennie. Ennek logikailag ekvivalens átirata a fenti példában látható. 34 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
A 2. esetben a pop művelet szerződésének formája a következő lehetne: 8 9
@ThrowEnsures({"EmptyStackException", "size() == 0"}) public T pop();
Itt a ThrowEnsures használatára láthatunk példát. Itt kivétel–utófeltétel párokat adhatunk meg, tetszőleges számban. Ha az utófeltétel igaz, az adott kivétel kiváltódására számítunk. Jó lenne azt is leírni, hogy mit tudunk az objektum állapotáról az egyes metódusok lefutását követően. Tudjuk, hogy ha a veremből kiveszünk egy elemet, akkor a verem elemszáma eggyel csökken, míg ha bővítjük a vermet, akkor eggyel több elem lesz benne. Ezek leírására utófeltételeket használhatunk: import com.google.java.contract.Ensures; import com.google.java.contract.Invariant; import com.google.java.contract.Requires; @Invariant("size() >= 0") interface Stack { public int size(); @Requires("size() >= 1") @Ensures("size() == old(size()) - 1") public T pop(); @Ensures("size() == old(size()) + 1") public void push(T obj); }
Itt a szerződésfajták megadására szolgáló annotációk mellett egy új elemmel, az old kulcsszóval találkozunk, amely egyike azoknak a bővítéseknek, amelyeket a cofoja keretrendszer támogat. Az old kulcsszó a régi érték vizsgálatát teszi lehetővé. Régi érték alatt az old paramétereként megadott kifejezésnek a metódushívás kezdetén érvényes értékét értjük. Az eddig megalkotott szerződéssel bíró interfész implementálásához még csak veremre sincs szükség, egy sima számláló változó segítségével is képesek lehetünk átverni szerződéseinket, hiszen semmit nem mondtunk az elemekről magukról, csak a verem méretére vonatkozóan írtuk elő feltételeinket. Nem nyilvánvaló azonban, hogy ennél mindenképpen tovább kell-e mennünk, hiszen a szerződés alapú tervezés nem követeli meg, hogy a program teljes specifikációját leírjuk. A szerződéseinket tovább erősíthetjük, ha nem csak az elemszámra, de a tartalomra vonatkozóan is előírásokat teszünk. A top művelet legfelső elemhez történő hozzáférést valósítja meg, anélkül, hogy az adott elemet kivennénk a veremből. Az előfeltétel persze ugyanaz, mint a pop esetében. import com.google.java.contract.Ensures; import com.google.java.contract.Invariant; import com.google.java.contract.Requires; @Invariant("size() >= 0") interface Stack { public int size(); @Requires("size() >= 1") public T top(); @Requires("size() >= 1") @Ensures("size() == old(size()) - 1") public T pop(); @Ensures("size() == old(size()) + 1") public void push(T obj); }
A top művelet segítségével most már lehetőségünk van a push és pop műveletek szerződését olyan módon bővíteni, hogy a tartalomra vonatkozó állítások is megjelenjenek. A pop által visszaadott elemnek (result) ugyanannak kell lennie, amely a pop hívást megelőzően a verem tetején volt, míg a push művelet esetén az
35 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
utófeltétel egy olyan részfeltétellel bővül, amely azt állítja, hogy a művelet elvégzése után a verem tetején a push által beszúrt elem áll. import com.google.java.contract.Ensures; import com.google.java.contract.Invariant; import com.google.java.contract.Requires; @Invariant("size() >= 0") interface Stack { public int size(); @Requires("size() >= 1") public T top(); @Requires("size() >= 1") @Ensures({ "size() == old(size()) - 1", "result == old(top())" }) public T pop(); @Ensures({ "size() == old(size()) + 1", "top() == old(obj)" }) public void push(T obj); }
A példából az is látható, hogy a cofoja elvárásai szerint hogyan lehet több részfeltételt egyetlen feltételbe gyömöszölni. Értelemszerűen ez nemcsak az utó-, de az előfeltételekre és az invariánsokra is ugyanilyen módon megadható. Az egyetlen annotációban megadott részfeltételek összeéselődnek. Készítsünk egy, a fenti interfészt megvalósító konkrét osztályt, amely a vermet egy ArrayList becsomagolásával valósítja meg! import com.google.java.contract.Ensures; import com.google.java.contract.Invariant; import java.util.ArrayList; @Invariant("elements != null") public class ArrayListStack implements Stack { protected ArrayList elements; public ArrayListStack() { elements = new ArrayList(); } public int size() { return elements.size(); } public T top() { return elements.get(elements.size() - 1); } public T pop() { return elements.remove(elements.size() - 1); } @Ensures("elements.contains(old(obj))") public void push(T obj) { elements.add(obj); } }
Ennek az osztálynak alig van saját jogú szerződése, azonban az általa implementált Stack interfész összes szerződését megörökli. Egy szerződéssel ellátott típus az összes szülőjének (ősosztályának, illetve az általa implementált interfészeknek) minden szerződését megörökli. Ilyen módon a szerződéseket a szuperosztály– alosztály illetve interfész–implementáló osztály viszonyrendszerekben finomítani lehet. Fontos észben tartani, 36 Created by XMLmind XSL-FO Converter.
Haladó programnyelvi eszközök
hogy a felüldefiniált metódusokkal törzsével ellentétben a szerződések nem az eredeti helyett, hanem azt kiegészítve értelmezendőek. Invariánsok és utófeltételek esetén az öröklött szerződésből származó és az újonnan megadott feltételek összeéselődnek, míg az előfeltételek esetén vagy művelettel kombinálhatóak össze. Más szóval, egy metódus mindig legalább annyi állapotot és argumentumot fogad el a bemeneten, mint a szülője (vagyis az előfeltételek csak gyengíthetőek), és legalább annyi garanciát nyújtania kell, mint a szülőjének (vagyis az utófeltételek és invariánsok csak erősíthetőek), máskülönben nem volna ahelyett használható (ami ellentmondana a helyettesíthetőség elvének). Érdemes megjegyezni, hogy az utolsó példában egy protected adattagra hivatkozó feltételeket adtunk meg. Egy szerződés hatásköre megyegyezik azon metódusdeklarációéval, amelyre vonatkozik, épp ezért pontosan azokat az adattagokat éri el és pontosan azokat a metódusokat hívhatja meg. A korlátozott hozzáféréssel rendelkező tagok elérésére tehát van lehetőség, de a szerződéseinket célszerű nyilvánossá tenni, amikor csak lehet, mert így mintegy dokumentációként is szolgálhatnak. Ezen túlmenően, a korlátozott hozzáférésű tagokra történő hivatkozás nemcsak ellenjavalt, de valójában értelme sem nagyon van.
37 Created by XMLmind XSL-FO Converter.
3. fejezet - Szoftvertesztelés A szoftvertesztelés fő feladata, hogy megbizonyosodjunk arról, hogy a szoftver az elvárásoknak megfelelően működik. Ez a tevékenység (illetve az ettől némileg általánosabb verfikációs és validációs folyamat) gyakorlatilag felöleli a teljes szoftveréletciklust, a követelmények vizsgálatától kezdve a szoftverterv áttekintésén át egészen az elkészült implementáció (maga a termék) tesztelésésig. Ráadásul, mivel az evolúciós fázisban a rendszer tovább változik, annak során is szükség lesz tesztelési tevékenységekre. Mivel jelen jegyzet középpontjában a megvalósítás áll, ezért a továbbiakban csak ebből a szemszögből foglalkozunk e folyamattal. A verifikációs és validációs tevékenységeket többféleképpen is csoporthatjuk. Az egyik csoportosítás alapja a szoftver vizsgálatához szükséges reprezentációján alapul. Ebből a szempontból beszélhetünk statikus illetve dinamikus tesztelésről. 1. A statikus tesztelés során nincs szükség a program lefuttatására. Ezt statikus programelemző eszközök alkalmazásával, illetve kódátvizsgálások segítségével végezhetjük, melynek során a forráskódot akár többen is áttekintik, hiányosságok illetve hibák után kutatkodva benne. 2. A dinamikus tesztelés során a program lefuttatásával ellenőrizzük, hogy az megfelelően működik-e. Ez átalában magával vonja annak ellenőrzését, hogy megadott bemeneti értékek esetén a program az elvárt kimenetet produkálja-e. A fejlesztés folyamatában betöltött szerepe alapján az alábbi csoportosítás végezhető: 1. Egységtesztelés (más néven modultesztelés vagy komponenstesztelés): az egyes programegységeket (például modulokat, osztályokat) egymástól függetlenül, izolálva teszteljük. Általában az adott egység fejlesztését végző programozó végzi. 2. Integrációs tesztelés: az egyes komponensek közötti interfészek, valamint az operációs rendszerrel, állományrendszerrel, hardverrel vagy más rendszerrel való interakciók tesztelését végezzük. Az integrációt végző fejlesztő, vagy (szerencsésebb esetben) egy direkt e célra létrehozott integrációs tesztelő csapat végzi. Az integrációs tesztelésnek több szintje is lehet, különféle célokkal. Tesztelhetjük a komponensintegrációt vagy a rendszerintegrációt. 3. Rendszertesztelés: A teljes rendszer (vagyis a késztermék) viselkedésének vizsgálatával foglalkozik. Általában ez az utolsó, a fejlesztést végző szervezet részéről elvégzett tesztelésfajta. 4. Elfogadási tesztelés: olyan felhasználó vagy ügyféloldali tesztelés, amelynek célja, hogy megbizonyosodjanak arról, hogy az elkészült rendszer mgfelel-e a célnak, elfogadható-e a felhasználók/ügyfelek számára. Jelen jegyzetben ezekből a fő hangsúlyt az egységtesztelésre helyezzük, az egyéb tesztelési szintek kívül esnek a tárgyalandó témákon. A szoftver verifikációja és validációja során a program hiányosságai rendes körülmények között kiderülnek, így a kódot módosítani kell a hiányosságok kiküszöbölése érdekében. Ezt a belövési folyamatot gyakran más verifikációs és validációs tevékenységekkel ötvözik. A tesztelés (vagy általánosabban, a verifikáció és a validáció) és a belövés azonban olyan különálló folyamatok, amelyeket nem kell integrálni: 1. A verifikáció és validáció az a folyamat, amely megállapítja, hogy egy szoftverrendszerben vannak-e hiányosságok. 2. A belövés az a folyamat, amely behatárolja és kijavítja ezeket a hiányosságokat. A programok belövésére nem létezik egyszerű módszer. A jó hibakeresők mintákat keresnek a teszt kimenetében, és a hiányosság típusának, a kimeneti mintának, a programozási nyelvnek és a programozási folyamatnak az ismeretében behatárolják a hiányosságot. A belövés során felhasználhatjuk a szokásos programozói hibákról (például egy számláló megnövelésének elmulasztása) szóló tudásunkat, és azokat illesztjük a megfigyelt mintákra. A programozási nyelvre jellemző hibákat – például rossz mutatóhivatkozás Cben – is figyelembe kell vennünk. A hibák behatárolása egy programban nem mindig egyszerű folyamat, mivel a hiba nem szükségszerűen annak a pontnak a közelében van, ahol a sikertelenség bekövetkezett. A programhiba behatárolásához a belövésért 38 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
felelős programozónak esetleg olyan újabb teszteket kell megterveznie, amelyek megismétlik az eredeti hibát, és segítik a hiba forrásának felderítését. Szükség lehet a program végrehajtását szimuláló kézi nyomkövetésre is. Bizonyos esetekben a program futásáról információt gyűjtő belövő eszközök is igen hasznosak lehetnek. Az interaktív belövő eszközök általában a fordítórendszerrel integrált nyelvi segédeszközökből álló programcsomagok részét képezik. Ezek speciális végrehajtási környezetet biztosítanak a programnak, amely hozzáférést tesz lehetővé a fordítóprogram szimbólumtáblájához, ezáltal a program változóinak értékeihez. A felhasználók gyakran utasításról utasításra „lépegetve” vezérelhetik a végrehajtást. Az utasítások végrehajtását követően, a változók értékeit megvizsgálva, a lehetséges hibák felderíthetők. A belövési folyamatról és eszköztámogatásáról részletesebben a 1. szakasz - Belövés szakaszban lesz szó.
1. Belövés Hibátlan program nem létezik. Éppen ezért roppant fontos, hogy amikor egy programhibára fény derül, hatékonyan meg tudjuk keresni, hogy a program mely része a hibás, hiszen ez elengedhetetlen a hiba kijavításához..A belövés (debugging) az a folyamat, amely behatárolja és kijavítja a szoftverben talált hiányosságokat. A folyamat során interaktív módon futtatjuk az alkalmazást, láthatjuk a forráskódban, hogy hol tart a végrehajtás, miközben lehetőségünk van egyes változók illetve kifejezések aktuális értékének a vizsgálatára is. A forráskódba töréspontokat (breakpoint) is elhelyezhetünk, amelyek segítségével azt befolyásolhatjuk, hogy hol kerüljön felfüggesztésre a program végrehajtása. Annak érdekében, hogy a végrehajtást egy adattag értékének olvasásához illetve módosításához kötődően függesszük fel, figyelőpontokat (watchpoint) hozhatunk létre. A töréspontokat és figyelőpontokat összefoglaló néven megállási pontoknak is nevezzük. Miután a program végrehajtása felfüggesztésre került, vizsgálhatjuk a változók értékeit, módosíthatjuk azokat, stb. A különféle integrált fejlesztői környezetek (így természetesen az Eclipse is) általában lehetőséget biztosítanak arra, hogy programjainkat belövési üzemmódban (Debug mode) futtassuk. Az Eclipse rendelkezik egy speciális belövési perspektívával (Debug perspective, amelyet a Window menü Open perspective almenüjének Debug menüpontjával hívhatunk elő), amely előre definiált nézetek segítségével teszi lehetővé, hogy vezéreljük programunk végrehajtását és vizsgáljuk változóink állapotait.
1.1. Belövés Eclipse-ben Töréspont létrehozásához a forráskódszerkesztő bal oldalán elhelyezkedő margóra kattintva kell kiválasztanunk a Toggle Breakpoint (Ctrl+Shift+B) elemet (Töréspont beállítása ábra), vagy egyszerűen csak duplán kell kattintanunk a margóra.
3.1. ábra - Töréspont beállítása
39 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
Ezt követően a margón megjelenő kis kék kör jelzi, hogy a 6. sorra töréspontot állítottunk be (A beállított töréspont ábra).
3.2. ábra - A beállított töréspont
Az alkalmazás belövéséhez a végrehajtandó Java alkalmazás futtatására van szükség, amelyet a megfelelő Java forráson jobb gombbal történő kattintással vagy az eszköztár ikonjának lenyitásával előnyíló menü Debug As almenü Java Application menüpontját kiválasztva tudunk elvégezni, ahogyan azt a Debug perspektíva ábra is mutatja.
3.3. ábra - Belövés indítása
40 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
A belövő elindításakor az Eclipse rákérdez, hogy szeretnénk-e átváltani a belövést segítő perspektívába, mihelyst egy megállási ponthoz elérünk. Itt válasszuk a Yes lehetőséget, ekkor az Eclipse a Debug perspektíva ábrán látható módon megnyitja a Debug perspektívát.
3.4. ábra - Debug perspektíva
Itt azt látjuk, hogy a program futtatása megkezdődött, de a korábban beállított töréspontunknál a végrehajtás felfüggesztésre került a 6. sorban szereplő utasítás végrehajtását megelőzően. A vizsgált program végrehajtásának vezérlésére az eszköztáron találunk gombokat, amelyeket természetesen gyorsbillentyűkkel is kiválthatunk. Az e célból használható főbb gyorsbillentyűket az alábbi táblázat foglalja össze:
3.1. táblázat - A Debug perspektíva gyorsbillentyűi Ikon
Gyorsbillentyű F8
41 Created by XMLmind XSL-FO Converter.
Leírás Arra utasítja a belövőt, hogy mindaddig folytassa a program végrehajtását, amíg a következő megállási pontot el nem érjük (illetve további megállási pontok hiányában a program végéig).
Szoftvertesztelés
Ikon
Gyorsbillentyű Ctrl+F2
Leírás Leállítja a belövést.
F5
Végrehajtja az aktuálisan kiválasztott sor utasításait, és a következő sorra lép. Ha a kiválasztott sorban metódushívás szerepel, akkor a belövő belép annak kódjába.
F6
Átlépi a hívást, vagyis úgy hajtja végre a metódust, hogy a belövő nem lép bele.
F7
Kilép az aktuálisan végrehajtott metódus hívójához. Ez azt jelenti, hogy az aktuálisan végrhajtott metódus befejeződik, és a végrehajtás a hívás helyén folytatódik.
Shift+F5
Be/kikapcsolja használatát.
a
lépésszűrők
3.5. ábra - A Debug nézet gyorsbillentyűi
A bal felső sarokban (Debug nézet) a végrehajtás során mindig az aktuális hívási lánc látható. A Hívási lánc a Debug nézetben ábra példáján az látszik, amikor a főprogram 7. sorában szereplő counter.count() metódushívásba belelépve éppen a Counter.java forrásállomány 11. sorában lévő utasítás végrehajtása következik. Ebben a pillanatban a hívási láncon a Main osztály main(String[]) metódusa (vagyis a főprogram) és a Counter osztály count() metódusa szerepel.
3.6. ábra - Hívási lánc a Debug nézetben
1.1.1. Breakpoints nézet 42 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
A Breakpoints nézet segítségével a megállási pontok (törés- és figyelőpontok) kezelése valósítható meg. Lehetőség van ezen pontok törlésére, passzívvá változtatására, illetve különféle tulajdonságaik módosítására.
3.7. ábra - Breakpoints nézet
Az összes töréspont egyidejűleg a
(Skip All Breakpoints) gombra kattintva hagyható figyelmen kívül.
1.1.2. Variables nézet A Variables nézetben ( Variables nézet ábra) az aktuális hívási lánc lokális változói, illetve objektumainak adattagjai jeleníthetőek meg. Csak a láthatósági- és élettartamszabályoknak megfelelő változók láthatóak itt. A lenyíló menü ( ) Java almenüjének menüpontjai segítségével beállítható, hogy a listában megjelenjenek-e a nevesített konstansok (static final), az osztály szintű (static) adattagok, a teljesen minősített nevek, a null értékű tömbreferenciák (alapértelmezés szerint ezek nem jelennek meg), illetve általában véve a referenciák (alapértelmezés szerint referencia helyett a hivatkozott objektum maga látszik). A Layout almenü Select Columns... menüpontjának segítségével a változók alapértelmezés szerint megjelenített nevén és értékén túlmenően beállíthatjuk a deklarált illetve tényleges típusának, a példányazonosítónak és a példányszámláló értékének a kijelzését is.
3.8. ábra - Variables nézet
Lehetőség van a változók értékének futásidejű megváltoztatására, ami megengei, hogy ha úgy találjuk, hogy egy változó értéke nem megfelelően került beállításra, akkor az adott belövési folyamatban ideiglenesen megváltoztatva ezt az értéket újrafuttatás nélkül vizsgáljuk, hogy az újonnan beállított érték megfelelő-e. Mindehhez a Változóérték megváltoztatása ábrán látható módon egyszerűen át kell írni az értéket, és a végrehajtás további részében a módosult érték figyelembe vételével zajlik a futtatás.
3.9. ábra - Változóérték megváltoztatása 43 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
Egy változó értéke alapértelmezetten a toString() metódus által visszaadott érték felhasználásával kerül megjelenítésre, azonban ez az alapértelmezés felülírható. Megadhatunk egy részletes formázót, amelyben Java kódban írhatjuk le, hogyan szeretnénk a változó értékét megjeleníteni. Például a Counter osztály esetén a toString() metódus nem szolgál túlságosan értelmes információkkal (hu.unideb.inf.prt.debugging.Counter@1224b90 a megjelenített érték), ezért ennek olvashatóbbá tétele érdekében (persze a toString() metódus felüldefiniálásának lehetősége mellett) a megfelelő változó jobb egérgombra kattintással előhívható környezeti menüjéből a New Detail Formatter... menüpont kiválasztásával adhatjuk meg, miképpen kellene az objektum megjelenítését elvégezni. A Részletes formázó hozzáadása ábrán látható példában a Counter osztály getResult() metódusát használjuk e célra.
3.10. ábra - Részletes formázó hozzáadása
1.1.3. Expressions nézet Az Expressions nézet (Window → Show view → Expressions) segítségével adatok vizsgálata végezhető el. Gyakori probléma, hogy a futó programnak számos, a Variable nézetben megjelenő változója van, azonban ezek közül csak néhányra vagyunk kíváncsiak, illetve nem pontosan valamely változó értéke érdekes, hanem egy komplex kifejezésé. Ekkor használhatjuk az Expressions nézetet ( Expressions nézet ábra), amely automatikusan megjelenítődik, ha hozzáadunk egy új kifejezést. Egy kifejezést legegyszerűbben a nézethez adni a forráskódban a kifejezést kijelölve, annak környezeti menüjéből a Watch menüpont kiválasztásával, illetve a nézet ) funkciójával lehet. Az adott helyen látható változókat és a belőlük képzett tetszőleges kifejezéseket adhatjuk ily módon a nézethez. A létrehozott kifejezések értéke automatikusan változik, ahogyan a kódban lépegetünk.
3.11. ábra - Expressions nézet
44 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
1.1.4. Töréspontok tulajdonságai Miután létrehoztunk egy töréspontot, a jobbklikk → Breakpoint Properties menüpont segítségével bármikor beállíthatjuk a tulajdonságait. Megadhatunk a töréspont aktiválására vonatkozó megszorító feltételeket, például hogy a töréspont hatására a végrehajtás csak akkor kerüljön felfüggesztésre, ha legalább bizonyos számú alkalommal áthaladt rajta a vezérlés. Ezt a minimumértéket a Hit Count érték beállításával (amely a Breakpoints nézet ábrán látható helyen is megadható) határozhatjuk meg. Amikor n-edjére halad át a vezérlés a törésponton, a végrehajtás felfüggesztésre kerül, és a töréspont mindaddig letiltott állapotba kerül, amíg újra nem engedélyezzük, illetve meg nem változtatjuk a minimumértéket. Ugyanitt lehetőség van feltételes kifejezés létrehozására is, ekkor a végrehajtás csak akkor kerül felfüggesztésre az adott töréspontnál, ha a feltétel igaz. Ezt a módszert kiegészítő naplózásra is használhatjuk, hiszen a feltételt megadó kódrészlet minden olyan alkalommal végrehajtásra kerül, amikor a vezérlés eléri ezt a pontot. A Törésponthoz tartozó feltétel megadása ábrán látható kódrészlet tehát minden alkalommal lefut, amikor elérjük a töréspontot, vagyis az első 50 iterációban a töréspont nem aktív, azonban ezt követően minden alkalommal megjelenítésre kerül az iteráció sorszáma, és a végrehajtás is felfüggesztésre kerül.
3.12. ábra - Törésponthoz tartozó feltétel megadása
1.1.5. A végrehajtás felfüggesztésének további lehetőségei • Figyelőpontok: Figyelőpontok beállításával a végrehajtást akkor függeszthetjük fel, ha egy adattag értéke kiolvasásra illetve megváltoztatásra kerül. Figyelőpont létrehozására az adattag deklarációja melletti margóra történő dupla kattintással van lehetőség. A töréspontokhoz hasonlóan a figyelőpontok is rendelkeznek 45 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
szerkeszthető tulajdonságokkal: lehetnek engedélyezettek illetve letiltottak, beállítható rájuk a Hit Count minimumérték, és szabályozható, hogy az adattag elérésekor, módosításakor, vagy akár (az alapértelmezés szerinti) mindkét esetben kerüljön-e felfüggesztésre a végrehajtás. • Kivételtöréspontok: Segítségükkel olyan töréspontok definiálhatóak, amelyek akkor aktivizálódnak, ha a forráskódban bekövetkezik valamilyen kivétel. Megadásához a Breakpoints nézet Add Java Exception Breakpoint ( ) menüpontjára kattintva van mód, és beállítható, hogy elkapott vagy el nem kapott kivételek esetén kerüljön felfüggesztésre a végrehajtás. • Metódustöréspontok: Metódustöréspont létrehozására a metódus fejléce melletti margóra történő dupla kattintással van lehetőség. A töréspontokhoz és figyelőpontokhoz hasonlóan a metódustöréspontok is rendelkeznek szerkeszthető tulajdonságokkal: lehetnek engedélyezettek illetve letiltottak, beállítható rájuk a Hit Count minimumérték, és szabályozható, hogy a metódusba történő belépéskor, annak elhagyásakor, avagy mindkét esetben kerüljön-e felfüggesztésre a végrehajtás. • Töréspontok osztálybetöltéshez: Egy osztálybetöltő-töréspont a végrehajtást akkor függeszti fel, amikor egy adott osztály betöltése megtörténik. Ennek beállítására a jobb alsó sarokban található Outline nézetben a kiválasztott osztályon nyomott jobb egérgomb, majd a Toggle Class Load Breakpoint menüpont kiválasztásával van mód. • Lépésszűrő: Bizonyos csomagok kihagyhatóak a belövés során. Ez olyan esetben lehet hasznos, ha például egy tesztelési keretrendszert használunk, de nem szeretnénk a keretrendszer osztályaiba belelépni a végrehajtás során. Ezt a beállítást a Window → Preferences → Java → Debug → Step filtering menüútvonal használatával végezhetjük el. • Drop to frame: Az Eclipse lehetővé teszi, hogy a hívási lánc bármely elemét kiválasztva rábírjuk a virtuális gépet arra, hogy ettől a ponttól újrakezdje a végrehajtást. Ennek segítségével a programunk egy részét tudjuk újrafuttatni, de azon változók, amelyek értékét a végrehajtás alatti kód már módosította, módosítottak maradnak. A funkció eléréséhez ki kell választanunk a hívási lánc egy szintjét, és meg kell nyomni a Drop to Frame gombot (
).
2. Egységtesztelés JUnit segítségével Egy egységtesztelés tipikusan fejlesztői tesztelés: a tesztelendő programot fejlesztő programozó gyakorlatilag a programozási munkája szerves részeként készíti és futtatja az egységteszteket. Erre azért van szükség, hogy ő maga is meggyőződhessen arról, hogy amit leprogramozott, az valóban az elvárások szerint működik. Az egység tesztelésére létrehozott tesztesetek darabszáma önmagában nem minőségi kritérium: nem állíthatjuk bizonyossággal, hogy attól, mert több tesztesetünk van, nagyobb eséllyel találjuk meg az esetleges hibákat. Ennek oka, hogy a teszteseteinket gondosan meg kell tervezni. Pusztán véletlenszerű tesztadatok alapján nem biztos, hogy jobb eséllyel fedezzük fel a rejtett hibákat, azonban egy olyan tesztesettervezési módszerrel, amely például a bemenetek jellegzetességeit figyelembe véve alakítja ki a teszteseteket, nagyobb eséllyel vezet jobb teszteredményekhez. Egy fontos mérőszám a tesztlefedettség, amely azon kód százalékos aránya, amelyet az egységteszt tesztel. Ez minél magasabb, annál jobb, bár nem éri meg meg minden határon túl növelni. A JUnit egységtesztelő keretrendszert Kent Beck és Erich Gamma fejlesztette ki, e jegyzet írásakor a legfrissebb verziója a 4.11-es. A 3.x változatról a 4.0-ra váltás egy igen jelentős lépés volt a JUnit életében, mert ekkor jelentős változások történtek, köszönhetően elsősorban a Java 5 által bevezetett olyan újdonságoknak, mint az annotációk megjelenése. Még ma is sokan vannak, akik a 3.x verziójú JUnit programkönyvtár használatával készítik tesztjeiket, de jelen jegyzetben csak a 4.x változatok használatát ismertetjük. A JUnit 4.x egy automatizált tesztelési keretrendszer, ami annyit tesz, hogy a tesztjeinket is programokként megfogalmazva írjuk meg. Ha már egyszer elkészítettük őket, utána viszonylag kis költséggel tudjuk őket újra és újra, automatizált módon végrehajtani. A JUnit keretrendszer a tesztként lefuttatandó metódusokat annotációk segítségével ismeri fel, tehát tulajdonképpen egy beépített annotációfeldolgozót is tartalmaz. Jellemző helyzet, hogy ezek a metódusok egy olyan osztályban helyezkednek el, amelyet csak a tesztelés céljaira hoztunk létre. Ezt az osztályt tesztosztálynak nevezzük. Az alábbi kódrészlet egy JUnit tesztmetódust tartalmaz. Az Eclipse-ben egy tesztosztály létrehozását a File → New → JUnit → JUnit Test Case menüpontban végezhetjük el.
46 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
@Test public void testMultiply() { // MyClass a tesztelendő osztály MyClass tester = new MyClass(); // leellenőrizzük, hogy a multiply(10,5) 50-nel tér-e vissza assertEquals("10 x 5 must be 50", 50, tester.multiply( 10, 5)); }
A JUnit tesztfuttatója az összes, @Test annotációval ellátott metódust lefuttatja, azonban, ha töb ilyen is van, közöttük a sorrendet nem definiálja. Épp ezért tesztjeinket úgy célszerű kialakítani, hogy függetlenek legyenek egymástól, vagyis egyetlen tesztesetmetódusunkban se támaszkodjunk például olyan állapotra, amelyet egy másik teszteset állít be. Egy teszteset általában úgy épül fel, hogy a tesztmetódust az @org.junit.Test annotációval ellátjuk, a törzsében pedig meghívjuk a tesztelendő metódust, és a végrehajtás eredményeként kapott tényleges eredményt az elvárt eredménnyel össze kell vetni. A JUnit keretrendszer alapvetően csak egy parancssoros tesztfuttatót biztosít, de ezen felül nyújt egy API-t az integrált fejlesztőeszközök számára, amelynek segítségével azok grafikus tesztfuttatókat is implementálhatnak. Az Eclipse grafikus tesztfuttatóját a Run → Run as → JUnit test menüpontból érhetjük el. Egy tesztmetódust kijelölve lehetőség nyílik csupán ennek a tesztesetnek a lefuttatására is. A JUnit a @Test annotáció mellett további annotációtípusokat is definiál, amelyekkel a tesztjeink futtatását tudjuk szabályozni. Az alábbi táblázat röviden összefoglalja ezen annotációkat.
3.2. táblázat - JUnit annotációk Annotáció
Leírás
@Test public void method()
A @Test annotáció egy metódust tesztmetódusként jelöl meg.
@Test(expected void method()
=
Exception.class)
public A teszteset elbukik, ha a metódus nem dobja el az
adott kivételt
@Test(timeout=100) public void method()
A teszt elbukik, ha a végrehajtási idő 100 ms-nál hosszabb
@Before public void method()
A teszteseteket inicializáló metódus, amely minden teszteset előtt le fog futni. Feladata a tesztkörnyezet előkészítése (bemenetei adatok beolvasása, tesztelendő osztály objektumának inicializálása, stb.)
@After public void method()
Ez a metódus minden egyes teszteset végrehajtása után lefut, fő feladata az ideiglenes adatok törlése, alapértelmezések visszaállítása.
@BeforeClass public static void method()
Ez a metódus pontosan egyszer fut le, még az összes teszteset és a hozzájuk kapcsolódó @Before-ok végrehajtása előtt. Itt tudunk olyan egyszeri inicializálós lépéseket elvégezni, mint amilyen akár egy adatbázis-kapcsolat kiépítése. Az ezen annotációval ellátott metódusnak mindenképpen statikusnak kell lennie!
@AfterClass public static void method()
Pontosan egyszer fut le, miután az összes tesztmetódus, és a hozzájuk tartozó @After metódusok végrehajtása befejeződött. Általában olyan egyszeri tevékenységet helyezünk ide, amely a @BeforeClass metódusban lefoglalt erőforrások felszabadítását végzi el. Az ezzel az annotációval ellátott metódusnak statikusnak kell lennie!
@Ignore
Figyelmen kívül hagyja a tesztmetódust, illetve tesztosztályt. Ezt egyrészt olyankor használjuk, ha megváltozott a tesztelendő kód, de a tesztesetet még nem frissítettük, másrészt akkor, ha a teszt végrehajtása túl hosszú ideig tartana ahhoz, hogy lefuttassuk. Ha nem metódus szinten, hanem osztály
47 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
Annotáció
Leírás szinten adjuk meg, akkor az osztály tesztmetódusát figyelmen kívül hagyja.
összes
A következő példa egy mesterséges példa, jelen esetben a Java Collections Framework egy kollekciójának tesztelése történik meg, azonban a JUnit alapvető eszközrendszere könnyen bemutatható általa. Itt tehát a @Test annotációval eljelölt metódusok azok, amelyek tesztesetként lefuttatatandók import org.junit.*; import static org.junit.Assert.*; import java.util.* ; public class JunitTestFirstExample { private Collection<String> collection; @BeforeClass public static void oneTimeSetUp() { System.out.println("@BeforeClass - oneTimeSetUp"); } @AfterClass public static void oneTimeTearDown() { System.out.println("@AfterClass - oneTimeTearDown"); } @Before public void setUp() { collection = new ArrayList(); System.out.println("@Before - setUp"); } @After public void tearDown() { collection.clear(); System.out.println("@After - tearDown"); } @Test public void testEmptyCollection() { assertTrue(collection.isEmpty()); System.out.println("@Test - testEmptyCollection"); } @Test public void testOneItemCollection() { collection.add("itemA"); assertEquals(1, collection.size()); System.out.println("@Test - testOneItemCollection"); } }
A végehajtás az alábbi eredménnyel zárul: @BeforeClass - oneTimeSetUp @Before - setUp @Test - testEmptyCollection @After - tearDown @Before - setUp @Test - testOneItemCollection @After - tearDown @AfterClass - oneTimeTearDown
Megjegyzés Ez csak egy lehetséges végrehajtási sorrend, ugyanis a JUnit tesztfuttatója nem definiál sorrendet az egyes tesztmetódusok végrehajtása között, ezért a testEmptyCollection és testOneItemCollection végrehajtása a fordított sorrendben is végbemehetett volna. Ami viszont biztos: a @Before mindig a teszteset futtatása előtt, az @After utána fut le, minden egyes tesztesetre. A 48 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
@BeforeClass egyszer, az első teszt, illetve hozzá tartozó @Before előtt, az @AfterClass ennek
tükörképeként, a legvégén, pontosan egyszer. A végrehajtás tényleges eredménye és az elvárt eredmény közötti összehasonlítás során állításokat fogalmazunk meg. Az állítások nagyon hasonlóak az 2. szakasz - Állítások (assertions) alfejezetben már megismertekhez, azonban itt nem az assert utasítást, hanem az org.junit.Assert osztály statikus metódusait használjuk ennek megfogalmazására. Ezen metódusok nevei az assert részsztringgel kezdődnek, és lehetővé teszik, hogy megadjunk egy hibaüzenetet, valamint az elvárt és téényleges eredményt. Egy ilyen metódus elvégzi az értékek összevetését, és egy AssertionError kivételt dob, ha az összehasonlítás elbukik. (Ez a hiba ugyanaz, amelyet az assert utasítás is kivált, ha a feltétele hamis.) A következő táblázat összefoglalja a legfontosabb ilyen metódusokat. a szögletes zárójelek ([]) közötti paraméterek opcionálisak.
3.3. táblázat - Az Assert osztály metódusai Állítás
Leírás
fail([String])
Feltétel nélkül elbuktatja a metódust. Annak ellenőrzésére használhatjuk, hogy a kód egy adott pontjára nem jut el a vezérlés, de arra is jó, hogy legyen egy elbukott tesztünk, mielőtt a tesztkódot megírnánk.
assertTrue([String], boolean)
Ellenőrzi, hogy a logikai feltétel igaz-e.
assertFalse([String], boolean)
Ellenőrzi, hogy a logikai feltétel hamis-e.
assertEquals([String], expected, actual)
Az equals metódus alapján megvizsgálja, hogy az elvárt és a tényleges eredmény megegyezik-e.
assertEquals([String], tolerance)
expected,
actual, Valós típusú elvárt és aktuális értékek egyezőségét
vizsgálja, hogy belül van-e tűréshatáron. expected[], Ellenőrzi, hogy a két tömb megegyezik-e
assertArrayEquals([String], actual[]) assertNull([message], object)
Ellenőrzi, hogy az ojektum null-e
assertNotNull([message], object)
Ellenőrzi, hogy az objektum nem null-e
assertSame([String], expected, actual)
Ellenőrzi, hogy az elvárt és a tényleges objektumok referencia szerint megegyeznek-e
assertNotSame([String], expected, actual)
Ellenőrzi, hogy az elvárt és a tényleges objektumok referencia szerint nem egyeznek-e meg
2.1. Parametrizált tesztek A JUnit 4-es verziójában jelent meg a parametrizált tesztek készítésének lehetősége. Ezek célja, hogy lehetővé tegyék ugyanazon tesztesetek többszöri lefuttatását, persze rendre különböző értékekkel. Leegyszerűsített példaként tekintsünk egy olyan osztályt, amelynek az add metódusa összeadja a paramétereként kapott két számot: public class Addition { public int add(int x, int y) { return x + y; } }
Parametrizált tesztek készítéséhez az alábbi öt tevékenységet kell elvégeznünk: • El kell látni a tesztosztályt a @RunWith(Parameterized.class) annotációval. @RunWith(Parameterized.class) public class AdditionTest { private int expected; private int first; private int second;
49 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
... }
• Készíteni kell egy olyan konstruktort, amely egy sornyi tesztadat információit képes befogadni. public AdditionTest(int expected, int first, int second) { this.expected = expected; this.first = first; this.second = second; }
• Létre kell hozni egy, a @Parameters annotációval ellátott olyan publikus statikus metódust, amely egydimenziós tömbök kollekcióját adja vissza. Egy-egy tömb ebben a kollekcióban az egyes tesztvégrehajtások során használt adatokat tartalmazza. A kollekció mérete azt mondja meg a tesztfuttatónak, hogy hányszor kell majd az egyes teszteseteket lefuttatni. Az egyes tömböknek azonos elemszámúaknak kell lennie, ráadásul ez pont annyi, mint a konstruktor paramétereinek a száma, hiszen a tesztfuttató majd a tömb elemei alapján fogja létrehozni a paraméterezett teszt végrehajtásához szükséges objektumot. @Parameters public static Collection addedNumbers() { return Arrays.asList(new Integer[][] {{3, 1, 2}, {5, 2, 3}, {7, 3, 4}, {9, 4, 5}}); }
A példában látható tömbök (például a 3, 1, 2 elemekt tartalmazó) felhasználásával hozza majd a futtató rendszer létre a tesztosztály egy-egy objektumát (az AdditionTest konstruktornak átadva a tömb elemeit). • Létre kell hozni a tesztmetódus(oka)t. Ezeket szokás szerint a @Test annotáció jelöli, és a tesztosztály példányváltozóin operálnak. @Test public void sum() { Addition add = new Addition(); assertEquals(expected, add.addNumbers(first, second)); }
•
Ha parancssorból szeretnénk a tesztfuttatót végrehajtani, akkor szükségünk lesz még az org.junit.runner.JUnitCore osztály runClasses nevű statikus metódusára is (grafikus tesztfuttató esetén erre nincs szükség). import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.notification.Failure; public class AdditionTestRunner { public static void main(String[] args) { Result result = JUnitCore.runClasses(AdditionTest.class); for (Failure fail : result.getFailures()) System.err.println(fail.toString()); if (result.wasSuccessful()) System.out.println("Az összes teszt sikerese lefuttatva..."); } }
2.2. Kivételek tesztelése Néha előfordul, hogy az elvárt működéshez az tartozik, hogy a tesztelt program egy adott ponton kivételt dobjon. Például a kivételkezeléssel foglalkozó alfejezetben így működött a push művelet,ha már tele volt a fix méretű verem: FullStackException kivételt vált ki, ha már elfogytak a helyek a veremben. Elvárásunkat, mely szerint kivételnek kellene bekövetkeznie, a teszteset @Test annotációjának expected paraméterének megadásával fogalmazhatjuk meg. Ilyenkor a tesztfuttató majd akkor tartja sikeresnek a tesztet, ha valóban a megjelölt kivétel hajtódik végre, és sikertelennek számít minden más esetben. Példa: @Test(expected=FullStackException.class) public void testPush() { FixedLengthStack s = new FixedLengthStack(3); for (int i = 0; i < 4; i++)
50 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
s.push(i); }
A példában egy háromelemű verembe 4 beszúrást kísérlünk meg. Arra számítunk, hogy ekkor a megjelölt kivétel kerül kiváltásra. Ha nem váltódik ki kivétel, vagy nem a várt kivétel váltódik ki, a teszt elbukik. Azaz ha kivétel nélkül jutunk el a metódus végére, a teszteset megbukik. Ha a kivétel üzenetének tartalmát akarjuk tesztelni, vagy a kivétel várt kiváltódásának helyét akarjuk szűkíteni (egy hosszabb tesztmetóduson belül), arra ez a módszer nem jó. Ilyenkor tegyük a következőt: • kapjuk el a kivételt mi magunk, • használjuk a fail-t, ha egy adott pontra nem volna szabad eljutni, • a kivételkezelőben pedig nyerjük ki a kivétel szövegét, és hasonlítsuk az elvárt szöveghez. public void testException() { try { exceptionCausingMethod(); // Ha eljutunk erre a pontra, a várt kivétel nem váltódott ki, ezért megbuktatjuk a tesztesetet. fail("Kivételnek kellett volna kiváltódnia"); } catch(ExceptedTypeOfException exc) { String expected = "Megfelelő hibaüzenet"; String actual = exc.getMessage(); Assert.assertEquals(expected, actual); } }
2.3. Tesztkészletek létrehozása Egy tesztkészlet (test suite) alatt összetartozó és együttesen végrehajtandó teszteseteket értünk. Ez akkor igazán hasznos, ha egy összetettebb funkció teszteléséhez számos, a részfunkciókat tesztelő teszteset tartozik, amelyeket ilyenkor sokszor különálló osztályokba szervezünk a könnyebb áttekinthetőség érdekében. A különböző tesztosztályokban elhelyezett teszteket azonban mégis szeretnénk együttesen (is) lefuttatni, amelyhez egy olyan tesztosztályra van szükségünk, amelyet a @RunWith(Suite.class) és a @Suite.SuiteClasses annotációkkal is el kell látnunk. Az előbbi a JUnit tesztfuttatónak mondja meg, hogy tesztkészlet végrehajtásáról lesz szó, míg a második paraméteréül a tesztkészletet alkotó tesztosztályok osztályliteráljait adjuk. import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ Test1.class, Test2.class, JunitTestFirstExample.class }) public class TestSuite { }
A példában a Test1, a Test2 és a JunitTestFirstExample tesztosztályok által tartalmazott tesztesetek végrehajtását végző tesztkészlet létrehozását láthatjuk.
2.4. JUnit antiminták A JUnit bemutatott eszközeinek segítségével viszonylag alacsony költséggel tudunk inkrementális módon olyan tesztkészletet fejleszteni, amellyel mérhetjük az előrehaladást, kiszúrhatjuk a nem várt mellékhatásokat, és jobban koncentrálhatjuk a fejlesztési erőfeszítéseinket. Az a többletkódolás, amit a tesztesetek kialakítása érdekében kell megtennünk, valójában általában gyorsan behozza az árát és hatalmas előnyöket biztosít fejlesztési projektjeink számára. Mindez persze csak akkor lesz, lehet így, amennyiben az egységteszteink jól vannak megírva – éppen ezért érdemes megvizsgálni, hogy melyek azok a tevékenységek, amelyek a 51 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
leggyakoribb hibákat jelentik az egységtesztelés során. Ha ezekkel tisztában vagyunk, remélhetőleg már nem követjük el őket mi magunk is.
2.4.1. Rosszul kezelt állítások A JUnit tesztek alapvető építőelemei az állítások (assertion-ök), amelyek olyan logikai kifejezések, amik ha hamisak, az valamilyen hibát jelez. Az egyik legnagyobb hiba velük kapcsolatban az, ha kézi ellenőrzést végzünk. Ez általában úgy jelenik meg (innen ismerhetünk rá), hogy a tesztmetódus viszonylag sok utasítást tartalmaz, azonban állítást egyet sem. Ilyenkor a fejlesztő a tesztet elsősorban arra használja, hogy ha valamilyen hiba (például egy kivétel) a teszt végrehajtása során bekövetkezik, akkor kézzel elkezdhesse debugolni. Ez a megközelítés azonban pontosan a tesztautomatizálás lényegét és egyben legnagyobb előnyét veszi el, nevezetesen, hogy tesztjeinket a háttérben, minde külső beavatkozás nélkül kvázi folyamatosan futtassuk. A kézi ellenőrzések másik tünete, ha a tesztek viszonylag nagy mennyiségű adatot írnak a szabványos kimenetre vagy egy naplóba, majd ezeket kézzel ellenőrzik, hogy minden rendben zajlott-e. Ehhez nagyon hasonló ellenminta a hiányzó állítások esete, amikor egy tesztmetódus nem tartalmaz egyetlen utasítást sem. Ezek a helyzetek kerülendőek. Nem csak a túl kevés, de a túl sok állítás is problémás lehet: ha egy tesztmetódusban több állítás is van, az általában azt jelenti, hogy a tesztmetódus túl sokat próbál tesztelni. Ezt orvosolhatjuk, ha szétvágjuk a tesztmetódust több tesztmetódusra. // KERÜLENDŐ! public class MyTestCase { @Test public void testSomething () { // Teszteset inicializálása, lokális változók manipulálása assertTrue (condition1); assertTrue (condition2); assertTrue (condition3); } } // JAVASOLT! public class MyTestCase { // A lokális változókból példányváltozók lesznek @Before protected void setUp() { // Teszteset inicializálása, példányváltozók manipulálása } @Test public void testCondition1() { assertTrue(condition1); } @Test public void testCondition2() { assertTrue(condition2); } @Test public void testCondition3() { assertTrue(condition3); } }
Megjegyzés Ez nem feltétlenül jelenti azt, hogy tesztenként pontosan egy állítás kerüljön megfogalmazásra! Tapasztalt tesztelők is készítenek néha olyat, hogy egy tesztmetódus több (de csak néhány) állítást tartalmaz. Általában azzal van a probléma, hogy összekeveredik a funkcionalitást tesztelő kód és az elvárt eredmények ellenőrzését végző kód, mert ilyenkor a hiba okát elég nehéz meglelni. A redundáns feltételek szintén kerülendőek. Egy redundáns állítás egy olyan assert metódus, amelyben a feltétel beleégetett módon true. Általában ezzel a helyes működési mód demonstrálását szeretnék elvégezni, 52 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
azonban a szükségtelen bőbeszédűség csak zsúfolttá teszi a metódust. Amennyiben egyéb állítások nincsenek is, akkor ez tulajdonképpen a kézzel ellenőrzés antimintával egyenértékű. Ha ilyennel találkozunk, egyszerűen csak szüntessük meg azon állításokat, amelyek a beégetett feltételt tartalmazzák. //KERÜLENDŐ! @Test public void testSomething() { ... assertTrue("...", true); }
A rossz állítás alkalmazása szintén problémát okozhat. Az Assert osztálynak elég sok metódusa kezdődik assert-tel, ráadásul sokszor csak kicsit eltérő közöttük a paraméterek száma és a szementikájuk. Sokan talán épp emiatt csupán egyetlen assert metódust használnak, mégpedig az assertTrue-t, és annak a logikai kifejezés részébe szuszakolják bele, hogy mit is szeretnének levizsgálni. Példák a rossz használatra: assertTrue("Objects must be the same", expected == actual); assertTrue("Objects must be equal", expected.equals(actual)); assertTrue("Object must be null", actual == null); assertTrue("Object must not be null", actual != null);
Ezek helyett használjuk rendre az alábbiakat: assertSame("Objects must be the same", expected, actual); assertEquals("Objects must be equal", expected, actual); assertNull("Object must be null", actual); assertNotNull("Object must not be null", actual);
2.4.2. Felszínes tesztlefedettség A kezdő egységtesztelők gyakorta csak valamilyen alapvető tesztkódot írnak, és nem vizsgálják meg teljesen a tesztelendő kódot. Ennek többféle megjelenési formája is van: • Csak az alapvető lefutás tesztelése: csak a rendszer elvárt viselkedése kerül tesztelésre. Érvényes adatokat megadva az elképzelt helyes eredmény ellenében történik az ellenőrzés, hiányoznak azonban a kivételes esetek vizsgálatai. Ilyen például, hogy mi történik hibás bemeneti adatok esetén, az elvárt kivételek eldobásra kerültek-e, melyek az érvényes és érvénytelen adatok ekvivalenciaosztályainak határai, stb. • Csak a könnyű tesztek: az előzőhez némiképpen hasonlóan, csak arra koncentrálunk, amit egyszerű ellenőrizni, és így a tesztelendő rendszer igazi logikája figyelmen kívül marad. Ez tipikusan a tapasztalatlan fejlesztő komplex kódot tesztelni célzó próbálkozásainak a tünete. Ezek ellen valamilyen tesztlefedettség-mérő eszköz alkalmazásával védekezhetünk, amely segít meghatározni, hogy a kód melyik része nincs kielégítő módon tesztelve.
2.4.3. Túlbonyolított tesztek Az egységtesztek kódjának az éles rendszer kódjához hasonlóan könnyen érthetőnek kell lennie. Általánosságban azt mondhatjuk, hogy egy programozónak a lehető leggyorsabban meg kell értenie egy teszt célját. Ha egy teszt olyan bonyolult, hogy nem tudjuk azonnal megmondani róla, jó-e vagy sem, akkor nehéz megállapítani, hogy egy sikertelen tesztvégrehajtás a tesztelendő vagy a tesztelő kód rossz mivolta miatt következett-e be. Vagy ami még ennél is rosszabb, fennáll a lehetősége annak, hogy egy kód úgy megy át egy teszten, hogy nem volna neki szabad. A túlbonyolított tesztek egyszerűsítését ugyanúgy végezzük, mint bármilyen más túlbonyolított kód egyszerűsítését: kódújraszervezést (refaktorálást) hajtunk végre a minél könnyebben érthető kód érdekében. Általában ezt a lépéssorozatot mindaddig végezzük, mígnem könnyen felismerhető módon a következő szerkezettel fog rendelkezni: 1. Inicializálás (set up) 2. Az elvárt eredmények deklarálása 3. A tesztelendő egység meghívása
53 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
4. A tevékenység eredményeinek beszerzése 5. Állítás megfogalmazása az elvárt és a tényleges eredményről.
2.4.4. Külső függőségek Annak érdekében, hogy a kód helyesen működjön, számos külső függőségre kell támaszkodnia, például függhet: • egy bizonyos dátumtól vagy időtől, • egy harmadik fél által készített (úgynevezett third-party) jar formátumú programkönyvtártól, • egy állománytól, • egy adatbázistól, • a hálózati kapcsolattól, • egy webszervertől, • egy alkalmazásszervertől, • a véletlentől, • stb. Az egységtesztek a tesztelési hierarchia legalsó szintjén helyezkednek el, a céljuk az, hogy kis mennyiségű kóddal izoláltan próbára tegyék az éles kód egy kis részét, vagyis az egységet. A magasabb szintű teszteléssel szemben az egységtesztelés célja tehát csakis önálló egységek ellenőrzése. Minél több függőségre van egy egységnek szüksége a futtatásához, annál nehezebb igazolni a megfelelő működést. Ha adatbázis-kapcsolatot kell konfigurálni, el kell indítani egy távoli szervert, stb., akkor az egységteszt futtatásáért nagy erőfeszítéseket kell tenni. Az egységtesztek hatékonyságának jó mérőszáma, hogy egy kezdő fejlesztő mennyi idő alatt jut el a tesztek lefuttatásához onnantól kezdve, hogy a verziókezelő rendszerből beszerezte a kódokat. A legegyszerűbb megoldás a verziókezelőből (például cvs, svn vagy git) történő checkout után a build-elést végző eszköz (például ant vagy maven) futtatása, amely csak akkor megy ilyen egyszerűen, ha nincsenek külső függőségek. Ökölszabály, hogy a külső függőségeket el kell kerülni. Ennek érdekében az alábbiakat tehetjük: • a harmadik fél által készített könyvtáraktól való függés elkerüléséhez használjunk tesztduplázókat (test doubles), például mock objektumokat, • biztosítsuk, hogy a tesztadatok a tesztkóddal együtt kerülnek csomagolásra, • kerüljük el az adatbázishívásokat egységtesztjeinkben, • ha mindenképpen adatbázisra van szükségünk, használjunk memóriában tárol adatbázist (például HSQLDBt).
2.4.5. Nem várt kivételek elkapása Míg az éles kód írásakor a fejlesztők általában tudatában vannak az el nem kapott kivételek problémáinak, ezért elég szorgalmasan elkapogatják és naplózzák a problémákat, azonban egységtesztelés esetén ez a minta teljességgel rossz! Tekintsük az alábbi tesztmetódust:: // KERÜLENDŐ! @Test public void testCalculation () { try { deepThought.calculate();
54 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
assertEquals("Calculation wrong", 42, deepThought.getResult()); } catch (CalculationException ex) { Log.error("Calculation caused exception", ex); } }
Ez teljesen rossz, hiszen a teszt átmegy akkor is, ha kiváltódott egy kivétel! Persze a napló bejegyzéseinek a vizsgálatával a problémára fény derülhet, de egy automatizált tesztelési környezetben gyakran senki nem olvassa a naplókat. Még ennél is agyafúrtabb példát láthatunk itt: // KERÜLENDŐ! @Test public void testCalculation() { try { deepThought.calculate(); assertEquals("Calculation wrong", 42, deepThought.getResult()); } catch(CalculationException ex) { fail("Calculation caused exception"); } }
Habár ez a példa annak rendje és módja szerint elbukik, és így jelzi a JUnit futtatónak, hogy valamivel hiba történt, a hiba helyét jelző aktuális veremtartalom elvész. A megoldás az lesz, hogy ne kapjuk el a nem várt kivételeket! Hacsaknem direkt azért írunk kivételkezelőt, hogy ellenőrizzük, hogy egy eldobandó kivétel dobása tényleg megtörténik, nincs okunk elkapni a kivételeket. Sokkal inkább tovább kellene adni a hívási láncot a JUnit-nak, hogy kezelje ő. Az átalakított kód valahogy így nézhet ki: @Test public void testCalculation() throws CalculationException { deepThought.calculate(); assertEquals("Calculation wrong", 42, deepThought.getResult()); }
Mint látható, eltűnt a try-blokk és megjelent egy throws utasításrész, a kód pedig könnybben olvashatóvá vált. Amennyiben azt kellene igazolnunk, hogy egy adott kivétel bekövetkezik, akkor azt megtehetnénk többféleképpen is. Az első példában lévő teszteset csak akkor nem bukik el, ha a kivétel bekövetkezik. @Test public void testIndexOutOfBoundsException() { try { ArrayList emptyList = new ArrayList(); Object o = emptyList.get(0); fail("Exception was not thrown"); } catch(IndexOutOfBoundsException ex) { // Siker! } }
A második példában ugyanerre a Test annotáció expected paraméterét használjuk: @Test(expected = IndexOutOfBoundsException.class) public void testIndexOutOfBoundsException() { ArrayList emptyList = new ArrayList(); Object o = emptyList.get(0); fail("Exception was not thrown"); }
2.4.6. Az éles és a tesztkód keveredése A rossz helyre szervezett tesztkódok zavart okozhatnak. Tekintsük az alábbi elhelyezést, ahol a tesztosztály ugyanabban a könyvtárban helyezkedik el, mint a tesztelendő osztály:
55 Created by XMLmind XSL-FO Converter.
Szoftvertesztelés
src/ com/ xyz/ SomeClass.java SomeClassTest.java
Ekkor nehéz megkülönböztetni a tesztkódot az alkalmazás kódjától. Ezen persze egy elnevezési konvencióval valamennyire lehet segíteni, azonban a tesztkódnak sokszor olyan segédosztályok is részét képezik, amelyek nem közvetlenül kerülnek tesztként futtatásra, csak felhasználják őket a tesztek. Egy másik rossz elhelyezés:, ha a tesztjeinket egy alkönyvtárba helyezzük a tesztelendő kód alá. src/ com/ xyz/ SomeClass.java test/ SomeClassTest.java
Ezzel egyszerűbb kitalálni, hogy melyik osztályra van a szükség a teszteléshez, és melyik az alkalmazás része, azonban a protected és csomag szintű láthatósággal rendelkező tagok tesztelésének búcsút inthetünk. Hacsaknem a teszt kedvéért kinyitjuk a hozzáférést, ami viszont megöli a bezárást. Ráadásul mindkét megoldás további pluszmunkát ró ránk a szoftver kiadásának elkészítésekor, hiszen az egységteszt kódját nem telepítjük az éles rendszerre, épp ezért a csomagolás során valahogyan ki kell zárnunk a tesztelésre használatos kódunkat, ami az alkalmazott elnevezési konvenciótól függően akár elég byonyolult dolog is lehet. A megoldás az, hogy a tesztkódokat ugyanabba a csomagba, de mégis eltérő (párhuzamos) hierarchiába tegyük. Ekkor könnyen szétválaszthatóak az éles kódok a tesztkódoktól és a bezárás megsértésének problémája sem lép fel, hiszen a tesztkód ugyanabban a csomagban helyezkedik el, mint a tesztelendő, ezért annak osztály szintű és protected tagjait is tesztelni tudja. src/ com/ xyz/ SomeClass.java test/ com/ xyz/ SomeClassTest.java
2.4.7. Nem létező egységtesztek Ez esetben nem magukkal a tesztekkel, hanem azok hiányával van a baj. Minden programozó tudja, hogy teszteket kellene írnia a kódjához, mégis kevesen teszik. Ha megkérdezik tőlük, miért nem írnak teszteket, ráfogják a sietségre. Ez azonban ördögi körhöz vezet: minél nagyobb nyomást érzünk, annál kevesebb tesztet írunk. Minél kevesebb tesztet írunk, annál kevésbé leszünk produktívak és a kódunk is annál kevésbé lesz stabil. Minél kevésbé vagyunk produktívak és precízek, annál nagyobb nyomást érzünk magunkon. Ezt a problémát elhárítani csak úgy tudjuk, ha teszteket írunk. Komolyan. Annak ellenőrzése, hogy valami jól működik, nem szabad, hogy a végfelhasználóra maradjon. Az egységtesztelés egy hosszú folyamat első lépéseként tekintendő. Azon túlmenően, hogy az egységtesztek a tesztvezérelt fejlesztés alapkövei, gyakorlatilag minden fejlesztési módszertan profitálhat a tesztek meglétéből, hiszen segítenek megmutatni, hogy a kódújraszervezési lépések nem változtattak a funkcionalitáson, és bizonyíthatják, hogy az API használható. Tanulmányok igazolták, hogy az egységtesztek használata drasztikusan növelni tudja a szoftverminőséget.
56 Created by XMLmind XSL-FO Converter.
4. fejezet - Tervezési minták Ebben a fejezetben néhány tervezési minta bemutatására kerül sor. Ahogyan már a bevezetőben megjegyeztük, a minták követése és alkalmazása segíthet a jobb programozóvá válásban. A szoftverfejlesztési életciklus minden elemét átszövik a minták, beszélhetünk elemzési, architekturális, tervezési, implementációs, de még tesztelési mintákról is. Ezen minták különféle absztrakciós szinten segítik az azt alkalmazó munkáját, azonban közös bennük, hogy valamilyen jól meghatározott cél érdekében jöttek létre, és javasolnak megoldást a felmerülő problémákra. A tervezési minták olyan középszintű minták, amelyek a szoftver részletes tervezése során kerülnek alkalmazásra. Szemben az implementációs mintákkal, nem kötődnek egyetlen programozási nyelvhez sem, annál magasabb absztrakciós szinten tesznek javaslatot az objektumok létrehozása, szerkezetének kialakítása, és az objektumok közötti kommunikáció kialakítása során felmerülő változatos problémákra.
1. A tervezési minták leírása [ GOF1994, GOF2004] a tervezési minták leírására az alábbi szerkezetet javasolja, ezáltal egységessé téve az egyes tervezési minták leírását:
4.1. táblázat - Tervezési minták leírására szolgáló sablon elemei Minta attribútuma Leírás A minta neve és A minta neve a minta lényegét közvetíti rövid formában. A jó név létfontosságú, mivel a besorolása tervezési szókincs részévé válik. A minta besorolását a későbbiekben tárgyalandó három alapvető kategória valamelyikébe végezzük. Cél
Rövid leírás, amely a következő kérdésekkel foglalkozik: Mit csinál az adott tervezési minta? Mi az értelme és a célja? Milyen sajátos tervezési problémára ad választ?
Egyéb nevek
A minta más, jól ismert nevei, ha vannak ilyenek, illetve a minta angol neve.
Feladat
Forgatókönyv, amely bemutatja a tervezési problémát és azt, hogy az osztályok és az objektumok hogyan oldják azt meg. A forgatókönyv segít a minta következő, elvontabb leírásának megértésében.
Alkalmazhatóság
Melyek azok a helyzetek, ahol az adott tervezési minta alkalmazható? Melyek azok a rossz tervek, amelyek leváltását a minta megcélozza? Hogyan ismerhetők fel ezek?
Szerkezet
A minta osztályainak grafikus szemléltetése.
Résztvevők
Az osztályok, illetve objektumok, amelyek részt vesznek a tervezési mintákban, valamint azok feladatai.
Együttműködés
Hogyan működnek együtt az objektumok, hogy végrehajtsák feladataikat?
Következmények
Hogyan támogatja az adott minta a kívánt célokat? Mik a minta használatának előnyei és hátrányai? A rendszer mely szerkezeti elemeit változtathatjuk szabadon az adott mintát használva?
Megvalósítás
Milyen buktatókra kell ügyelni, milyen módszereket érdemes használni a minta megvalósításakor? Vannak nyelvi sajátosságok?
Példakód
Programkód-töredékek, amelyek illusztrálják, hogyan valósítható meg a minta valamely objektumorientált programozási nyelven.
Ismert felhasználások
Valós rendszerekből vett példák a mintára. Legalább két példát szerepeltetünk, különböző területekről.
Kapcsolódó minták
Mely tervezési minták kapcsolódnak szorosan az adott mintához? Mik a legfontosabb különbségek? Milyen más mintákkal együtt célszerű használni az adott mintát?
2. GoF tervezési minták katalógusa Az alábbi táblázat a GoF által összegyűjtött, leírt és rendszerezett 23 tervezési minta magyar és angol nevét, valamint rövid leírását tartalmazza ([GOF1994, GOF2004]): 57 Created by XMLmind XSL-FO Converter.
Tervezési minták
4.2. táblázat - A GoF 23 tervezési mintájának katalógusa Minta Minta angol Leírás magyar neve neve Elvont gyár
Abstract Factory
Kapcsolódó vagy egymástól függő objektumok családjának létrehozására szolgáló felületet biztosít a konkrét osztályok megadása nélkül.
Illesztő
Adapter
Az adott osztály interfészét az ügyfelek által igényelt interfésszé alakítja. E módszerrel az egyébként összeférhetetlen felületű osztályok együttműködését biztosíthatjuk.
Híd
Bridge
Az elvont ábrázolást elválasztja a megvalósítástól, hogy a kettő egymástól függetlenül módosítható legyen.
Építő
Builder
Az összetett objektumok felépítését függetleníti az ábrázolásuktól, így ugyanazzal az építési folyamattal különböző ábrázolásokat hozhatunk létre.
Felelősséglánc Chain of Arra szolgál, hogy függetleníteni tudjuk a klienst hívottól. Ezt úgy érjük el, Responsibility hogy több objektumnak is jogot adunk a kérelem kezelésére. A fogadó objektumokat láncba állítjuk, amelyen a kérelem addig halad, amíg el nem ér egy objektumot, ami képes a kezelésére. Parancs
Command
A kérelmeket objektumba zárja, aminek célja, hogy az ügyfeleknek paraméterként különböző kérelmeket adjunk át, ezeket sorba állítsuk vagy naplózzuk, illetve támogassuk a műveletek visszavonását.
Összetétel
Composite
Az objektumokat faszerkezetbe rendezi, hogy ábrázolhassuk a rész–egész viszonyokat. A módszer révén az önálló objektumokat és az objektumösszetételeket egységesen kezelhetjük.
Díszítő
Decorator
Az objektumokhoz dinamikusan további felelősségi köröket rendel. A kiegészítő szolgáltatások biztosítása terén e módszer rugalmas alternatívája az alosztályok létrehozásának.
Homlokzat
Façade
Egy alrendszerben interfészek egy halmazához egységes interfészt biztosít. A módszerrel magasabb szintű interfészt határozunk meg, amelynek révén az adott alrendszer könnyebben használhatóvá válik.
Gyártófüggvé Factory ny Method
Felületet határoz meg egy objektum létrehozásához, de az alosztályokra bízza, melyik osztályt példányosítják. A gyártófüggvények megengedik az osztályoknak, hogy a példányosítást az alosztályokra ruházzák át.
Pehelysúlyú
Flyweight
Megosztás révén támogatja a finomszemcsézett objektumok tömegeinek hatékony felhasználását.
Értelmező
Interpreter
Egy adott nyelv nyelvtanát ábrázolja, illetve ehhez az ábrázoláshoz értelmezőt biztosít, amely annak alapján képes az adott nyelv mondatait megérteni.
Bejáró
Iterator
Az összetett objektumok elemeinek soros elérését a háttérben megbúvó ábrázolás felfedése nélkül biztosító módszer.
Közvetítő
Mediator
Egy objektumot határoz meg, amely objektumok egy halmazának együttműködését irányítja. (Vagyis ezeket egyetlen objektumba zárjuk be.) Ezzel laza csatolást hozunk létre, amelyben az egyes objektumok közvetlenül nem hivatkozhatnak egymásra, a köztük levő kapcsolatok pedig egymástól függetlenül módosíthatók.
Emlékeztető
Memento
Az egységbe zárás (encapsulation) megsértése nélkül kinyeri és rögzíti egy objektum belső állapotát, hogy az később ebbe az állapotba visszaállítható legyen.
Megfigyelő
Observer
Objektumok között 1:N számosságú függőségi kapcsolatot hoz létre, így amikor az egyik objektum állapota megváltozik, minden tőle függő objektum értesül erről és automatikusan frissül.
Prototípus
Prototype
Prototípus példány használatával meghatározza, milyen típusú objektumokat kell létrehozni, az új objektumok létrehozását pedig ennek a prototípusnak a lemásolásával állítja elő.
Helyettes
Proxy
Egy adott objektumot egy helyettesítő objektummal váltunk fel, amely szabályozza az eredeti objektumhoz történő hozzáférést is. 58 Created by XMLmind XSL-FO Converter.
Tervezési minták
Minta Minta angol Leírás magyar neve neve Egyke
Singleton
Egy osztályból csak egy példányt engedélyez, és ehhez globális hozzáférési pontot ad meg.
Állapot
State
Adott objektum számára engedélyezi, hogy belső állapotának megváltozásával megváltoztathassa viselkedését is. Az objektum ekkor látszólag módosítja az osztályát.
Stratégia
Strategy
Algoritmuscsaládot határoz meg, melyben az algoritmusokat egyenként egységbe zárjuk és egymással felcserélhetővé tesszük. E módszer révén az algoritmus a felhasználó klienstől függetlenül módosítható.
Sablonfüggvé Template ny Method
Adott művelet algoritmusának vázát készíti el, amelynek egyes lépéseit alosztályokra ruházza át. Így az alosztályok az algoritmus egyes lépéseit felülbírálhatják, anélkül, hogy az algoritmus szerkezete módosulna.
Látogató
Objektumszerkezet elemein végrehajtandó műveletet reprezentál: a Látogató minta segítségével anélkül határozhatunk meg egy új műveletet, hogy a benne részt vevő elemek osztályát meg kellene változtatnunk.
Visitor
2.1. Tervezési minták rendszerezése és kapcsolataik A tervezési minták részletezettségükben és absztrakciós szintjüket illetően különbözőek. Mivel sok tervezési minta van, szükség van arra, hogy valamilyen módon rendszerezzük őket. Az osztályba sorolás segíti a katalógusban levő minták gyorsabb megtanulását és új minták felfedezésére is ösztönözhet. A tervezési mintákat két szempont szerint osztályozhatjuk (4.3. táblázat - Tervezési minták osztályozása). Az első a cél, ami azt tükrözi, mit csinál a minta. A mintáknak létrehozási, szerkezeti vagy viselkedési célja lehet. A létrehozási minták (vagy alkotó minták) az objektum-létrehozás folyamatában érdekeltek, gyakorlatilag az objektumpéldányosítás absztrakcióiról van szó. A szerkezeti minták az objektumok és osztályok felépítésével foglalkoznak, míg a viselkedési (vagy más nével kommunikációs) minták azon tulajdonságokat írják le, ahogyan az objektumok kölcsönösen együttműködnek és felosztják a felelősségi köröket. A második szempont a hatókör, ami meghatározza, hogy a minta objektumra vagy osztályra alkalmazható-e. Az osztályminták az osztályok és az alosztályok viszonyával foglakoznak. Ezen viszonyok az öröklődéssel jönnek létre, ezért statikusak, vagyis fordításkor rögzítettek. Az objektumminták objektumkapcsolatokra vonatkoznak, amelyek futásidőben megváltoztathatók, ezért jóval dinamikusabbak. Majdnem minden minta használ valamilyen szintű öröklést, így csak azokat a mintákat hívjuk „osztálymintáknak”, amelyek az osztályok közötti viszonyokra összpontosítanak. A legtöbb minta az „objektum” hatókörbe tartozik.
4.3. táblázat - Tervezési minták osztályozása Cél
Osztály
Hatókör Objektum
Létrehozási
Szerkezeti
Gyártófüggvény
(Osztály)illesztő
Viselkedési Értelmező Sablonfüggvény
(Objektum)illesztő
Felelősséglánc
Elvont gyár
Híd
Parancs
Építő
Összetétel
Bejáró
Prototípus
Díszítő
Közvetítő
Egyke
Homlokzat
Emlékeztető
Pehelysúlyú
Megfigyelő
59 Created by XMLmind XSL-FO Converter.
Tervezési minták
Cél Létrehozási
Szerkezeti
Viselkedési
Helyettes
Állapot Stratégia Látogató
A tervezési minták rendszerezésének másik módja, amikor aszerint csoportosítjuk őket, hogy a „Kapcsolódó minták” leírásban mely más mintákra hivatkoznak. Az alábbi ábra ezeket a viszonyokat ábrázolja.
4.1. ábra - Tervezésiminta-kapcsolatok [GOF2004]
2.2. Hogyan válasszunk tervezési mintát? 60 Created by XMLmind XSL-FO Converter.
Tervezési minták
A különféle mintakatalógusokban található tervezési minták közül nem könnyű megtalálni, melyikre van éppen szükségünk egy adott probléma megoldásához, különösen, ha a gyűjtemény új és ismeretlen számunkra. Éppen ezért tekintsünk át néhány javaslatot ([GOF1994, GOF2004] nyomán) arra vonatkozóan, hogy milyen irányelvek mentén végezzük a megfelelő tervezési minta kiválasztását: • Vegyük figyelembe, hogy a tervezési minták hogyan oldják meg a tervezési problémákat. • Nézzük át a célról szóló részeket. A különféle mintakatalógusok a 2. szakasz - GoF tervezési minták katalógusa részben leírtakhoz hasonlóan rendre felsorolják a tartalmazott (leírt) minták céljait. Olvassuk át ezt a részt, azok után a célok kutatva, amelyek kapcsolódnak az adott problémához. A minták osztályozása szintén jó támpontot adhat a tekintetben, hogy melyek a szóba jöhető minták. • Tanulmányozzuk a minták kapcsolatát. Ezt nagy mértékben meg tudja könnyteni, ha az egyes minták közötti kapcsolatok grafikusan is megjelennek, mint ahogyan az a 4.1. ábra - Tervezésiminta-kapcsolatok GOF2004 ábrán is látható. Ezen kapcsolatok tanulmányozása segíthet a jó minta vagy mintacsoport megtalálásában. • Tanulmányozzuk a hasonló célú mintákat. A katalógus három fejezetből áll: az első a létrehozási mintákról szól, a második a szerkezeti mintákról, a harmadik pedig a viselkedési mintákról. Minden fejezet a mintákat bemutató megjegyzésekkel indít, és egy olyan résszel zárul, ami összehasonlítja a mintákat. Ezek a részek a hasonló célú minták közti hasonlatosságokba és különbségekbe engednek betekintést. • Vizsgáljuk meg az újratervezés okait. Vizsgáljuk meg az újratervezés okait, hogy tisztában legyünk vele, hogy pontosan miért van szükség rá. Aztán nézzük át a mintákat, amelyek segíthetnek elkerülni az újratervezést. Az újratervezés gyakori okai Annak érdekében, hogy olyan rendszert tudjunk tervezni, amelyet egy-egy (például a követelményekben bekövetkező) változás esetén nem feltétlenül kell rögtön újratervezni, tisztában kell lennünk azzal, hogy milyen tipikus okai vannak a változásnak. Az a terv, amelyik ezeket nem veszi figyelembe, a későbbi alapos újratervezés szükségességének kockázatát hordozza magában.
Megjegyzés Persze akármilyen gondosan és előrelátóan is végezzük a tervezést, a későbbiekben felmerülhetnek olyan igények, amelyek akár a legjobb tervet is változatásra kényszerítik. Itt tehát a célunk csak az lehet a változás lehetséges okainak vizsgálatával, hogy megpróbáljuk csökkenteni annak esélyét, hogy ilyenre szükség legyen. A tervezési minták használata abban tud segítséget nyújtani, hogy a rendszer meghatározott módokon módosítható legyen. Mivel minden tervezési mintában vannak a rendszerszerkezetnek olyan részei, amelyek a többitől függetlenül változtathatóak, így a megfelelő minták alkalmazásával a rendszert ellenállóbbá tudjuk tenni bizonyos változásfajtákkal szemben. Az újratervezést szükségessé tévő leggyakoribb okok az alábbiak: • Az objektumlétrehozást úgy végezzük, hogy konkrét osztálytól való függőséget alakítunk ki. Ez mindig megtörténik, ha az objektumaink létrehozására a new operátort használjuk. Ekkor, ha a későbbiekben egy másik osztály példányát kellene létrehoznunk (például valamely később létrehozandó alosztályét vagy az ugyanazon interfészt megvalósító valamely másik osztályét), akkor bizony módosítanunk kell a kódot. Ez tulajdonképpen szembemegy az interfészre programozzunk, nem implementációra alapelvvel, hiszen a new után egy konkrét megvalósítást jelentő osztály neve szerepel. Ehelyett az Elvont gyár (Abstract Factory), a Gyártófüggvény (Factory Method) vagy éppen a Prototípus (Prototype) minta alkalmazásával elérhetjük, hogy az objektumok létrehozása közvetett módon történjen, így megszüntetve a konkrét implementációs osztálytól való függőséget. Minderre persze csak akkor lesz szükségünk, ha várható, hogy a későbbiekben majd új típusú objektumok is megjelennek, azok létrehozására is szükség lesz. • Konkrét műveletektől való függőség. A helyzet az előzőhöz hasonló, csak nem egy konkrét objektum létrehozásától, hanem egy konkrét művelet elvégzésétől függünk, amely a kérésnek egyfajta kielégítési módját jelenti. A Felelősséglánc (Chain of Responsibility) és a Parancs (Command) minták segítségével függetleníthetjük magunkat a megváltoztathatatlanul kódolt kérésektől.
61 Created by XMLmind XSL-FO Converter.
Tervezési minták
• Hardver- és szoftverkörnyezettől való függőség. A különféle hardverelemekkel, operációs rendszerekkel és egyéb szoftverkomponensekkel történő kapcsolattartás során azzal szembesülhetünk, hogy különböző környezetekben más-más lehet a kommunikáció módja. A környezetétől függő programot nehéz más környezetekhez illeszteni (vagyis a hordozhatóság csorbát szenved), ezért például az Elvont gyár (Abstract Factory) vagy a Híd (Bridge) minta alkalmazásával tehetjük programunkat környezetfüggetlenebbé. • Az objektumok reprezentációjától és megvalósításától való függőség. Ha egy kliens tudja, hogy egy objektum reprezentációja, tárolása, megvalósítása hogyan történik, és ráadásul ki is használja ezt a tudást, függővé válik: ha maga az objektum megváltozik, az is változtatásra szorul. Ez persze bezárási problémát jelent, de hát különböző bezárási szintekről beszélhetünk. Az Elvont gyár (Abstract Factory), a Híd (Bridge), az Emlékeztető (Memento) és a Helyettes (Proxy) minták azok, amelyek alkalmazásával az objektumunk részleteit elrejthetjük a kliensek elől, így növelve a függetlenséget, és segítve objektumaink rugalmas változtathatóságát. • Algoritmikus függőségek. Az algoritmusok hatékonyságát sokszor utólag finomhangolják, kibővítik, vagy éppen teljesen lecserélik őket a fejlesztés és újrafelhasználás során. Az ilyen algoritmusoktól függő objektumok kénytelenek lesznek megváltozni az algoritmus megváltozásakor, ezért azon algoritmusokat, amelyek valószínűsíthetően meg fognak változni, érdemes elkülöníteni. Erre az Építő (Builder), a Bejáró (Iterator), a Sablonfüggvény (Template Method), a Látogató (Visitor) és talán leginkább a Stratégia (Strategy) minták szolgálnak. • Szoros csatolás (tight coupling). A lazán csatoltság (loose/low coupling) ellentéte. Azokat az osztályokat, amelyek egymással szoros viszonyban vannak (vagyis egymástól függenek), nehéz – vagy akár lehetetlen – egymástól függetlenül újrafelhasználni. Ez azt eredményezi, hogy nem változtathatunk meg vagy cserélhetünk le egy osztályt a rendszerben anélkül, hogy sok másik osztály működését is meg ne értenénk, vagy akár át ne írnánk. Rendszerünk így nehezen áttekinthetővé és módosíthatóvá válik. Az Elvont gyár (Abstract Factory), a Híd (Bridge), a Felelősséglánc (Chain of Responsibility), a Parancs (Command), a Homlokzat (Façade), a Közvetítő (Mediator) és a Megfigyelő (Observer) minták használata abban segít minket, hogy csökkentsük a csatolás szorosságát. • A működés alosztályokkal történő kibővítése. Egy létező osztály funkcionalitásának alosztályokkal történő kibővítése nem egyszerű feladat, mert egy alosztály létrehozásához a szuperosztály mély megértésére van szükség (hiszen nemcsak a szerkezet, de a viselkedés is öröklődik). Például egy metódus felüldefiniálása más metódus(ok) felüldefiniálását is maga után vonhatja (mint ahogyan az Object-ből örökölt equals és hashCode esetében is elvárás). A kompozíció és a delegáció az alosztályokkal történő kibővítés rugalmas alternatívái lehetnek. hiszen anélkül tudunk új szolgáltatásokat adni a rendszerhez, hogy új alosztályok sokaságát kellene létrehoznunk. A kompozíció túlzott használata persze nehezíti a megértést, éppen ezért tehet jó szolgálatot a Híd (Bridge), a Felelősséglánc (Chain of Responsibility), az Összetétel (Composite), a Díszítő (Decorator), a Megfigyelő (Observer) vagy éppen a Stratégia (Strategy) minta, amelyek sokszor csak egyetéen alosztály létrehozását javasolják, míg a további funkciókat a meglévő objektumok kompozíciójával állíthatjuk elő. • Osztályok kényelmetlen módosítása. Néha olyan osztály megváltoztatására van szükség, amelyet bonyolult volna (például a változás rengeteg alosztály megváltoztatását is magával vonná), vagy akár nem is lehetséges (mert esetleg ez egy harmadik féltől származó – úgynevezett third-party – osztálykönyvtár, amelynek nem rendelkezünk a forrásával). Az Illesztő (Adapter), a Díszítő (Decorator) és a Látogató (Visitor) minták ezen módosítások elvégzését egyszerűsítik le. • Gondoljuk át, mit tegyünk változtathatóvá rendszerünkben. Ez a megközelítés az újratervezés okaira való összpontosítás ellentéte. Ahelyett, hogy azt néznénk, mi miatt kellhet megváltoztatnunk a tervet, azon gondolkodunk el, hogy mit akarunk majd újratervezés nélkül módosíthatóvá tenni. Itt a változó elemek egységbe zárására összpontosítunk, ami számos tervezési minta témája. Az 4.4. táblázat - A tervezési minták által megengedett változtatható elemek táblázat azokat az elemeket sorolja fel, amelyeket az egyes tervezési minták használatával függetlenül, vagyis újratervezés nélkül megváltoztathatunk.
4.4. táblázat - A tervezési minták által megengedett változtatható elemek Cél
Tervezési minta
Változtatható elemek 62 Created by XMLmind XSL-FO Converter.
Tervezési minták
Cél
Létrehozási
Szerkezeti
Viselkedési
Tervezési minta
Változtatható elemek
Elvont gyár
Leszármazott objektumok családjai
Építő
Hogyan készül az összetett objektum
Gyártófüggvény Egy példányobjektum alosztálya Prototípus
Egy példányobjektum osztálya
Egyke
Egy osztály egyetlen példánya
Illesztő
Egy objektum interfésze
Híd
Egy objektum megvalósítása
Összetétel
Egy objektum szerkezete és összetétele
Díszítő
Egy objektum kötelességei leszármaztatás nélkül
Homlokzat
Felület egy alrendszerhez
Pehelysúlyú
Objektumok tárolásának költsége
Helyettes
Hogyan érünk el egy objektumot; az objektum helyzete
Felelősséglánc
Az objektum, ami a kérelmeket teljesíti
Parancs
Mikor és hogyan teljesül egy kérelem
Értelmező
Egy nyelv szabályai és értelmezése
Bejáró
Hogyan érjük el és járjuk be egy aggregátum elemeit
Közvetítő
Mely objektumok hatnak egymásra, és hogyan
Emlékeztető
Milyen privát információ tárolódik az objektumon kívül, és mikor
Megfigyelő
Számos más objektumtól függő objektum; hogyan maradnak a függő objektumok naprakészek
Állapot
Egy objektum állapotai
Stratégia
Egy algoritmus
Sablonfüggvény Egy algoritmus lépései Látogató
Olyan műveletek, amelyek alkalmazhatók objektum(ok)ra az osztályuk megváltoztatása nélkül
2.3. Hogyan használjuk a tervezési mintákat? Ha már kiválasztottuk a tervezési mintát, hogyan használjuk? Következzen egy rövid útmutató, amely lépésről lépésre megmutatja, hogyan használhatjuk fel hatékonyan a mintákat: 1. Olvassuk át a mintát, hogy legyen róla elképzelésünk. Legyünk különös figyelemmel az Alkalmazhatóság és a Következmények részekre, hogy biztosak lehessünk benne, hogy ez a megfelelő minta a problémánk megoldására. 2. Térjünk vissza a Szerkezet, a Résztvevők és az Együttműködés fejezetekre. Győződjünk meg róla, hogy értjük a mintában szereplő osztályokat és objektumokat, és hogy azok hogyan kapcsolódnak egymáshoz. 3. Nézzük át a Példakód részt, hogy lássunk egy konkrét példát a minta kódba ágyazására. A kód tanulmányozása segít a minta megvalósításának megtanulásában. 4. Válasszunk olyan neveket a minta résztvevőinek, amelyek értelmesek lesznek az alkalmazott környezetben. A tervezési mintákban található nevek általában túl elvontak ahhoz, hogy egy alkalmazásban használhatók legyenek. Mindazonáltal hasznos a résztvevő nevének belefoglalása abba a névbe, ami majd az alkalmazásban megjelenik. Ennek segítségével nyilvánvalóbbá válik a minta alkalmazása a megvalósításban. Például ha a Stratégia (Strategy) mintát használjuk egy szövegszerkesztő algoritmushoz, akkor olyan osztályaink lehetnek, mint például az EgyszerűElrendezésStratégia vagy a TeXElrendezésStratégia. 5. Határozzuk meg az osztályokat. Adjuk meg a felületüket, és hozzuk létre az öröklődési kapcsolataikat, majd határozzunk meg olyan példányváltozókat, amik mutatják az adat- és objektumhivatkozásokat. Azonosítsuk
63 Created by XMLmind XSL-FO Converter.
Tervezési minták
az alkalmazás azon létező osztályait, amelyekre hatással lesz a minta, és ennek megfelelően módosítsuk azokat. 6. Adjunk az alkalmazásra jellemző neveket a mintában szereplő műveleteknek. Itt a nevek újra csak az alkalmazástól függnek. Használjuk a műveletekhez rendelt feladatokat és együttműködési kapcsolatokat útmutatóként. Továbbá legyünk következetesek az elnevezési konvenciókat illetően. Például egy gyártófüggvény nevében használjuk mindig a „Létrehoz-” (vagy Create, esetleg New) előtagot. 7. Valósítsuk meg a mintában található feladatokat és együttműködéseket ellátó műveleteket. A Megvalósítás rész tippeket ad a megvalósításra, de a Példakód részben leírt példák is segíthetnek.
3. Tervezési minták alkalmazása a gyakorlatban 3.1. Létrehozási minták 3.1.1. Egyke (Singleton) Az egyke minta akkor használatos, ha garantálnunk kell, hogy egy objektumpéldányból egy időben egy JVMben csak egy létezzen, amelyet alkalmazásunk objektumai közösen használnak. Ez az adott osztály korlátozott példányosításával biztosítható. Az egyke konstruktora az osztályon kívülről nem látható, az egyetlen példány elérésére nyilvános metódus(ok) szolgál(nak).
4.2. ábra - Az egyke minta megvalósításának osztálydiagramja
A privát láthatóságú instance osztályszintű adattag tárolja a példányt, amelyet a nyilvános getInstance metódussal kérdezhetünk le. Példa: import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionHelper { private static final ConnectionHelper connectionHelper = new ConnectionHelper(); private ConnectionHelper() { } public static ConnectionHelper getInstance() { return connectionHelper; } private private private private private private
Connection conn; String username; String password; String hostname = "servername.inf.unideb.hu"; int port = 1521; String sid = "ORCL";
public void setUsername(String username) { this.username = username; } public void setPassword(String password) {
64 Created by XMLmind XSL-FO Converter.
Tervezési minták
this.password = password; } public void setHostname(String hostname) { this.hostname = hostname; } public void setPort(int port) { this.port = port; } public void setSid(String sid) { this.sid = sid; } public Connection getConnection() throws SQLException, MissingCredentialsException { if (conn == null) { synchronized (connectionHelper) { if (conn == null) { if ( username == null || username.isEmpty() || password == null || password.isEmpty()) throw new MissingCredentialsException("Missing credentials."); conn = DriverManager.getConnection(new StringBuilder("jdbc:oracle:thin:@") .append(hostname).append(':') .append(String.valueOf(port)).append(':') .append(sid).toString(), username, password); } } } return conn; } public void closeConnection() { if (conn != null) { try { conn.close(); } catch (SQLException e) { // Kivétel kezelése } finally { conn = null; } } } }
Itt a getInstance metódusa singleton egy példányát adja vissza, amely bezárja a kapcsolódási adatok megadására szolgáló set... metódusokat, valamint a kapcsolat kiépítését lusta inicializációval (lazy init) elvégző getConnection metódust, amely, ha már létrejött a globálisan elérhető kapcsolat, azt adja vissza, egyébként pedig létrehozza azt, és úgy adja vissza. A closeConnection a globális hozzáférési ponttal rendelkező kapcsolat bezárására szolgál.
Megjegyzés A példában a singleton példányt hagyományos módon, míg az általa bezárt Connection objektumot lusta inicializációval hoztuk létre. A fenti példa a singleton klasszikus implementációs megoldása, azonban [BLOCH2008EN] és [BLOCH2008HU] azt javasolja, hogy az 1.5-ös feletti Java verziókban használjunk egy egyelemű felsorolásos típust (enum-ot) a singleton megvalósítása érdekében. Példa: import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public enum ConnectionHelper { INSTANCE;
65 Created by XMLmind XSL-FO Converter.
Tervezési minták
private Connection conn; private ConnectionHelper() { } private private private private private
String username; String password; String hostname = "servername.inf.unideb.hu"; int port = 1521; String sid = "ORCL";
public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setHostname(String hostname) { this.hostname = hostname; } public void setPort(int port) { this.port = port; } public void setSid(String sid) { this.sid = sid; } public Connection getConnection() throws SQLException, MissingCredentialsException { if (conn == null) { if ( username == null || username.isEmpty() || password == null || password.isEmpty()) throw new MissingCredentialsException(Missing credentials."); conn = DriverManager.getConnection(new StringBuilder("jdbc:oracle:thin:@") .append(hostname).append(':') .append(String.valueOf(port)).append(':') .append(sid).toString(), username, password); } return conn; } public void closeConnection() { if (conn != null) { try { conn.close(); } catch (SQLException e) { // Kivétel kezelése } finally { conn = null; } } } }
A hívás helyén pedig az alábbi módon használhatjuk fel az enum példányt: import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; public class ConnMain { public static void main(String[] args) throws SQLException, MissingCredentialsException { ConnectionHelper.INSTANCE.setUsername("username"); ConnectionHelper.INSTANCE.setPassword("password"); Connection conn = ConnectionHelper.INSTANCE.getConnection(); // Műveletek ConnectionHelper.INSTANCE.closeConnection();
66 Created by XMLmind XSL-FO Converter.
Tervezési minták
} }
3.1.2. Gyártó minták A gyártó minták úgy készítenek új objektumokat, hogy közben a létrehozás logikáját elrejtik a kliens elől, amely az újonnan létrehozott példányt egy interfészen keresztül érheti el.
4.3. ábra - A gyártó minták általános megvalósításának osztálydiagramja [OODesign]
A kliens által igényelt objektum a new operátor alkalmazása helyett a megadott paraméterek alapján a gyárban készül. A kliens nincs tisztában a létrehozás és az objektum konkrét implementációjával. 3.1.2.1. Gyártófüggvény (Factory method) Ezt a tervezési mintát akkor használjuk, rendelkezünk egy szülőosztállyal, amelynek több leszármazottja van, és a bemeneten múlik, hogy mely gyermekosztályra van szükségünk. A minta egy gyártó metódusra teszi az objektumok létrehozásának felelősségét.
4.4. ábra - A gyártófüggvény minta megvalósításának osztálydiagramja [OODesign]
67 Created by XMLmind XSL-FO Converter.
Tervezési minták
A Product interfész a gyártó metódus által előállított objektum típusa, amelyet a ConcreteProduct osztály implementál. A Factory osztály definiálja a gyártó metódust, amely egy Product objektummal tér vissza. A ConcreteFactory osztály implementálja a metódust, amely a konkrét objektumot állítja elő. Példák: A String osztály valueOf metódusa, amely gyártófüggvényként legyártja a paraméterének megfelelő csomagolóosztálybeli objektumot. A getInstance metódusok (sokszor statikusak) legyártanak egy-egy megfelelő objektumpéldányt (erre láttunk példát a singleton esetében is, de a szabványos Java API-ban előforduló statikus gyártófüggvényekre is van példa): Timestamp currentTimestamp = new Timestamp(GregorianCalendar.getInstance().getTimeInMillis()); NumberFormat nf = NumberFormat.getInstance(); colValues.add(nf.format(book.getPrice())); Locale.setDefault(new Locale("hu", "HU")); ResourceBundle rBundle = ResourceBundle.getBundle(bundleName);
3.1.2.2. Elvont gyár (Abstract factory) Az elvont gyár minta hasonló a gyártófüggvényhez, azonban magasabb absztrakciós szintet képvisel. A gyártók gyárának is szokták nevezni. Ebben a mintában nem if–else vagy catch blokkokkal határozzuk meg, hogy mely termékalosztályt példányosítjuk, hanem minden termékalosztályhoz tartozik egy gyár osztály, amely segítségével az absztrakt gyár elkészíti az objektumot. Absztrakt gyár alkalmazásával elrejthetjük a platformfüggő osztályokat a kliens elől.
4.5. ábra - Az elvont gyár minta megvalósításának osztálydiagramja [OODesign]
Az AbstractFactory osztály absztrakt objektumok előállítására tartalmaz metódusokat, amelyeket a ConcreteFactory osztályok definiálnak felül a konkrét objektumok előállítása érdekében. Az AbstractProduct típusok a Product konkrét típusok közös műveleteit és tulajdonságait írják le. A konkrét Product objektumokat a konkrét gyárak állítják elő, a kliens azonban az absztrakt gyárat és az absztrakt terméket használja, így teljesen rejtve maradnak előle a konkrét implementációk. Példák (itt az első sorok rendre statikus gyártófüggvényre példák, legyártanak egy absztrakt gyárat, míg az absztrakt gyár segítségével gyártjuk le a megfelelő implementáció XML-feldolgozást segítő objektumát): DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = factory.newDocumentBuilder();
68 Created by XMLmind XSL-FO Converter.
Tervezési minták
TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser saxParser = factory.newSAXParser(); XMLInputFactory inputFactory = XMLInputFactory.newInstance(); InputStream in = new FileInputStream(xmlFile); XMLEventReader eventReader = inputFactory.createXMLEventReader(in);
3.1.3. Építő (Builder) Ez a minta szintén objektumok előállítására szolgál, viszont egyben megoldást is kínál a gyár mintákkal kapcsolatos problémákra. A gyár minták nehezen boldogulnak a sok attribútummal rendelkező objektumokkal. Az építő azonban lépésről lépésre építi fel az objektumot, állítja be az attribútumok értékét, míg végül visszaadja a teljesen elkészült példányt.
4.6. ábra - Az építő minta megvalósításának osztálydiagramja [OODesign]
A Builder osztály egy absztrakt felületet határoz meg, amely egy objektumot épít fel annak részeiből. A ConcreteBuilder, amely implementálja a Builder absztrakt metódusát, összeállítja az objektumot annak részeiből, az elkészült példányt pedig eltárolja. A végterméket a getResult metódussal kérdezhetjük le. A Builder interfész segítségével a Director osztály hozza létre az összetett objektumot. A Product a konstruálandó objektum típusa. Példák: DocumentBuilder docBuilder = factory.newDocumentBuilder(); Document doc = docBuilder.newDocument(); Element rootElement = doc.createElement("products"); doc.appendChild(rootElement); XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(new FileOutputStream(xmlFile)); StartDocument startDocument = eventFactory.createStartDocument(); eventWriter.add(startDocument);eventWriter.add(newLine); StringBuilder sb = new StringBuilder(); for (Person person : contribs) { sb.append(person.getLastName()); sb.append("-"); }
3.1.4. Prototípus (Prototype) A prototípus minta olyan esetekben használatos, amikor több hasonló objektum előállítására van szükség, és ez az előállítás magas költségekkel jár. A mintának szüksége van egy már létező objektumra, amelyet lemásol, 69 Created by XMLmind XSL-FO Converter.
Tervezési minták
majd módosítja az attribútumait. Ennek megvalósítására klónozást alkalmazhatunk, amelynek implementálásáról a másolandó objektum osztályának kell gondoskodnia. A másolás igény szerint sekély és mély lehet.
Megjegyzés A sekély másolat (shallow copy) készítésekor az eredeti objektum és annak primitív típusú adattagjai másolódnak csak le, a referencia típusú alsóbb szintű (tartalmazott) objektumoknak csak a referenciái másolódnak le, vagyis ha az adott referenciával rendelkező objektum megváltozik, akkor a másolat is a megváltozottat éri majd el. A mély másolat (deep copy) készítése során az eredeti objektumnak nem csupán a primitív típusó adattagjairól, de a referencia típusú adattagok mögött álló objektumokról is másolat (méghozzá mély másolat) készül, vagyis a prototípus alapján legyártott objektum kezdőállapota megegyezik a prototípusáéval, azonban a legyártott objektum által hivatkozott objektumok függetlenek a prototípusobjektum elemeitől. Az Object osztályban a clone metódus áll rendelkezésre prototípusok készítésére, ez sekély másolatot készít.
4.7. ábra - A prototípus minta megvalósításának osztálydiagramja [OODesign]
A kliens létrehoz egy új objektumot a prototípusnak küldött klónozási kéréssel. A prototípus olyan felületet nyújt, amely lehetővé teszi a klónozást. A konkrét prototípusok valósítják meg a klónozási mechanizmust. Példa: public class Product implements Cloneable ... { @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } ... } public class X { ... public void y(...) { try { Product prod = (Product) prod.clone(); } catch (CloneNotSupportedException ex) { ... } ... } }
70 Created by XMLmind XSL-FO Converter.
Tervezési minták
3.2. Szerkezeti minták 3.2.1. Illesztő (Adapter) Az illesztő minta a nem összeillő interfészek együttműködését teszi lehetővé. Az egyik interfészhez olyan felületet rendel, amely becsatolható a másik interfészbe.
4.8. ábra - Az illesztő minta megvalósításának osztálydiagramja [OODesign]
A Target egy, a kliens által használt szakterületspecifikus interfész. Az Adaptee az adapter által a Target-hez alakított már létező interfész. Példa: public void X(String[] columnNames ...) { List<String> simpleTableColNames = Arrays.asList(columnNames); ... }
3.2.2. Összetétel (Composite) Az összetétel minta egy fastruktúrát kezel. Ez egy olyan hierarchia, amelynek segítségével rész-egész viszonyt fejezhetünk ki. Az egyes részek ugyanolyan módon kezelendők, mint az egész.
4.9. ábra - Az összetétel minta megvalósításának osztálydiagramja [OODesign]
71 Created by XMLmind XSL-FO Converter.
Tervezési minták
A Component egy olyan interfész, amelyet mind az összetett osztálynak, mind a levélelemnek implementálnia kell. A Leaf osztály reprezentálja a tovább már nem osztható elemeket, míg a Composite osztály objektumai további komponensekből épülnek fel. Példa jPanel2Layout.setVerticalGroup(jPanel2Layout .createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jLabel3) .addComponent(jPasswordField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jButton1) .addComponent(jLabel2) .addComponent(jButton2) ) );
3.2.3. Helyettes (Proxy) A helyettes, ahogyan a neve is mutatja, egy másik objektumot helyettesít, illetve a másik objektumhoz való hozzáférést felügyeli. Abban az esetben is használjuk, amikor az eredeti objektum létrehozása magas költségekkel jár. A helyettesítő egy másik objektum funkcionalitását mutatja a külvilág felé.
4.10. ábra - A helyettes minta megvalósításának osztálydiagramja [OODesign]
72 Created by XMLmind XSL-FO Converter.
Tervezési minták
A Subject interfészt implementáló RealSubject osztályt helyettesíti a Proxy, így a helyettesítőnek is implementálnia kell a Subject interfészt. Példa: A Subject mintaelemet reprezentáló interfész egy operációs rendszerbeli könyvtár műveleteit ábrázolja: public interface IFolder { public void performOperations(); }
A Folder osztály a RealSubject mintaelemet valósítja meg: public class Folder implements IFolder{ public void performOperations() { // különféle műveletek elvégzése a könyvtáron System.out.println("Művelet elvégzése"); } }
A proxyobjektum ugyanúgy viselkedik, mint a RealSubject (ez esetben a Folder), de többletfeladatként hozzáférés-ellenőrzést valósít meg: a username nevű, password jelszavú felhasználó számára engedélyezzük a műveletet, mások számára nem. Figyeljük meg, hogy proxyobjektum az ellenőrzést követően delegálja a műveletvégzést a Folder objektumnak!
Megjegyzés A User osztály itt egy felhasználói név és jelszó párost bezáró egyszerű objektum, kódját mellőzzük. public class FolderProxy implements IFolder { Folder folder; User user; public FolderProxy(User user) { this.user = user; } public void performOperations() { if ( user.getUserName().equalsIgnoreCase("username") && user.getPassword().equalsIgnoreCase("password")) { folder = new Folder(); folder.performOperations(); } else {
73 Created by XMLmind XSL-FO Converter.
Tervezési minták
System.out.println("Hozzáférés megtagadva!"); } } }
A hívás helyén így egyszerűen egy proxyobjektumot hozunk létre, amely a műveletvégzéskor a konstuktorában megkapott felhasználóra elvégzi a jogosultságellenőrzést: String username = ...; String password = ...; IFolder folder = new FolderProxy(new User(username, password)); folder.performOperations();
Megjegyzés Természetesen valamely létrehozási minta alkalmazásával lehetőségünk van a példányosítási függőség kimozgatására.
3.2.4. Pehelysúlyú (Flyweight) A pehelysúlyút akkor használjuk, amikor egy osztály több példányára is szükségünk van, és ezeknek az előállítása magas költségekkel jár. A memóriafogyasztás csökkentésére az osztályok osztozhatnak az objektumok külső állapotán.
4.11. ábra - A pehelysúlyú minta megvalósításának osztálydiagramja [OODesign]
A Flyweight interfész biztosítja azt a felületet, amelyen keresztül a pehelysúlyúak külső állapotokat kaphatnak, és ezeken különböző műveleteket végezhetnek. A ConcreteFlyweight osztály implementálja a Flyweight interfészt és tárolja a belső állapotot. Egy konkrét pehelysúlyú objektumnak megoszthatónak kell lennie, és kezelnie kell mind a belső, mind a külső állapotot. A FlyweightFactory osztály felelős a pehelysúlyúak előállításáért és közzétételéért. A gyár egy tárolót tart fenn a különböző pehelysúlyú objektumok számára, ahonnan példányokat ad vissza, ha azok már elkészültek, és ahová az új objektumokat menti el. A kliens feladata a pehelysúlyúak hivatkozása és a külső állapotok kezelése. Példák: A Java sztring-poolozása, valamint Attribute attribute = productStartElement.getAttributeByName(QName.valueOf("selector"));
3.2.5. Homlokzat (Façade) A homlokzat minta segítségével a kliens egyszerűbben kommunikálhat a rendszerrel. A háttérben álló osztályok, interfészek és egyéb programozás eszközök bonyolultságának növekedésével nehezedik a rendszer használata. A homlokzat minta egy olyan felületet biztosít, amelyen keresztül a külvilág számára egyszerűen érhetők el a bonyolultabb mechanizmusok.
74 Created by XMLmind XSL-FO Converter.
Tervezési minták
4.12. ábra - A homlokzat mintát megvalósító JFileChooser
Példa: ... FileFilter filter = new FileNameExtensionFilter("XML fájlok", "xml"); JFileChooser fc = new JFileChooser(); fc.addChoosableFileFilter(filter); int retVal = fc.showDialog(this, "Megnyitás"); if (retVal == JFileChooser.APPROVE_OPTION) { List importedProductList = xmlReader.readXmlWithDOM(fc.getSelectedFile()); productsById = new HashMap<>(); for (Product product : importedProductList) { productsById.put(product.getId(), product); } ... } ...
A fenti kódrészletben egy javax.swing.JFileChooser objektumot használunk ahelyett, hogy közvetlenül hivatkoznánk a mögötte álló bonyolult kódot.
3.2.6. Híd (Bridge) A híd minta az absztrakciót és az implementációt választja szét, hogy azok egymástól függetlenül változtathatóak maradhassanak. Ezzel egy időben összetétel segítségével közvetít is a kettő között. Az összetétel előnyt élvez az öröklődés alkalmazásával szemben. A híd felület konkrét osztályok funkcionalitását veszi át ahelyett, hogy interfészeket implementálna.
4.13. ábra - A híd minta megvalósításának osztálydiagramja [OODesign]
75 Created by XMLmind XSL-FO Converter.
Tervezési minták
Az Abstraction egy interfészt reprezentál. Az AbstractionImpl az absztrakciós interfészt implementálja az Implementor típus egyik objektumának segítségével. Az Implementor egy felületet nyújt az implementáló osztályok számára, amelynek nem szükséges közvetlenül megfelelnie az absztrakciós interfésznek. Az absztrakciós interfész implementációja azok alapján a műveletek alapján történik, amelyeket az Implementor interfész biztosít. A konkrét implementorok valósítják meg az Implementor interfészt. Példa: Az absztrakciót példánkban az egy alakzatot reprezentáló Shape interfész jelenti: public interface Shape { void colorIt(); }
Az absztrakciót implementáló osztály, amely Implementor típusú objektumot tartalmaz egy téglalapot ír le: public class Rectangle extends Shape { private Color color; public Rectangle(Color color) { this.color = color; } @Override public void colorIt() { color.fillColor(); System.out.print(" színezett téglalap"); } }
A megvalósítás absztrakcióját (vagyis a minta Implementor elemét) a Color interfész írja le: public interface Color { void fillColor(); }
Két konkrét implementációt készítettünk a Color interfészhez, egy piros és egy kék színű alakzat készíthető segítségükkel. public class RedColor implements Color { @Override public void fillColor() { System.out.println("Pirosra"); } }
76 Created by XMLmind XSL-FO Converter.
Tervezési minták
public class BlueColor implements Color { @Override public void fillColor() { System.out.println("Kékre"); } }
Alkalmazási példa: Shape s1 = new Rectangle(new RedColor()); Shape s2 = new Rectangle(new BlueColor());
A végrehajtás eredménye: Pirosra színezett téglalap Kékre színezett téglalap
Megjegyzés Ez a struktúra könnyen bővíthető mind új konkrét megvalósítással (jelen példa esetében színnel), mind pedig új absztrakciómegvalósításokkal (jelen példában további alakzatokkal, például körrel, stb.). Ezek ráadásul egymástól függetlenül bővíthetőek.
3.2.7. Díszítő (Decorator) A díszítő minta egy objektum funkcionalitását módosítja futási vagy fordítási időben. Másik neve csomagoló (Wrapper). Az új funkcionalitás vagy felelősségi kör megvalósítása nem érinti az eredeti objektumot.
4.14. ábra - A díszítő minta megvalósításának osztálydiagramja [OODesign]
A Component interfész objektumaihoz további funkciók adhatók. A ConcreteComponent implementálja a Component interfészt. A Decorator típus a Component objektumokra tartalmaz hivatkozást, és egy olyan felületet biztosít, amely illeszkedik a Component felületéhez. A ConcreteDecorator terjeszti ki a komponens funkcionalitását új állapot vagy műveletek hozzáadásával. Példa: AbstractTableModel twoSelTableModel ...; ... getContentPane().add(new JScrollPane(new JTable(twoSelTableModel)));
A fenti kódrészletben a tábla komponenst egy görgethető panel díszíti.
3.3. Viselkedési minták 77 Created by XMLmind XSL-FO Converter.
Tervezési minták
3.3.1. Sablonfüggvény (Template method) A sablonfüggvény minta egy metóduscsonkot hoz létre, amely néhány lépés implementálását az alosztályokra hagyja. A sablonmetódus meghatározza egy algoritmus lépéseit, amelyek némelyikéhez implementációt is ad. Ez minden leszármazott osztály által közösen használható.
4.15. ábra - A sablonfüggvény minta megvalósításának osztálydiagramja [OODesign]
Az AbstractClass azokat az egyszerű metódusokat határozza meg, amelyeket az algoritmus lépéseiként a leszármazott osztályoknak felül kell definiálniuk. A templateMethod sablonmetódus az algoritmus váza, amely a felüldefiniálandó metódusokat hívja. A ConcreteClass azokat a metódusokat implementálja, amelyek az algoritmus egyes lépéseit valósítják meg. Példa: A javax.swing.table.AbstractTableModel osztály minden nem absztrakt metódusa, például setValueAt(Object ertek, int sorindex, int oszlopindex);
3.3.2. Közvetítő (Mediator) A közvetítő minta egy központilag használt médiumot biztosít, amelyen keresztül a rendszer különböző objektumai kommunikálhatnak egymással. Amennyiben az objektumok közvetlenül egymással kommunikálnak, egy szorosan összekötött viszonban vannak, amelynek költséges a karbantartása, rugalmatlan és nehezen bővíthető. A közvetítő arra fókuszál, hogy az objektumok rajta keresztül lépjenek egymással interakcióba, így lazán kapcsoltak maradjanak.
4.16. ábra - A közvetítő minta megvalósításának osztálydiagramja [OODesign]
78 Created by XMLmind XSL-FO Converter.
Tervezési minták
A Mediator interfész nyújt felületet a Colleague objektumok kommunikációjához. A ConcreteMediator osztály hivatkozást tartalmaz a Colleague objektumokra, és üzeneteket közvetít közöttük. A Colleague osztályok hivatkozást tartalmaznak a közvetítőjükre, és azzal kommunikálnak ahelyett, hogy egy másik Colleague objektummal tennék azt. Példa public class BrowseAndSearch extends JFrame { ... private Login login; private BookShelf bookShelf; private ShoppingCart shoppingCart; private OrderDialog orderDialog; ... } public class BookShelf extends JDialog { ... private BrowseAndSearch parentFrame; ... }
3.3.3. Felelősséglánc (Chain of responsibility) A felelősséglánc a lazán csatoltság megvalósítására szolgál a szoftvertervezésben. A klienstől érkező kérésobjektum objektumok egy sorozatán (láncán) jár végig, amíg feldolgozásra nem kerül. A lánc minden objektuma kezelheti a kérést, továbbíthatja azt, vagy esetleg mindkettőt végezheti. A láncot felépítő objektumok maguk döntik el, hogy melyiküknek kell feldolgoznia a kérést, és hogy az továbbmenjen-e a láncon vagy sem. Ezzel biztosítjuk, hogy a kérést az arra legalkalmasabb objektum dolgozza fel.
Megjegyzés Ha valamelyik objektum feldolgozza a kérést, az általában nem halad már tovább a láncon, bár a konkrét megvalósítás dönthet másként. Köznapi példa erre egy üdítőautomata, ahol bár különféle pénzérmékkel fizethetünk, mégsincs külön nyílás minden pénzérmefajta számára, hanem a bedobott pénzérméket az automata egy felelősséglánc segítségével dolgozza fel: külön ellenőrzés tartozik minden pénzérmefajtához, de ha egy ellenőrzés nem képes meghatározni a pénzérme típusát, akkor továbbítja azt a következő ellenőrzőnek.
79 Created by XMLmind XSL-FO Converter.
Tervezési minták
4.17. ábra - A felelősséglánc minta megvalósításának osztálydiagramja [OODesign]
A Handler interfész a kérések kezelését biztosítja. A RequestHandler kezeli azt a kérést, amelyért ő a felelős. Amennyiben fel tudja dolgozni a kérést, megteszi, ellenkező esetben továbbküldi azt a következő kezelő láncszemnek. A kliens küldi el a kérést a lánc első elemének feldolgozásra. Példa: A Handler interfészt példánkban az EmailHandler interfész adja meg: public interface EmailHandler { void setNext(EmailHandler handler); void handleRequest(Email email); } ConcreteHandler megvalósításból többet is készítünk: az egyik az üzleti, a másik a gmail-es e-mail címre
érkező levelek megfelelő mappába mentését végzi. Ha a kezelő nem ismeri fel az e-mail cím végződését, akkor továbbítja, egyébként elvégzi a megfelelő feldolgozási műveleteket. public class BusinessMailHandler implements EmailHandler { private EmailHandler next; public void setNext(EmailHandler handler) { next = handler; } public void handleRequest(Email email) { if(!email.getFrom().endsWith("@businessaddress.com") { next.handleRequest(email); } else { // a kérés kezelése (megfelelő mappába mentés) } } } public class GMailHandler implements EmailHandler { private EmailHandler next; public void setNext(EmailHandler handler) { next = handler; } public void handleRequest(Email email) { if(!email.getFrom().endsWith("@gmail.com") { next.handleRequest(email); } else { // a kérés kezelése (megfelelő mappába mentés) }
80 Created by XMLmind XSL-FO Converter.
Tervezési minták
} }
Az EmailProcessor osztály a kezelők menedzselését végzi, segítségével dinamikusan tudunk új kezelőket hozzáadni, vagyis bővíteni tudjuk a felelősségláncot. public class EmailProcessor { // az előző kezelő referenciáját nyilvántartjuk, így könnyebb a következőt hozzáadni private EmailHandler prevHandler; public void addHandler(EmailHandler handler) { if(prevHandler != null) { prevHandler.setNext(handler); } prevHandler = handler; } }
Példa az alkalmazásra: Email email = ...; EmailProcessor ep = new EmailProcessor(); ep.addHandler(new BusinessMailHandler()); ep.addHandler(new GMailHandler()); ep.handleRequest(email);
Tipp Könnyebben olvashatóvá tehetjük a kódunkat, ha úgynevezett folyékony API-t (fluent API) készítünk. Ehhez módosítsuk az EmailProcessor osztály addHandler metódusát úgy, hogy ne eljárás legyen, hanem EmailProcessor példányt adjon vissza, mert ekkor az eredményobjektumon újabb műveleteket végezhetünk. ... public EmailProcessor addHandler(EmailHandler handler) { if(prevHandler != null) { prevHandler.setNext(handler); } prevHandler = handler; return this; } ... Email email = ...; new EmailProcessor() .addHandler(new BusinessMailHandler()) .addHandler(new GMailHandler()) .handleRequest(email);
3.3.4. Megfigyelő (Observer) A megfigyelő minta akkor hasznos, amikor egy objektum egy másik objektum állapotában érdekelt, és tudatában kell lennie (értesítést kell kapnia) az ebben bekövetkező változásokról. A megfigyelő mintában megfigyelőnek (Observer) hívják azt az objektumot, amely ellenőrzi egy másik objektum, a megfigyelt alany (Subject, Observable) állapotváltozásait.
4.18. ábra - A megfigyelő minta megvalósításának osztálydiagramja [OODesign]
81 Created by XMLmind XSL-FO Converter.
Tervezési minták
Az Observable vagy más néven alany (Subject) egy interfész vagy absztrakt osztály, amely metódusokat tartalmaz a megfigyelők kapcsolódására és lekapcsolódására. A ConcreteObservable osztály a megfigyelt objektum állapotát kezeli, és értesítést küld a felcsatolt megfigyelőknek, amennyiben ebben az állapotban változás következik be. Az Observer interfész vagy absztrakt osztály metódusokat tartalmaz a megfigyelt objektumok állapotváltozásainak feldolgozására. A ConcreteObserver osztályok az Observer interfész implementációi. Példa: A Java Swing osztálykönyvtárában számos helyen találkozunk a Megfigyelő mintával. Az alábbi példában a guiObject játssza az Observable szerepét (a minta attach metódusának megfelelője az addActionListener, amellyel az egyes eseménykezelők regisztrálhatják magukat a megfigyelhető felhasználóifelület-komponensnél). Az ActionListener interfész az Observer szerepében tűnik fel (az update metódus megfelelője itt az actionPerformed), míg maga a MyActionListener osztály egy konkrét megfigyelőt ír le. public class MyActionListener implements ActionListener { @Override public void actionPerformed(java.awt.event.ActionEvent evt) { //Exportáláshoz használt fájlformátum kiválasztása JComboBox cb = (JComboBox) evt.getSource(); String selectedFileFormat = (String) cb.getSelectedItem(); ... } }
A figyelő regisztrációját a megfigyeltnél az alábbi módon végezzük: guiObject.addActionListener(new MyActionListener());
Egy másik példa azt mutatja be, hogy hogyan alkalmazhatjuk a Megfigyelő mintát egy billentyűzetről beolvasott egész szám bináris, oktális és hexadecimális értékének a megjelenítésére. Ehhez először is definiálnunk kell egy Observer interfészt. Mivel minden konkrét megfigyelő kapcsolatban áll a megfigyelttel (subject-tel), ezért ezt absztrakt osztályként valósítjuk meg, amely rendelkezik egy Subject referenciával.
82 Created by XMLmind XSL-FO Converter.
Tervezési minták
public abstract class Observer { protected Subject subject; public abstract void update(); }
A Subject a megfigyelt objektum, amely a példában a beolvasott egész érték hordozója. Mindemellett nyilvántartja a nála feliratkozott megfigyelőket is. A notifyObservers metódus az összes regisztrált megfigyelőt értesíti. Erre az értesítésre az állapot változásakor ( setState) van szükség. import java.util.ArrayList; import java.util.List; public class Subject { private List observers; private int state; public Subject() { observers = new ArrayList<>(); } public int getState() { return state; } public void setState(int state) { this.state = state; notifyObservers(); } public void attach(Observer o) { observers.add(o); } public void detach(Observer o) { observers.remove(o); } private void notifyObservers() { for (Observer o : observers) o.update(); } }
Most már létrehozhatjuk a konkrét megfigyelőket megvalósító ConcreteObserver-eket, mint az Observer absztrakt osztály leszármazottait: public class BinObserver extends Observer { public BinObserver(Subject subject) { this.subject = subject; } @Override public void update() { System.out.println(Integer.toBinaryString(subject.getState()) + " BIN"); } } public class OctObserver extends Observer { public OctObserver(Subject subject) { this.subject = subject; } @Override public void update() { System.out.println(Integer.toOctalString(subject.getState()) + " OCT"); } } public class HexObserver extends Observer { public HexObserver(Subject subject) {
83 Created by XMLmind XSL-FO Converter.
Tervezési minták
this.subject = subject; } @Override public void update() { System.out.println(Integer.toHexString(subject.getState()) + " HEX"); } }
A főprogramban létrehozzuk a Subject-et, regisztráljuk nála az egyes megfigyelőket, majd mindaddig, amíg negatív számot nem olvasunk, egészeket olvasunk a billentyűzetről, és frissítjük vele a Subject objektum állapotát. import java.util.Scanner; public class ObserverMain { public static void main(String[] args) { Subject s = new Subject(); s.attach(new BinObserver(s)); s.attach(new OctObserver(s)); s.attach(new HexObserver(s)); Scanner sc = new Scanner(System.in); while (true) { System.out.print("Adjon meg egy számot: "); int x = sc.nextInt(); if (x < 0) break; s.setState(x); } sc.close(); } }
A program kimenetén lthatjuk, hogy az állapotbeállításra reagálva automatkusan lefutnak a megfigyelők update metódusai: Adjon meg egy számot: 10000000000 BIN 2000 OCT 400 HEX Adjon meg egy számot: 111 BIN 7 OCT 7 HEX Adjon meg egy számot: 1101 BIN 15 OCT d HEX Adjon meg egy számot: 0 BIN 0 OCT 0 HEX Adjon meg egy számot:
1024
7
13
0
-1
Tipp A Megfigyelő (Observer) minta támogatására a szabványos Java API-ban is találunk eszközöket, ezeket is alkalmazhatjuk!. Ezek a java.util csomagban elhelyezkedő Observable osztály (amely a megfigyeltet, vagy más néven a subject-et valósítja meg) és Observer interfész (amely a megfigyelők számára biztosít felületet). Ez azt jelenti, hogy nincs szükség saját Observer interfész megvalósítására, és a megfigyelők megfigyeltnél történő regisztrációra is alapból adott (ezt az Observable osztály biztosítja). A kódunk ekkor az alábbiak szerint alakul: import java.util.Observable; public class Subject extends Observable { private int state; public int getState() {
84 Created by XMLmind XSL-FO Converter.
Tervezési minták
return state; } public void setState(int state) { this.state = state; setChanged(); notifyObservers(); } }
Látható, hogy a Subject osztályból kikerültek a megfigyelők regisztrálására, deregisztrálására illetve értesítésére szolgáló metódusok, mert ezeket a Subject osztály az Observable-től (implementációval együtt) megörökli. A setState metódusban az Observable-től örökölt setChanged metódussal jelezzük, hogy megváltozott az állapot, és a notifyObservers-szel értesíttjük a regisztrált megfigyelőket. A konkrét megfigyelők implementációja is egyszerűsödik: nem kell nyilvántartanunk a figyelőben a megfigyeltet, mert azt mindig megkapjuk az update első paramétereként. import java.util.Observable; import java.util.Observer; public class BinObserver implements Observer { @Override public void update(Observable o, Object arg) { if (o instanceof Subject) System.out.println(Integer.toBinaryString(((Subject)o).getState()) + " BIN"); } } import java.util.Observable; import java.util.Observer; public class BinObserver implements Observer { @Override public void update(Observable o, Object arg) { if (o instanceof Subject) System.out.println(Integer.toOctalString(((Subject)o).getState()) + " BIN"); } } import java.util.Observable; import java.util.Observer; public class BinObserver implements Observer { @Override public void update(Observable o, Object arg) { if (o instanceof Subject) System.out.println(Integer.toHexString(((Subject)o).getState()) + " BIN"); } }
A főprogram is egyszerűsödik: a Subject objektum Observable-től örökölt addObserver metódusával végezzük a figyelők regisztrációját. import java.util.Scanner; public class ObserverMain { public static void main(String[] args) { Subject s = new Subject(); s.addObserver(new BinObserver()); s.addObserver(new OctObserver()); s.addObserver(new HexObserver()); Scanner sc = new Scanner(System.in); while (true) { System.out.print("Adjon meg egy számot: "); int x = sc.nextInt();
85 Created by XMLmind XSL-FO Converter.
Tervezési minták
if (x < 0) break; s.setState(x); } sc.close(); } }
3.3.5. Stratégia (Strategy) A stratégia minta egy feladat elvégzésre több különböző algoritmust biztosít, és a kliens dönti el, hogy ezek közül melyiket használja (például paraméter segítségével).
4.19. ábra - A stratégia minta megvalósításának osztálydiagramja [OODesign]
A Strategy interfész egy közös felületet nyújt a támogatott algoritmusok számára. Azt, hogy ennek mely ConcreteStrategy implementációja hajtódjon végre, a stratégia környezete dönti el. Minden konkrét stratégia osztály egy algoritmust implementál. A Context osztály hivatkozást tartalmaz egy stratégia objektumra, miközben a konkrét implementációkról semmit nem tud. Példák A Java Collections Framework-ben található Collections.sort metódus, amelynek tetszőleges Comparator implementációt adhatunk meg, valamint public class XmlReader { public List readXml(File xmlFile, String withAPI) { if ("DOM".equalsIgnoreCase(withAPI)) { return new DOMReader().readXmlWithDOM(xmlFile); } else if ("StAX".equalsIgnoreCase(withAPI)) { return new StAXReader().readXmlWithStAX(xmlFile); } else { return new SAXReader().readXmlWithSAX(xmlFile); } } } public class DOMReader { public List readXmlWithDOM(File xmlFile) {...} } public class SAXReader { public List readXmlWithSAX(File xmlFile) {...} } public class StAXReader { public List readXmlWithStAX(File xmlFile) {...} }
86 Created by XMLmind XSL-FO Converter.
Tervezési minták
3.3.6. Parancs (Command) A parancs minta laza kapcsolat megvalósítására szolgál egy kérés–válasz modellben. Ebben a mintában a kérés a hívónak küldődik, amely továbbítja azt a beágyazott parancsobjektum felé. A parancsobjektum továbbítja a kérést a fogadó megfelelő metódusához, hogy végrehajtódjon a kívánt tevékenység.
4.20. ábra - A parancs minta megvalósításának osztálydiagramja [OODesign]
A Command egy művelet végrehajtásához biztosít felületet. A ConcreteCommand a parancs interfészének megvalósítása a megfelelő műveletek meghívásával a fogadó objektumon. A kliens egy konkrét parancsobjektumot hoz létre, beállítva a fogadót. Az Invoker kéri meg a parancsobjektumot, hogy hajtsa végre a kérést. Egy parancs végrehajtásának folyamata a kliens kérésére indul el. Az Invoker veszi a parancsot, beágyazza, majd a ConcreteCommand objektum végrehajtja a kért parancsot és az eredményt visszaszolgáltatja a fogadónak. Példa A javax.swing.Action minden implementációja. Ezek ActionListener objektumként vannak a komponenshez hozzáadva. jComboBox1.addActionListener(new MyActionListener());
3.3.7. Állapot (State) Állapot mintát abban az esetben használunk, ha egy objektum megváltoztatja a viselkedését a belső állapota alapján. Ez a viselkedési mód if–else blokkok használata helyett állapotobjektumokkal valósul meg. A különböző állapottípusok mind ugyanannak az általános osztálynak a leszármazottai. Ez a stratégia egyszerűsíti és könnyen érthetővé teszi a kódot.
4.21. ábra - Az Állapot minta megvalósításának osztálydiagramja [Sourcemaking]
87 Created by XMLmind XSL-FO Converter.
Tervezési minták
Példa: Mozifilm-kölcsönző rendszer esetén az árképzés megvalósításánál figyelembe kell vennünk, hogy a filmek árkategóriái futásidőben is változhatnak, hiszen egy új kiadású film idővel kikerül az új kiadásúak számára fenntartott árkategóriából, és valamely más kategóriába kerül át (egyik állapotból a másikba jut). Mivel a Java nyelvben az objektumok nem képesek arra, hogy dinamikusan osztályt váltsanak, ezért az Állapot minta alkalmazásával érhetjük el a kívánt hatást. Ez esetben a környezet, vagyis a Context a Movie osztály lesz, a minta State elemének szerepét pedig a Price osztály játssza, amelynek leszármazottjai az egyes konkrét árképzési stratégiák (NewReleasePrice, ChildrensPrice, RegularPrice). Mivel a Movie osztály nem öröklődést, hanem objektumösszetételt használ, az egyazon mozifilm objektumhoz kapcsolódó Price objektum dinamikusan változtatható lesz. A Price egy absztrakt osztály, amely a különféle állapotok ősosztálya lesz. Ez hordozza az állapotfüggő viselkedést is, amely példánk esetén a film kölcsönzési árának kiszámítása a kölcsönzési napok függvényében. package hu.unideb.inf.prt.refactoring; public abstract class Price { public abstract double getCharge(int daysRented); }
A Movie osztály tartalmaz tehát egy Price referenciát, amely az adott mozifilm árát reprezentálja. Az állapotváltozásokkor a setState metódus meghívására van szükség, melynek paramétere egy konkrét állapot (a Price valamely leszármazottja) lesz. package hu.unideb.inf.prt.refactoring; public class Movie { private String title; private Price price; public Movie(String title, Price price) { this.title = title; this.price = price; } public Price getPrice() { return price; } public void setPrice(Price price) {
88 Created by XMLmind XSL-FO Converter.
Tervezési minták
this.price = price; } ... }
A konkrét állapotokat a Price alosztályai határozzák meg, az éllapotfüggő viselkedés pedig polimorf megvalósítással rendelkezik: package hu.unideb.inf.prt.refactoring; public class NewReleasePrice extends Price { @Override public double getCharge(int daysRented) { return daysRented * 3; } } package hu.unideb.inf.prt.refactoring; public class ChildrensPrice extends Price { @Override public double getCharge(int daysRented) { double thisAmount = 0; thisAmount += 1.5; if (daysRented > 3) thisAmount += (daysRented - 3) * 1.5; return thisAmount; } } package hu.unideb.inf.prt.refactoring; public class RegularPrice extends Price { @Override public double getCharge(int daysRented) { double thisAmount = 0; thisAmount += 2; if (daysRented > 2) thisAmount += (daysRented - 2) * 1.5; return thisAmount; } }
Az Állapot minta alkalmazására a 5. fejezet - Kódújraszervezés fejezethez tartozó videók között is találhat példát az olvasó.
3.3.8. Látogató (Visitor) A látogató minta az ugyanahhoz a típushoz tartozó objektumok csoportján végzendő műveletek elvégzésére szolgál. A minta segítségével a logikát az objektum osztályából egy másik osztályba vihetjük át. Különböző látogatók használatával különböző funkcionalitások rendelhetők a látogatott osztályhoz anélkül, hogy annak szerkezetében változás következne be.
4.22. ábra - A látogató minta megvalósításának osztálydiagramja [OODesign]
89 Created by XMLmind XSL-FO Converter.
Tervezési minták
A Visitor interfész határozza meg a látogat metódusokat minden látogatható típusra. A metódusok neve ugyanaz, de paraméterük eltér aszerint, hogy milyen látogatható osztályokhoz adnak visit metódust. A ConcreteVisitor a látogató interfész megvalósítása. Minden látogató más és más műveletekért felel. A Visitable interfész egy accept metódust definiál, amely belépési pontként szolgál a látogató metódusa számára. A ConcreteVisitable osztályok azok a típusok, amelyeken a látogató műveleteket végez. Az ObjectStructure osztály tartalmazza az összes látogatható objektumot, és mechanizmust kínál az objektumok bejárására is. Osztálya nem feltétlenül kollekció, lehet összetétel is. Példa: public interface PriceVisitable { public double accept(PriceVisitor visitor); } public interface PriceVisitor { public double visit(Item item); public double visit(Customer customer); } public class Item implements PriceVisitable { ... @Override public double accept(PriceVisitor visitor) { return visitor.visit(this); } ... }
90 Created by XMLmind XSL-FO Converter.
Tervezési minták
... public static class BDSPriceCalculator implements PriceVisitor { @Override public double visit(Item item) { double discountedTotalPrice = item.getProduct().getPrice(); discountedTotalPrice *= (1 - item.getArticle().getDiscounts()); discountedTotalPrice *= item.getPieces(); return discountedTotalPrice; } @Override public double visit(Customer customer) { double customerDiscount = 0; switch (customer.getCategory()) { case "NONE": {customerDiscount = 0; break;} case "SILVER": {customerDiscount = 0.05; break;} case "GOLD": {customerDiscount = 0.1; break;} ... } return customerDiscount; } } public class Order ... { ... public void calculateTotalPrice() { PriceVisitor priceVisitor = new ....BDSPriceCalculator(); double tp = 0; for (Item it : items) { tp += it.accept(priceVisitor); } this.totalPrice = tp; } ... }
3.3.9. Értelmező (Interpreter) Ez a minta egy nyelvtanhoz ad értelmezést. Általában fastruktúrában építi fel a nyelv elemeit, amelyhez egy szakterületet, egy nyelvtant és egy OO világra történő leképezést határoz meg.
4.23. ábra - Az értelmező minta megvalósításának osztálydiagramja [OODesign]
Az Expression osztályok az értelmezendő nyelv grammatikájának különböző típusú elemeit reprezentrálják.
91 Created by XMLmind XSL-FO Converter.
Tervezési minták
Példák: A reguláris kifejezések feldolgozása (java.util.regex csomag), valamint NumberFormat nf = NumberFormat.getInstance(); colValues.add(nf.format(book.getPrice()));
Az Értelmező minta egyik klasszikus példája a római számok értelmezése (és arab számokká alakítása). Az értelmezendő kifejezés egy olyan sztring, amelynek a környezetét a még nem értelmezett római szám sztring és a már elemzett résznek megfelelő szám együttesen alkotja. Ezt a környezetet négy értelmezőnek adjuk át: az egyik az ezresek, a másik a százasok, a harmadik a tízesek, míg a negyedik az egyesek értelmezését végzi (ebben a példában csak terminális kifejezések szerepelnek). A környezet kezdeti állapota a teljes átalakítandó római számot tartalmazó sztring és a 0 érték (a kimenő decimális). public class Context { private String input; private int output; public Context(String input) { this.input = input; } public String getInput() { return input; } public void setInput(String input) { this.input = input; } public int getOutput() { return output; } public void setOutput(int output) { this.output = output; } }
Az Expression osztály a kifejezések absztrakt ősosztálya, amely a környezet alapján megvalósítja az értelmezést. public abstract class Expression { public void interpret(Context context) { if (context.getInput().length() == 0) return; if (context.getInput().startsWith(nine())) { context.setOutput(context.getOutput() + (9 * multiplier())); context.setInput(context.getInput().substring(2)); } else if (context.getInput().startsWith(four())) { context.setOutput(context.getOutput() + (4 * multiplier())); context.setInput(context.getInput().substring(2)); } else if (context.getInput().startsWith(five())) { context.setOutput(context.getOutput() + (5 * multiplier())); context.setInput( context.getInput().substring(1)); } while (context.getInput().startsWith(one())) { context.setOutput(context.getOutput() + (1 * multiplier())); context.setInput(context.getInput().substring(1)); } } public public public public
abstract abstract abstract abstract
String String String String
one(); four(); five(); nine();
92 Created by XMLmind XSL-FO Converter.
Tervezési minták
public abstract int multiplier(); }
Az egyes terminális kifejezéseket megvalósító osztályok megvalósítják az absztrakt osztály metódusait. public class ThousandExpression extends Expression { public String one() { return "M"; } public String four(){ return " "; } public String five(){ return " "; } public String nine(){ return " "; } public int multiplier() { return 1000; } } public class HundredExpression extends Expression { public String one() { return "C"; } public String four(){ return "CD"; } public String five(){ return "D"; } public String nine(){ return "CM"; } public int multiplier() { return 100; } } public class TenExpression extends Expression { public String one() { return "X"; } public String four(){ return "XL"; } public String five(){ return "L"; } public String nine(){ return "XC"; } public int multiplier() { return 10; } } public class OneExpression extends Expression { public String one() { return "I"; } public String four(){ return "IV"; } public String five(){ return "V"; } public String nine(){ return "IX"; } public int multiplier() { return 1; } }
A kliens feladata a környezet létrehozása és a nyelvtan által definiált mondatokat reprezentáló szintakszisfa felépítése. Miután ez felépült, meghívja az interpret metódust a kiértékelés (vagyis a római–arab konverzió elvégzése) érdekében. public class MainInterpreter { public static void main(String[] args) { String roman = "MCMXXVIII"; Context context = new Context(roman); List<Expression> tree = new ArrayList<Expression>(); tree.add(new ThousandExpression()); tree.add(new HundredExpression()); tree.add(new TenExpression()); tree.add(new OneExpression()); for (Expression exp : tree) exp.interpret(context); System.out.println(roman + " = " + Integer.toString(context.getOutput())); } }
3.3.10. Bejáró (Iterator) A bejáró minta szabványos módot kínál objektumcsoportok, -kollekciók szekvenciális bejárására, amely során a kollekció elemei egyesével dolgozhatók fel. A bejárás különböző módokon történhet (minden elemet érintünk-e, milyen irányban haladunk). Az iterátor objektum elrejti a mögötte álló implementációt, és leveszi a kollekcióról a bejárás felelősségét.
4.24. ábra - A bejáró megvalósításának osztálydiagramja [OODesign]
93 Created by XMLmind XSL-FO Converter.
Tervezési minták
Az Iterator interfész kollekciók bejárására szolgáló metódusokat definiál, amelyeket a ConcreteIterator osztály valósít meg. Az Aggregate interfész vagy absztrakt osztály reprezentálja a bejárandó kollekciót, és egy iterátor objektum létrehozására alkalmas metódust. A ConcreteAggregate osztály az Aggregate interfész megvalósítása, amelynek elemeit a konkrét iterátor járja végig. Példák: A Java Collections Framework ezt a mintát használja az elemek bejárására, valamint XMLInputFactory inputFactory = XMLInputFactory.newInstance(); InputStream in = new FileInputStream(xmlFile); XMLEventReader eventReader = inputFactory.createXMLEventReader(in); ... while (eventReader.hasNext()) { XMLEvent event = eventReader.nextEvent(); ... } ...
3.3.11. Emlékeztető (Memento) Az emlékeztető minta abban az esetben hasznos, ha el kell tárolnunk és vissza kell állítanunk egy objektum állapotát. Az objektum mentett állapota nem érhető el az objektumon kívülről, így megőrizhető az állapotadatok integritása. A mementó minta két objektummal dolgozik: egy kezdeményezővel ( Originator) és egy gondozóval (Caretaker). A kezdeményező az az objektum, amelynek állapotát elmentjük, majd visszatöltjük. A mentés egy, a kezdeményező belső osztályaként létrehozott mementó objektumba kerül. A műveletet a gondozó végzi el.
4.25. ábra - Az emlékeztető minta megvalósításának osztálydiagramja [OODesign]
94 Created by XMLmind XSL-FO Converter.
Tervezési minták
A Memento osztály egy objektuma az Originator belső állapotát tárolja. A belső állapotot teszőleges számú adattag képviselheti. Az Originator belső osztálya a Memento , amelyet a tartalmazó osztály példánya hoz létre. Az Originator a Memento-t arra használja, hogy saját belső állapotát tárolja benne, majd visszatöltse azt. A Caretaker felelős a Memento megtartásáért, és azért, hogy az az Originator osztályon kívülről ne legyen módosítható. A Caretaker nem végezhet műveleteket a Memento-n. Az Originator egy visszavonási utasítás esetén használja a Memento-t előző állapota visszatöltésére. Példa: ... public class ShelfAndCartMemento { private final List bookShelfContent; private final List shoppingCartContent; public ShelfAndCartMemento(List shelf, List cart) { bookShelfContent = new ArrayList<>(shelf); shoppingCartContent = new ArrayList<>(cart); } public List getSavedBookShelfContent() { return bookShelfContent; } public List getSavedShoppingCartContent() { return shoppingCartContent; } } ... public ShelfAndCartMemento saveShelfAndCart() { List productsOnTheShelf = new ArrayList<>(); List productsInTheCart = new ArrayList<>(); Map<String, String> shelfAndCart = moreSelTableModel.getSelectionValuesById(); for (String productId : shelfAndCart.keySet()) { if (productsById.containsKey(productId)) { if (shelfAndCart.get(productId).equalsIgnoreCase("polcra")) { productsOnTheShelf.add(productsById.get(productId)); } else if (shelfAndCart.get(productId).equalsIgnoreCase("kosárba")) { productsInTheCart.add(productsById.get(productId)); } } } return new ShelfAndCartMemento(productsOnTheShelf, productsInTheCart); } public void restoreShelfAndCart(ShelfAndCartMemento memento) { List productsOnTheShelfAndInTheCart = new ArrayList<>(memento.getSavedBookShelfContent()); productsOnTheShelfAndInTheCart.addAll(memento.getSavedShoppingCartContent()); moreSelTableModel.setTableDataWithVisitor(productsOnTheShelfAndInTheCart); for (int i = 0; i < productsOnTheShelfAndInTheCart.size(); i++) { if (i < memento.getSavedBookShelfContent().size()) { moreSelTableModel.setValueAt("Polcra", i, moreSelTableModel.getColumnCount() - 1); } else { moreSelTableModel.setValueAt("Kosárba", i, moreSelTableModel.getColumnCount() - 1); } } jTable1.repaint(); } ...
95 Created by XMLmind XSL-FO Converter.
5. fejezet - Kódújraszervezés Az implementáció során gyakran olyan helyzetbe kerülünk, hogy érezzük, hogy valami „nem feltétlenül stimmel vele”. Olyan ez, mint amikor kinyitjuk a hűtőt, és hirtelen érzünk valami furcsa szagot. Ez nem feltétlenül jelenti azt, hogy valami romlott, lehet, hogy csak a sajtnak van olyan furcsa szaga, amilyennek lennie kell. Forráskódjaink esetén ez azt jelenti, hogy a kódunk is lehet „gyanús szagú”, amely mélyebb problémákat jelezhetnek, de fontos megjegyezni, hogy egy-egy ilyen gyanús szag nem feltétlenül jelenti azt, hogy a kódunkkal probléma van. A gyanús szag (bad smell) kifejezést programkódra először a ’90-es évek végén, a Refactoring: Improving the Design of Existing Code című könyv ([FOWLER1999]) szerzői használták. Nagyon sok minden jelezhet potenciális problémákat kódunkkal kapcsolatban. Például egy hosszabb metódus is már „bűzlik”, hiszen ez azt jelezheti, hogy nehéz gyorsan áttekinteni és megérteni, mit tesz a metódus, sőt, talán több felelősséget is megvalósít a kelleténél. Ez azonban önmagában még nem jelenti, hogy ezzel a metódussal biztosan probléma van: a hossz oka lehet az is, hogy például a grafikus felhasználó felület egy osztályaként sok felületetelemet kell elhelyezni, amely megnöveli a hosszát. A gyanúsan szagló kód felderítése során azonban sok tényezőt figyelembe kell venni. Ilyen tényező például a forráskód nyelve, a fejlesztés során alkalmazott módszertan, stb. Minden bad smell-hez tartozik egy úgynevezett baseline, azaz küszöbérték. A baseline alatti értékek még nem számítanak bad smell-nek, viszont a felette szereplő értékekkel rendelkező kódelemek már igen. Ezeket az értékeket a használt nyelv, az alkalmazott fejlesztőkörnyezet ismeretében kell megválasztani, mivel különböző helyzetekben eltérhetnek a még elfogadható értékek határai. Egy gyanús szagra adott konkrét példa az úgynevezett Long Method (hosszú metódus) bad smell. Erről akkor beszélünk, ha egy metódus hossza meghalad egy előre definiált küszöbértéket. Ezen érték meghatározásakor figyelembe kell venni a kérdéses programozási nyelvet, mivel nyelvfüggő lehet az, hogy egy adott problémát hány sorban, hány utasítással lehet megvalósítani. Figyelembe kell venni az alkalmazott kódolási konvenciókat is, illetve a rendszer célját és felhasználási környezetét is. Persze a hosszú metódusok nem feltétlenül okoznak komoly gondokat, de mégis célszerű foglalkozni velük (például több, kisebb metódusra bonthatjuk a hosszú metódust), mivel egy rövidebb metódust könnyebb átlátni, ami a karbantarthatóság szempontjából fontos. Az is előfordulhat persze, hogy a fejlesztő tisztában volt azzal, hogy a metódus hosszú lesz, viszont a feladat megoldása megkövetelte a küszöbérték átlépését. Egy másik gyanús eset, ha úgy találjuk, hogy adatosztályokat használunk (ez a Data Class bad smell). Ez azt jelzi, hogy az osztály szinte csak adattagokat, és azok lekérdező és beállító metódusait tartalmazza, amely szintén nem feltétlenül komoly probléma, vagy potenciális veszélyforrás, de mégis célszerűbb lenne az ilyen adatokat a felelősségek meghatározásának elve alapján felosztani (vagyis az adatok mellé kell rendelni a hozzájuk kapcsolódó funkcionalitást – a rajtuk végezhető műveleteket – is). Számos ilyen, potenciális problémát jelző gyanús szagot dokumentáltak már, [FOWLER1999] (és magyar nyelvű fordítása, [FOWLER2006]) is több ilyet leír. Fontos tehát megjegyezni, hogy attól, mert egy kód „bűzlik”, nem feltétlenül van vele baj. Ezek a szagok (smellek) elsősorban csak felhívják a fejlesztők figyelmét arra, hogy itt valamilyen potenciális hiba bújhat meg. Ha a fejlesztő megvizsgálja a helyzetet, dönthet úgy, hogy ezzel nem foglalkozik, mert esetleg szándékosan lett ilyen a kód (például a hosszú GUI osztály esetén). Ha azonban úgy ítéli meg, hogy ez bizony tényleg valamilyen probléma jelenlétére utal, akkor a vonatkozó kódrészeket javítani kell. Az ilyen javításokat nevezzük kódújraszervezésnek, vagy -átszervezésnek (refactoring-nak). A kódújraszervezés (refactoring) az a művelet, amikor a rendszerünk struktúráját úgy változtatjuk meg, hogy annak funkcionalitása nem módosul. A rendszer strukturális változtatásakor nem szükségszerű óriási, komplex változtatásra gondolni. Egy egyszerű osztályátnevezés is kódújraszervezés, ugyanúgy, ahogy egy metódus átalakítása, vagy kibontása. A refactoring minden egyes lépésekor figyelnünk kell arra, hogy a kód működése konzisztens maradjon az eredeti rendszerével. Amennyiben ezt a műveletet kézzel hajtjuk végre, úgy a kockázat is magas, mivel sokkal könnyebben kerülhetnek bele elírások (egy osztály átnevezésekor figyelni kell arra, hogy minden egyes helyen, ahonnan azt osztályt hivatkozzák, átírjuk a nevet), vagy hiányozhatnak az átszervezés lépései. Minden egyes átszervezés után a rendszert tesztelni kell(ene), ami nagyon idő- és erőforrásigényes. Amennyiben ez a művelet automatikusan történik, úgy a hiba kockázata sokkal kisebb, és a tesztelés is sokkal gyorsabban végrehajtható. Tény, hogy az átszervezés nem változtatja meg a szoftver megfigyelhető viselkedését. A szoftver továbbra is ugyanazt a tevékenységet végzi, mint korábban. A felhasználó, legyen az akár végfelhasználó, akár egy másik programozó, nem veszi észre, hogy megváltozott a rendszer szerkezete. 96 Created by XMLmind XSL-FO Converter.
Kódújraszervezés
Ez az észrevétel Kent Beck „két sapka” metaforájához vezet el minket. Szoftverfejlesztés során két különböző tevékenység között osztjuk fel az időnket: a szolgáltatások bővítése és az átszervezés között. Amikor új szolgáltatásokat adunk a programhoz, nem változtatjuk meg a meglévő kódot, csak új részeket adunk hozzá. Előrehaladásunkat tesztek létrehozásával és működőképessé tételével mérhetjük. Amikor átszervezünk, akkor szándékosan nem veszünk fel új tevékenységeket, csak átépítjük a kódot. Nem készítünk új teszteket (kivéve, ha egy korábban kihagyott esetet találunk), és csak akkor változtatunk meg egy tesztet, ha erre mindenképpen szükségünk van az interfész esetleges megváltozásának kezelése érdekében. A szoftver fejlesztése során valószínűleg gyakran kapjuk magunkat „sapkacserén”. Először megpróbálunk a kódhoz adni egy új szolgáltatást, és rájövünk, hogy ez sokkal egyszerűbb volna, ha más lenne a kód szerkezete. Tehát a másik sapkánkat magunkra öltve, eredeti kód fejlsztése helyett egy ideig átszervezési tevékenységeket végzünk. Amikor a kódnak már jobb a szerkezete, ismét magunkra öltjük fejlesztői sapkánkat, és megírjuk az új szolgáltatást. Amint az működőképes lesz, rájövünk, hogy túl bonyolultan kódoltuk, így ismét sapkacsere jön, és átszervezünk. Mindez talán csak rövid ideig (akár csak tíz percig) tart, de fontos, hogy mindvégig tisztában legyünk vele, hogy éppen melyik sapkánkat viseljük. Kódújraszervezés nélkül előbb-utóbb elkerülhetetlenül romlik a program szerkezete. Ahogy a programozók megváltoztatják a kódot (rövid távú célok megvalósítása érdekében, vagy a kód szerkezetének teljes átlátása nélkül), a kód elveszíti eredeti szerkezetét, és nehezebb lesz azt megérteni. A kódújraszervezés a kód rendbetétele: azért végezzük, hogy eltávolítsunk olyan darabokat, amelyek nem a megfelelő helyen vannak. A kód szerkezetének összekuszálódása halmozott hatásokkal jár: minél nehezebb átlátni a kód szerkezetét, annál nehezebb módosítani a programot és annál gyorsabban romlik a minősége. A rendszeres kódújraszervezés segít formában tartani a kódot. Egy rosszul tervezett program esetében általában több kód szükséges ugyanazon funkció megvalósításához, gyakran azért, mert a kód gyakorlatilag szó szerint ugyanazt végzi több különböző helyen. Ezért a felépítés javításának fontos szempontja a többször szereplő (ismétlődő) kódrészek eltávolítása. Ennek fontossága a kód jövőbeni módosításakor jelentkezik. A kód mennyiségének csökkentése nem eredményezi a rendszer gyorsabb futását, mindazonáltal nagy változást jelent a kódban. Minél több a kód, annál nehezebb megfelelően módosítani azt, hiszen több kódot kell megérteni. Gyakori probléma, hogy megváltoztatunk egy kódrészletet az egyik helyen, de nem változtatunk meg egy másik részt, amelyik nagyjából ugyanazt a feladatot végzi, csak kissé más környezetben. A megkettőzött kódok kiküszöbölésével azt érjük el, hogy a kód mindent csak egyszer tartalmaz, és ez a jó felépítés lényege (ez összhangban van a DRY alapelvvel). Az alábbi videók egy kódújraszervezési lépéssorozat alkalmazását szemléltetik:
1. Tesztvezérelt fejlesztés Tesztvezérelt fejlesztés (test-driven development, TDD) alatt egy olyan szoftverfejlesztési megközelítést értünk, amely a tesztelés és a kódfejlesztés folyamatát együttesen, egymástól szét nem választható módon, párhuzamosan végzi. A kód kifejlesztése inkrementális módon történik, és mindig magával vonja az adott inkremens tesztjeinek a fejlesztését is. Mindaddig nem léphetünk a következő inkremens fejlesztésére, amíg a korábbi kódok át nem mennek a teszteken.
97 Created by XMLmind XSL-FO Converter.
Kódújraszervezés
A tesztvezérelt fejlesztés eredetileg az extrém programozásnak (extreme programming, XP) nevezett agilis szoftverfejlesztési módszertan részeként jelent meg, de sikerrel alkalmazhatjuk nemcsak agilis, de hagyományos (terv központú) szoftverek kifejlesztése során is. A tesztvezérelt fejlesztés három törvénye: 1. Tilos bármilyen éles kódot írni mindaddig, amíg nincs hozzá olyan egységtesztünk, amely elbukik. 2. Tilos egyszerre annál több egységtesztet írni, mint ami ahhoz szükséges, hogy a kód elbukjon a teszt végrehajtásán. A fordítási hiba is bukásnak számít! 3. Nem szabad annál több éles kódot fejleszteni, mint amennyire ahhoz van szükség, hogy egy elbukó egységteszt átmenjen. Ez a három törvény azt jelenti számunkra, hogy először mindig egységtesztet kell készítenünk ahhoz a funkcionalitáshoz, amelyet le szeretnénk programozni. Azonban a 2. szabály miatt nem írhatunk túl sok ilyen tesztet: mihelyst az egységteszt kódja nem fordul le, vagy nem teljesül valamely állítása, az egységteszt fejlesztését be kell bejezni, és az éles kód írásával kell foglalkoznunk. A 3. szabály miatt azonban ilyen kódból csak annyit (azt a minimális mértékűt) szabad fejlesztenünk, amely a tesztet lefordíthatóvá és sikeresen lefuttathatóvá teszi. Katák a szoftverfejlesztésben A katák fogalma a japán harcművészetekből (cselgáncs, kendó, karate, aikidó) szivárgott be a szoftverfejlesztés területére. Eredetileg egy kata alatt egy olyan harcművészeti formagyakorlatot értünk, amely egyénileg vagy párban végrehajtható, részletesen megkoreografált mozgásmintát takar. A bowling kata A bowling katát Robert C. Martin (unclebob) írta le először. A gyakorlat célja egy olyan alkalmazás kifejlesztése, amely egy játékos vagy csapat bowling játékban elért pontjait számolja. „A bowlingot csoportban és egyénileg is lehet játszani. Lényege, hogy tíz mezőt (frame) kell teljesíteni, az első kilenc mezőben két lehetőségünk van a gurításra. Ha a bábukat (pin) egy gurításból letaroljuk – ezt nevezzük strike-nak –, akkor a következő két gurítás eredményét a gép automatikusan hozzáadja a tarolás eredményéhez. Abban az esetben, ha másodikra döntjük le a bábukat – ez a spare –, csak a következő gurítás eredménye adódik hozzá pluszban a pontszámokhoz. A játék legutolsó, tizedik framejében strike esetén azonnal legurítjuk a 2 jutalomdobást, spare esetén pedig szintén (persze csak egyet). A maximális 300-as eredményt 12 egymást követő strike adja. Az eredmény nyilvántartása, számolása: Ha nem sikerül két dobásból sem levinni az összes (10) bábut, akkor a ledobott bábuk száma adja a mező értékét (un. "nyitott" frame). Spare-nek hívjuk (jele /), ha két dobásból visszük le a 10 bábut. E mezőhöz az ezt követő gurítás eredménye adódik hozzá. Strike-nak nevezzük jele (X), ha az első gurításra ledöntjük mind a 10 bábut. Ilyenkor elmarad a második gurítás. E mezőhöz a következő két gurítás eredménye is hozzáadódik. Ha egy játékos a tizedik mezőt is hibátlanul teljesíti, akkor ezt követően kap még egy ún. »bónuszdobást«, mely spare esetén egy, strike esetén pedig plusz két dobást jelent. Lényegében minden frame-ben 30 pontot lehet elérni, így a 10 mező adja a maximum 300 pontot, amit tehát úgy lehet elérni, hogy a lehetséges 10 kezdő + 2 bónuszdobás mindegyike strike.” [ http://hu.wikipedia.org/wiki/Bowling#Szab.C3.A1lyai ]. Az alábbi ábrán egy bowlingjáték eredménye látható: egy-egy nagy négyzet egy-egy keretet (frame-et) jelképez, a felső sorban az egyes keretekben elért gurítások, míg az alsó részben a kumulatív pontszám látható. Az első keretben az első gurítás során egy, a második során négy bábu borult fel, ezért összesen 5 pontot szerzett a játékos. A második frame-ben első gurítása négy, a második öt bábut döntött fel, ezért a keret eredménye 9 pont, így a második keret végére 14 ponttal áll a játékos. A felső sorbeli tömör négyzet strike-ot, az átló mentén kettévágott négyzet pedig spare-t jelöl.
5.1. ábra - Egy bowling játék eredménye
98 Created by XMLmind XSL-FO Converter.
Kódújraszervezés
A legfontosabb tehát a tesztvezérelt fejlesztésse kapcsolatban, hogy megértsük, hogy csupán néhány nagyon egyszerű lépést ismétlünk újra és újra. Ezek az egyszerű kis lépések azonban remek kódolási tapasztalathoz juttathatnak minket. A tesztvezérelt fejlesztés tehát ciklikus tevékenység, mindig ugyanazokat a tevékenységeket hajtjuk végre alkalmazása során. Szokás ezt red–green–refactor cilkusnak is nevezni, mert az egységtesztek grafikus futtatói között bevált színséma segítségével határozhatjuk meg a teendőket: 1. Először is írni kell egy olyan tesztet, amely elbukik (red, hiszen a grafikus futtatók ezt a piros színnel jelölik). 2. Ezt követően alakítani kell a tesztelendő rendszeren mindaddig, amíg az átnem megy majd a teszten (green, hiszen ezt zölddel jelölik). 3. A harmadik lépsben az ismétlődések megszüntetése, a kód újraszervezése (refactoring).
5.2. ábra - A tesztvezérelt fejlesztés ritmusa [KACZANOWSKI2013]
Fontos, hogy megértsük a szemléletmódot: ha valamilyen megvalósítandó funkcionalitásról gondolkodunk, akkor azt tesztek formájában írjuk le előbb! Mivel ez a funkcionalitás még nem készült el, ezért a teszt nyilvánvalóan bukásra van ítélve (sőt, elképzelhető, hogy le sem fordul!). Az alapötlet itt az, hogy rögtön kezdjünk el a kliens fejével és szemszögéből gondolkodni a rövidesen megírásra kerülő kódunkkal kapcsolatban, és képesek leszünk a valóban fontos dolgokra összpontosítani. Mihelyst ezzel megvagyunk, jöhet a green lépés: megírjuk a funkcionalitást úgy, hogy a tesztelendő kód átmenjen a teszteken. Fontos, hogy csak kis lépésekben haladjunk! Ne akarjunk túlságosan előrehaladni. Lehetőség szerint mindig csak azt a minimális mennyiségű kódot írjuk meg, ami ahhoz kell, hogy a teszten átmenjen. Nem baj, ha már biztosan tudjuk, hogy az a későbbiekben nem lesz elég, de vegyük figyelembe, hogy lesz egy csomó más tesztünk is, amelyek mind valamilyen funkcionalitás meglétét vizsgálják. Ha túlságosan előre gondolkozunk, fennáll a veszélye, hogy a későbbi döntési lehetőségeink közül veszünk el, a szabadsági fokunkat szűkítjük egy esetlegesen túl korán meghozott döntéssel.
5.3. ábra - A tesztvezérelt fejlesztés ritmusa részletezettebben [KACZANOWSKI2013]
99 Created by XMLmind XSL-FO Converter.
Kódújraszervezés
A harmadik lépés a kód újraszervezése. Miután a tesztjeink sikeresen lefutnak, keresnünk kell a bad smelleket, azon pontokat a kódunkban, amelyek potenciális veszélyforrást jelenthetnek (például az ismétlődő kódrészek sértik a DRY alapelvet, a bonyolult feltételes logikák esetleg nincsenek összhangban a KISS alapelvvel, stb. A tesztjeink az újraszervezés során is jó szolgálatot tesznek: a feladatunk annyi, hogy az újraszervezés során mindvégig zölden tartsuk a futtatási eredményeket (ez látszik a részletesebb ábrán is), hiszen ez jelzi, hogy a kódunk kívülről (a kliens szemszögéből) ugyanúgy viselkedik, mint mielőtt nekikezdtünk az átszervezésnek. Ha bármikor pirosra fordul a jelző, akkor valamit hibáztunk, szóval vonjuk vissza az előző lépést.
Fontos Nem csak a tesztelendő kódra gondoljunk! Ugyanilyen fontos a tesztkód tisztány és érthetően tartása is, vagyis az újraszervezési lépéseket a tesztosztályainkon éppúgy végezzük el, mint a tesztelt osztályainkon!
Tipp A tananyaghoz kapcsolódó videók között a Bowling Kata bemutatásán keresztül nyújtunk betekintést a tesztvezérelt fejlesztés mikéntjébe.
2. Kódújraszervezési technikák Egy rendszer kódjának újraszervezésére számos technika létezik. A következőkben tekintsünk néhány gyakran használt átszervezési lépést. • Technikák a magasabb absztrakciós szint elérése érdekében: • Mezők egységbe zárása: A biztonságos kód érdekében az adattagokat el kell rejteni, és azokat csak lekérdező és beállító (getter/setter) metódusokon keresztül lehessen elérni. • Általános típusok: A típusok általánosításával a kód újrahasznosítható lesz. • Feltételek helyettesítése: A feltételek helyett polimorfizmus alkalmazása.
100 Created by XMLmind XSL-FO Converter.
Kódújraszervezés
• Típusellenőrzés helyettesítése: A típusellenőrzés helyett Állapot (State) illetve Stratégia (Strategy) minta alkalmazása. • Technikák a kód logikai felbontására: • Metódus szétbontása: A nagy metódusokat kisebb részekre bontva sokkal átláthatóbb, érthetőbb kódot kapunk, amely megkönnyíti a dolgunkat minden téren. Ez a technika mindig alkalmazható. • Osztály szétbontása: Egy osztály két, vagy több osztályra bontása, attól függően, hogy mely része milyen funkciót lát el. • Technikák a nevek és elhelyezkedések javítására: • Metódus illetve adattag mozgatása: Mindig a hozzá legjobban illő osztályba kell, hogy tartozzon a metódus vagy mező (ez a felelősségek megfelelő szétválasztásának igényéből adódó elvárás). • Metódus illetve adattag átnevezése: A legmegfelelőbb név választása, amely leírja az adattag vagy metódus funkcióját, szerepkörét. • Metódus illetve adattag „felhúzása” (Pull Up): Bizonyos metódusok illetve adattagok átmozgatása az ősosztályba. • Metódus illetve adattag „lenyomása” (Push Down): Az előzőhöz hasonlóan, csak ellenkező irányban: az ősből a leszármazottba mozognak a tagok.
3. Kódújraszervezési eszköztámogatás A szoftverrendszer kódjának újraszervezésére számos eszköz létezik, többek között az egyes IDE-kbe beépítve is. Azért érdemes ezeket alkalmazni, mert ezek az eszközök már a fejlesztési szakaszban rendelkezésre állnak, így már a fejlesztés során is sokkal gyorsabban és biztonságosabban leszünk képesek a szoftver átszervezésére. A különálló eszközök használata esetén hátrány, hogy a megszokott fejlesztőkörnyezettől eltérő környezetben kell elvégeznünk a szükséges átszervezéseket, majd visszatérni a jól megszokott felületünkhöz. Ez sok esetben zavaró lehet.Manapság a refactoring szerepe akkora, hogy gyakorlatilag minden IDE rendelkezik legalább minimális kódújraszervezési eszköztámogatással, mert felismerték, hogy a fejlesztés és karbantartás fázisának szerves része kell, hogy legyen ez a folyamat. Az Eclipse mint integrált fejlesztői keretrendszer, lehetőséget ad arra, hogy a projekteken minimális szintű kódújraszervezést hajtsunk végre. Ezeket részben beépített eszközökkel, vagy külön erre a célra fejlesztett pluginekkel tehetjük meg. A beépített eszközöket a Refactor környezeti menü elemeinek segítségével érhetjük el, ilyen például egy osztály átnevezése vagy áthelyezése úgy, hogy az összes, az osztályra mutató hivatkozás is frissül. Az Eclipse beépített eszközeit 3 csoportba sorolhatjuk: • Név és fizikai szerkezet változtatása: Ebbe a csoportba tartozik a mezők, változók, osztályok, interfészek átnevezése, továbbá a csomagok és osztályok mozgatása egy projekten belül. • Logikaiszerkezet-változtatás: A kód logikai felépítésének változtatása osztály szinten. Többek között a névtelen osztályok belső osztályokká, vagy belső osztályok legfelső szintű osztályokká alakítása; interfész létrehozása konkrét osztály alapján; metódusok, adattagok mozgatása ős-, illetve leszármazott osztályba; vagy éppen paraméterobjektumok bevezetése. • Adott osztályon belüli kódváltoztatás: Például lokális változók konvertálása adattaggá, vagy egy kijelölt kód metódussá történő kiemelése.
5.4. ábra - Az Eclipse Refactor menüje
101 Created by XMLmind XSL-FO Converter.
Kódújraszervezés
102 Created by XMLmind XSL-FO Converter.
6. fejezet - Adatkezelés A különböző programozási nyelven írt alkalmazások, bár sokrétűek és céljuk is eltérő lehet, egy dologban mégis megegyeznek: működésükhöz adatokra van szükségük. Adatok nélkül nem tudnák elvégezni a tőlük elvárt tevékenységet. Ezek az adatok különféle forrásokból származhatnak: adatbázisokból, egyszerű szöveges állományokból, XML dokumentumokból, JSON-fájlokból, CSV-fájlokból, Excel-táblázatokból, és így tovább. Ebben a fejezetben kétféle adatforrásból, adatbázisokból és XML dokumentumokból származó adatok feldolgozását ismerjük meg. Azt is megtanuljuk, hogy a Java nyelv milyen alkalmazásprogramozói felületeket biztosít ezeknek az adatforrásoknak az elérésére, feldolgozására és létrehozására. Az első alfejezet az XML dokumentumok feldolgozására szolgáló Java API-kat mutatja be, míg a második alfejezet adatbázisban tárolt adatok Java kódból történő feldolgozásáról szól.
1. XML dokumentumok kezelése Az online és egyéb rendszerek az adatok tárolására és továbbítására legtöbbször a széles körben elterjedt XML formátumot használják. Az XML dokumentum helyes és hibátlan feldolgozása megköveteli, hogy a dokumentum jólformált és érvényes legyen. A XML-dokumentum feldolgozása nem végezhető el, ha a jólformáltság követelményei nem teljesülnek. Az érvényesség ellenőrzésére a keretrendszerek validációs eljárásokat alkalmaznak, amelyek többféle XML sémanyelvre is képesek elvégezni az ellenőrzést (DTD és XML Schema). A Java több API-t is biztosít XML dokumentumok feldolgozásához. Ezek összefoglaló neve JAXP (Java API for XML Processing). Ez az API háromféle XML feldolgozó programkönyvtárat tartalmaz, ezek • a Document Object Model (DOM), • a Simple API for XML (SAX), és a • a Streaming API for XML (StAX).
Fontos Az XML-feldolgozók olyan programok, amelyek képesek XML-dokumentumokat beolvasni, továbbá hozzáférést biztosítanak a dokumentum tartalmához és szerkezetéhez. Az XML-feldolgozót egy másik program – például egy Java alkalmazás – vezérli azáltal, hogy az kéri a feldolgozót a dokumentum feldolgozására. Egy ilyen XML-feldolgozót tipikusan harmadik fél által készített (úgynevezett thirdparty) programkönyvtárként szerzünk be és használunk fel. Minden XML-feldolgozó köteles jelezni, ha az XML-specifikációban megszabott jólformáltsági követelmények nem teljesülnek. Mindhárom API közös jellemzője, hogy az Absztrakt gyár (Abstract factory) tervezési mintát használja az XML elemző létrehozására. Ez a módszer konfigurálhatóvá teszi a gyárat, így lehetőségünk nyílik például a használni kívánt implementáció kiválasztására. A gyár által előállított elemzőn szükség szerint további beállításokat is elvégezhetünk.
Megjegyzés Habár nem a JAXP része, de érdemes említést tenni a JAXB API-ról (Java Architecture for XML Binding), amely Java osztályok és XML sémák közötti leképezésre használatos. Az API marshallingnak nevezi a Java objektumok XML-re képezését, és unmarshallingnak az XML visszaképezését Java objektumokká. Az adatok a memóriában tárolódnak, a Java és XML közötti összeköttetés annotációk segítségével írható le. Azokban az esetekben ajánlott a JAXB API használata, amikor az XML specifikáció bonyolult és gyakran változik. Ilyen esetekben a Java és XML közötti szinkronizáció nehezen tartható karban a többi XML feldolgozó API segítségével. A JAXB.-ről bővebb információ többek között a Java tutorial JAXB fejezetében (http://docs.oracle.com/javase/tutorial/jaxb/) olvasható.
1.1. Áttekintés 103 Created by XMLmind XSL-FO Converter.
Adatkezelés
A JAXP API központi csomagja a javax.xml.parsers csomag. Ebben olyan gyártófüggetlen absztrakt gyár osztályok helyezkednek el, mint amilyen a SAXParserFactory, DocumentBuilderFactory és a TransformerFactory, amelyek egy SAX stílusú (SAXParser) és egy DOM stílusú (DocumentBuilder) feldolgozót, valamint egy XSLT-transzformátort tartalmaznak. Az absztrakt gyárként történő megvalósítás lehetővé teszi, hogy anélkül váltsunk egy másik gyártó XML implementációjára, hogy a forráskódunkon változtatni kellene. A ténylegesen használt implementáció a javax.xml.parsers.SAXParserFactory, a javax.xml.parsers.DocumentBuilderFactory, illetve a javax.xml.transform.TransformerFactory rendszerbeállítások alapján dől el, amit kódban a System.setProperties() metódushívással, ant build szkriptben a <sysproperty key="..." value="..."/>, míg parancssorban a -DpropertyName="..." JVM opció segítségével állíthatunk be. Ha nem állítjuk be ezeket, akkor a rendszer alapértelmezése alapján dől el, hogy mely implementáció kerül alkalmazásra, ez az Oracle JDK esetén az Apache Xerces (a SAX- és DOM-feldolgozók esetén) illetve Xalan (az XSLT-transzformátor esetén) programkönyvtárainak használatát jelenti. A három feldolgozási modell közül a SAX csak létező dokumentumok feldolgozását teszi lehetővé, de a másik két módszer (a DOM és a StAX) a létező dokumentumok feldolgozásán túlmenően egyaránt alkalmas dokumentumok létrehozására is.
1.1.1. SAX A SAX egy eseményvezérelt API, amely soros elérést biztosít az XML dokumentum elemeihez. Az egyes elemek feldolgozása független a korábban feldolgozott elemektől. SAX feldolgozás során szükség van egy úgynevezett kezelő objektumra (handlerre) is, amelyben leírjuk, hogy az XML dokumentum egy adott részének beolvasásakor mi történjen. A SAX API használata több programozási munkát igényel, mint a DOM API előkészítése. Az eseményvezérelt feldolgozást úgynevezett callback metódusok segítségével valósítják meg
Megjegyzés Callback, vagy visszahívás alatt egy olyan programozási modellt értünk, amikor egy futtatható kódot egy másik futtatható kódnak adunk át, amely majd a saját logikája alapján meghívja azt. Számos programozási nyelvben létezik erre eszközrendszer. C-ben például függvénymutatók segítségével valósíthatjuk meg, egy alprogramnak paraméterül egy függvénymutatót adva az alprogram képes lesz a függvénymutató által címzett függvény lefuttatására, akkor, amikor a saját logikája alapján úgy ítéli meg, hogy arra szükség van. Java nyelven az interfészek és a késői kötés tesznek jó szolgálatot a callback mechanizmus megvalósítása során: a SAX API részeként definiált interfészt az XML dokumentum feldolgozásában érdekelt programozó implementálja (ez a kezelő), de meghívni (az interfészre programozás alapelvének megfelelően) a SAX implementáció fogja. Az elemző (parser) ezeket a kezelőben definiált callback metódusokat hívja, amikor beolvassa az XML dokumentum egyes részeit. Nincs lehetőség a dokumentum korábban már beolvasott részeihez visszatérni. A SAX API-t más néven push-parsing API-nak is hívják, mivel az elemzés során bekövetkező eseményeket „rátolja” az azt feldolgozó alkalmazásra, amelynek az elemzési események feldolgozását tekintve nincs mozgástere a feldolgozásban, nem rajta múlik, hogy a dokumentum feldolgozása hogyan zajlik. Ehelyett a SAX feldolgozó az tehát, amely az XML dokumentum beolvasása során talált úgynevezett elemzési események – mint amilyen a dokumentum kezdete és vége, egy XML nyitócímke illetve zárócímke, karakteres adat olvasása, megjegyzés, feldolgozási utasítás, vagy akár dokumentumtípus-deklaráció olvasása – hatására meghívja a kezelőben leírt, az adott események kezelésére szolgáló callback metódusokat.
6.1. ábra - Az XML dokumentumok SAX stílusú feldolgozásának sematikus modellje
104 Created by XMLmind XSL-FO Converter.
Adatkezelés
A SAX API-t rendszeresen használják szervletek és hálózati programok esetében, amelyek kommunikációja XML dokumentumokon keresztül történik, mivel a SAX API gyors és kis memóriaigényű. 1.1.1.1. A SAX API főbb interfészei és osztályai A SAX API főbb elemeit a 6.2. ábra - A SAX API elemei [JAXPTutorial] ábra foglalja össze. A főbb osztályok és interfészek a következők:
6.2. ábra - A SAX API elemei [JAXPTutorial]
SAXParserFactory: A SAXParserFactory osztály feladata, hogy a javax.xml.parsers.SAXParserFactory
rendszertulajdonság (system property) által meghatározott elemző (parser) objektumot, vagyis magát az XMLfeldolgozót létrehozza.. SAXParser: Ez az absztrakt osztály reprezentálja magát a feldolgozót. Legfontosabb metódusai a parse metódusok, amelyek különféle forrásokból (File, InputStream, InputSource és String) olvasott XML
dokumentumok feldolgozását képesek elvégezni. Ahogyan a fenti ábrán is látható, a SAXParser megfelelő működéséhez szükség van négy interfészt (ContentHandler, ErrorHandler, DTDHandler, és EntityResolver) megvalósító objektumokra, amelyek valójában a feldolgozás mikéntjént határozza meg. Az ezen interfészeket megvalósító objektumok mintegy pluginként csatlakoztathatók a rendszerhez, és a SAXParser a callback mechanizmus segítségével hívja meg az így definiált metódusokat. Ezt a négy interfészt az Illesztő (Adapter) tervezési mintának megfelelően egy DefaultHandler objektum fogja össze, a feldolgozási logikát megvalósító programozó ennek alosztályaként hozza létre a feldolgozást végző kódot, amit a forrás mellett a parse metódus paramétereként kell átadni. Az elemzőnek tehát általában egy XML adatforrást és egy DefaultHandler kezelőobjektumot kap, majd ezután feldolgozza az XML dokumentumot miközben meghívja a kezelőobjektum megfelelő metódusait. SAXReader (valójában az API-ban ez XMLReader interfészként jelenik meg): A SAXParser magában foglalja a SAXReader-t . A SAXReader-nek kell „társalognia” az általunk definiált SAX eseménykezelővel. Itt adhatóak
hozzá az egyes kezelőinterfészeket megvalósító objektumok a feldolgozást végző kódhoz (például a 105 Created by XMLmind XSL-FO Converter.
Adatkezelés
void setContentHandler(ContentHandler handler);
metódus segítségével). ContentHandler: Ez a dokumentum logikai tartalmáról, főbb elemzési eseményeiről értesítést kapó interfész. Ennek metódusait hívja majd a SAXParser a feldolgozás során. Főbb metódusai a startDocument (a dokumentum kezdetéről kap értesítést), az endDocument (a dokumentum végének olvasásakor kerül meghívásra), a startElement (egy elem kezdetekor – vagyis a nyitócímke beolvasásakor – hívódik meg), az endElement (egy zárócímke beolvasásakor kerül meghívásra), illetve a characters(), amely szöveges adat
olvasására reagálva aktivizálódik. ErrorHandler: Metódusai, az error, fatalError, és a warning, akkor kerülnek meghívásra, ha elemzési
hibák következnek be. Az XML specifikáció definiálja, hogy milyen feltételek teljesülésekor milyen súlyosságú hibát kell a feldolgozónak jelentenie, ezzel összhangban értelmezendő a három metódus: a fatalError a végzetes (vagy más szóval helyreállíthatatlan, az XML specifikáció terminológiája szerint non-recoverable), az error a helyreállítható (recoverable) hibák esetén hívódik meg, míg a warning a figyelmeztetések (warning) esetén aktivizálódik. A hibakezelő alaphelyzetben a végzetes hibák esetén kivételt dob, a többi hibát ugyanakkor figyelmen kívül hagyja – így például a validációs hibákat is! – ezért alkalmazásunk helyes működése érdekében célszerű saját hibakezelőt írni, amelyet az elemző szintén callback-szerűen meghív. DTDHandler: a dokumentumtípus-deklarációkhoz kapcsolódó bizonyos események (jelölések és elemzetlen
egyedek deklarációja) esetén kerülnek meghívásra a metódusai. Az alkalmazások jelentős része ilyen eseményekkel nem foglalkozik. EntityResolver: a külső egyedek (beleértve a küldő DTD részhalmazt is) feloldását megelőzően biztosít
lehetőséget arra, hogy az alkalmazás elfogja (az Elfogás, avagy Interceptor minta alapján) a külső egyedeket, még mielőtt azok beemelésre kerülnének. Az alkalmazások jelentős többségének nem lesz szüksége ezen interfész megvalósítására. DefaultHandler: Az org.xml.sax.helpers.DefaultHandler osztály implementálja a ContentHandler, ErrorHandler, DTDHandler, és EntityResolver interfészeket (üres törzsű metódusokkal), így a saját
implementációnkban elég ezt az egy osztály kiterjeszteni, és a szükséges metódusait implementálni, és ezen osztály egy példánya kerül majd paraméterátadásra a feldolgozó parse metódusa számára. Ez tulajdonképpen az Illesztő (Adapter) tervezési minta megjelenési formája.
Megjegyzés Az org.xml.sax.ext csomag további interfészeket és osztályokat tartalmaz, amelyek a SAX továbbfejlesztett változatának, a SAX2-nek az eszközeit írják le. Egy SAX-feldolgozónak nem feltétlenül kell ezeket is megvalósítania, de egy SAX2-nek megfelelő feldolgozó ezen interfészek és osztályok segítségével a korábbiakban leírt eseményorientált módon biztosít lehetőséget arra, hogy értesítést kapjunk a fentieken túl például dokumentumtípus-deklarációs (elem-, attribútum-, belső- és külsőegyed-deklarációkról szóló) eseményekről (a DeclHandler interfész segítségével), illetve olyan lexikális eseményekről (a LexicalHandler interfészen keresztül), mint amilyen CDATA-szakaszok, egyedek, vagy akár DTD beolvasásának kezdete és vége, vagy egy, a dokumentumban elhelyezett megjegyzés. Ezekről az eseményekről az alapvető SAX API nem küld értesítést. Hasonlóan az alap SAX API-hoz, a SAX2-ben is alkalmazásra került az Illesztő (Adapter) minta, ezért a csomag tartalmaz egy DefaultHandler2 nevű osztályt is, amely a SAX2 eseményeinek kezelőmetódusaihoz is az üres implementációt valósítja meg.
1.1.2. DOM A dokumentum objektummodell, vagyis a DOM objektummodell a szó klasszikus, objektumorientált értelmében. A teljes dokumentum maga, és annak egyes részei is azonossággal, struktúrával, viselkedéssel és kapcsolatokkal rendelkező objektumok, amelyek belső állapotukat bezárják a külvilág elől. Természetesen másmás szerkezettel és kapcsolatokkal rendelkezik például egy feldolgozási utasítás (processing instruction), mint mondjuk egy elem (element). A Java SE-ben a DOM API elemei az org.w3c.dom csomagban helyezkednek el, mivel szabványosítását az XML specifikációt is jegyző World Wide Web Consortium (W3C) végezte, illetve végzi.
106 Created by XMLmind XSL-FO Converter.
Adatkezelés
A DOM felépítése moduláris, az egyes modulok jól körülhatárolható funkciót valósítanak meg. A 6.3. ábra DOM modulok ábrán látható, hogy milyen funkciók vannak elkülönítve. A teljesség igénye nélkül van például modul, az XPath-kifejezések kiértékeléséhez és dokumentumra illesztéséhez kapcsolatos interfészeket és osztályokat tartalmazza (XPath modul), van, amelyik a dokumentumok valamely sémanyelvvel szemben történő érvényesítéséhez szükséges konstrukciók összefogását végzi (Validation modul), és van, amelyik az XMLdokumentumok szabványos módon történő betöltését és elmentését teszi lehetővé (LS – Load/Save – modul). A legfontosabb modul minden kétséget kizáróan a magmodellt leíró Core, az összes többi modul közvelenül vagy közvetve, de ettől függ, az ez által biztosított objektumokat, interfészeket és azok szolgáltatásait veszik igénybe.
6.3. ábra - DOM modulok
http://www.w3.org/TR/DOM-Level-3-Core/introduction.html A DOM az XML-dokumentumot logikailag faként kezeli, amely Node típusú csomópontokból épül fel. A Node interfész alinterfészei pedig az XML-specifikáció által megengedett egységeket írják le, ilyen például az Element, a DocumentType, a CharacterData, stb. A DOM API főbb elemeit a 6.4. ábra - DOM API interfészei ábra mutatja be, amelyen a kékkel jelölt interfésze a DOM Core, míg a sárgával jelöltek a DOM XML moduljának elemei.
6.4. ábra - DOM API interfészei
107 Created by XMLmind XSL-FO Converter.
Adatkezelés
A DOM API egy faszerkezetet épít fel a memóriában (ez látható a 6.5. ábra - Az XML dokumentumok DOM stílusú feldolgozásának sematikus modellje ábra közepén) az XML dokumentumban található csomópontokból, amelyek két leggyakoribb típusa az elem csomópont és a szöveges csomópont. A DOM API használata lehetővé teszi csomópontok létrehozását, törlését, valamint a csomópontok tartalmának és magának a dokumentumszerkezetnek a megváltoztatását.
6.5. ábra - Az XML dokumentumok DOM stílusú feldolgozásának sematikus modellje
Nem túl nagy XML dokumentumok feldolgozása esetén célszerű a DOM API-t igénybe venni. A DOM API a teljes dokumentum tartalmát, illetve az abból épített teljes fát a memóriában tartja. Ezért ennek az XML feldolgozási módszernek a legnagyobb a memóriaigénye mind közül. Előnyei közé tartozik viszont a közvetlen hozzáférés a csomópontokhoz, ami általában jól illeszkedik a Java oldali megközelítéshez.
6.6. ábra - A DOM API elemei [JAXPTutorial]
108 Created by XMLmind XSL-FO Converter.
Adatkezelés
A javax.xml.parsers.DocumentBuilderFactory absztrakt gyár osztályt használjuk egy DocumentBuilder objektumpéldány létrehozására, amely egy Document interfészt megvalósító objektum előállítására szolgál. A builder objektumot a javax.xml.parsers.DocumentBuilderFactory rendszertulajdonság határozza meg, amely kiválasztja a builder gyártójának implementációját. A DocumentBuilder objektum magának az XMLfeldolgozónak az absztrakciója: képes akár egy létező XML dokumentum feldolgozásával előállítani egy dokumentumfát (ez a klasszikus értelemben vett XML-feldolgozás), de képes arra is, hogy a semmiből előbb egy üres dokumentumot reprezentáló Document csomópontot hozzon létre, amelyhez kapcsolódóan az Épító (Builder) tervezési minta alkalmazásával további dokumentumrészek (gyökérelem, illetve további elemek, karakteres adatok és egyéb XML konstrukciók) létrehozását és dokumentumfába illesztését elvégezze. A DocumentBuilder osztályban egy új, üres dokumentum létrehozására a newDocument metódus, míg létező dokumentumok feldolgozására a parse metódusok szolgálnak.
1.1.3. StAX A StAX egy eseményvezérelt, API, amely leginkább az állapotfüggő feldolgozásra helyezi a hangsúlyt. Más néven pull-parsing feldolgozásnak is nevezik az ilyen szemléletmódú feldolgozást, mivel itt a dokumentumot feldolgozni kívánó alkalmazás teljes egészében kontrollálja a feldolgozás folyamatát: feldolgozási tevékenység csak akkor történik, ha azt az alkalmazás kéri (vagyis szükség esetén lekéri – pull –a következő feldolgozási eseményt) Használata gyors, és kis memóriaigényű feldolgozást tesz lehetővé. A StAX API két részből áll: az egyik része az úgynevezett kurzor API, amely sok tekintetben a SAX API-ra hasonlít (nagyjából ugyanazok az események – startdocument, startelement, characters, stb. – vannak jelen, csak a feldolgozási mód, azaz az eseményekről történő értesülés különbözik jelentősen), a másik része pedig az úgynevezett eseményiterátor API, amely XML eseményekkel dolgozik. A
két
API
dokumentum
beolvasására
szolgáló
objektumait
ugyanaz
az
absztrakt
gyár,
a
javax.xml.stream.XMLInputFactory állítja elő. XMLInputFactory xif = XMLInputFactory.newInstance();
A kurzor API segítségével végzett olvasáshoz egy XMLStreamReader-re, míg az eseményiterátor API-val való olvasásáshoz az XMLEventReader szolgál. Ezek létrehozását mutatja be a következő két kódrészlet: XMLStreamReader reader = xif.createXMLStreamReader(new FileInputStream("products.xml")); XMLEventReader eventReader = xif.createXMLEventReader(new FileInputStream("products.xml"));
A továbbiakban az iterátor API-val ismerkedünk meg közelebbről.
1.1.4. A három API összehasonlítása táblázatos formában:
Megjegyzés 109 Created by XMLmind XSL-FO Converter.
Adatkezelés
A DOM és a StAX API-k XML olvasására és írására egyaránt használhatók, azonban a SAX API-val csak olvasni tudunk, írni nem. Az XSLT-t API biztosít lehetőséget XML dokumentum készítésére.
6.1. táblázat - XML-feldolgozási modellek jellemzői Tulajdonság
SAX
DOM
API típusa
Push parser
Memóriában tárolt fa Pull parser
Használat nehézsége
Közepes
Könnyű
Könnyű
XPath-támogatás
Nem
Igen
Nem
Teljesítmény (CPU és memória)
Jó
Attól függ
Jó
Csak előrefelé olvas
Igen
Nem
Igen
XML dokumentumok olvasása
Igen
Igen
Igen
XML dokumentumok írása
Nem
Igen
Igen
Create, Read, Update, Delete (CRUD) Nem műveletek támogatása
Igen
Nem
StAX
1.2. A SAX API használata A SAX API lehetővé teszi XML dokumentumok beolvasását és érvényességük ellenőrzését, létrehozásukhoz azonban már nem nyújt támogatást.
1.2.1. SAX feldolgozás Az alábbi példametódus egy XML fájlt olvas be, amely termékekről tartalmaz információkat. A feldolgozás alapjául szolgáló példadokumentum Az XML-feldolgozás lépéseit bemutató programkódok az alábbi struktúrával rendelkező XML dokumentumok feldolgozását végzi el: <products> <product id="ISBN9789630791205" selector="Book"> A titokzatos kék vonat <price>2200 0 8 The Mystery of the Blue Train 2011 Hercule Poirot;Rufus Van Aldin;Ruth Kettering;Katharine Grey HU;EN <series>Agatha Christie: Poirot történetei <seriesbeginyear>1920 Krimi Európa Kiadó 333 <product id="ISBN9789630793285" selector="Book"> Hétvégi gyilkosság
110 Created by XMLmind XSL-FO Converter.
Adatkezelés
lastname="Christie" marker="(1)"/> <price>2300 0 10 The Hollow 2011 Hercule Poirot;Lucy Angkatell HU;EN <series>Agatha Christie: Poirot történetei <seriesbeginyear>1920 Krimi Európa Kiadó 317 <product id="ISBN9630779641" selector="Book"> Gyilkosság az Orient expresszen <price>1800 0 6 Murder on the Orient Express 2006 Hercule Poirot;Ratchett HU;EN <series>Agatha Christie: Poirot történetei <seriesbeginyear>1920 Krimi Európa Kiadó 281 <product id="ISSND5999545581110" selector="DVD"> Nevem Senki <starring> <starring> <musicby> <price>990 0 20 1973 HU;IT Western;Vígjáték Klub Publishing Kft.
111 Created by XMLmind XSL-FO Converter.
Adatkezelés
112 <product id="ISSND999546330748" selector="DVD"> Szárnyát vagy combját? <starring> <starring> <musicby> <price>1100 0 5 L'aile ou la cuisse 2003 Charles Duchemin;Jacques Tricatel HU;FR Vígjáték Klub Publishing Kft. 105
A fájl feldolgozását először a SAX API segítségével végezzük. A feldolgozás során a dokumentumban tárolt termékek alapján termékobjektumokat (Product osztálybeli objektumokat) hozunk létre, amelyeket egy listában tárolunk el. 1 public List readXmlWithSAX(File xmlFile) { 2 final List productList = new ArrayList<>();
A SAX elemzőt egy gyár objektum segítségével hozzuk létre. A gyár objektumot a SAXParserFactory osztály newInstance metódusa segítségével készítjük el. A visszakapott gyár állítja elő a SAX elemzőt. Az elemző parse metódusa hajtja végre a feldolgozást, és állítja elő a termékeket tartalmazó listát. A parse metódus első paramétere a beolvasandó XML fájl neve, második paramétere pedig a kezelőobjektum. 8 9 10
try { SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser saxParser = factory.newSAXParser();
A DefaultHandler osztályt kiterjesztő implementáció egy névtelen osztály, amelynek a definícióját az objektumának példányosításakor adjuk meg. Mivel a későbbiekben nem lesz szükség erre az osztályra, csupán egyetlen példányára, ezért lokális névtelen osztályként hoztuk létre.
10
DefaultHandler handler = new DefaultHandler() { Product product; Method productMethod; String whichElement; String elementValue;
A ContentHandler interfészben definiált startElement metódust újraimplementáljuk. Abban az esetben, ha az XML-ben egy termék nyitótagja az esemény, akkor a Java oldalon létrehozunk egy új termék objektumot, amely egy könyv vagy egy DVD. Ellenkező esetben a termék egy adattagjának nyitóeleméről van szó, ekkor megjegyezzük ennek az adattagnak a nevét, erre szolgál a névtelen osztály whichElement adattagja. A metódus akkor hívódik meg, amikor a SAX olvasó egy új XML elemhez ér. 8 9
@Override public void startElement(String uri, String localName, String qName,
112 Created by XMLmind XSL-FO Converter.
Adatkezelés
Attributes attributes) throws SAXException { 10 try { 11 if (qName.equalsIgnoreCase("product")) {
A termék kezdőelemének "selector" attribútumának értéke alapján dől el, hogy könyv vagy DVD-e az adott termék. A startElement metódus attributes paramétere tartalmazza az elem összes attribútumát. Ennek getValue metódusa segítségével egy konkrét attribútum értékét kaphatjuk vissza, ha paraméterként megadjuk az attribútum nevét. if (attributes.getValue("selector").substring(0, 1).equalsIgnoreCase("B")) { product = new Book(); 10 } else { product = new DVD(); } ... 15 } if (xmlToJavaElements.keySet().contains(qName)) { whichElement = qName; } ... 20 } catch (InstantiationException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException LOGGER.log(Level.SEVERE, null, ex); } }
ex) {
A ContentHandler interfészben definiált endElement metódust is újraimplementáljuk. Abban az esetben, ha egy termék zárótagja az esemény, akkor már minden adattagja értéket kapott, és a kész termékobjektumot eltároljuk a listában. Ellenkező esetben a termék egy adattagjának záróeleméről van szó, ekkor az adattag előzőleg (a startElement és a characters metódusokban) elmentett értékét átadjuk a termék megfelelő adattagjának. Az adattaghoz tartozó beállító metódust az adattag nevéből állítjuk elő (a névtelen osztály whichElement adattagja vagy az endElement metódus qName paramétere), míg a beállító metódus paraméterének értékét a névtelen osztályunk elementValue adattagjától kérjük el. Az xmlToJavaElements kollekció az adattagok és az XML dokumentumbeli megfelelőjük neve közötti leképezéseket tárolja. Az endElement metódus akkor hívódik meg, amikor a SAX olvasó egy XML elem zárótagjához ér. @Override public void endElement(String uri, String localName, String qName) throws SAXException { 10 try { if (xmlToJavaElements.keySet().contains(qName)) { Field productField = xmlToJavaElements.get(qName); productMethod = productClass.getMethod(getSetterName(productField.getName()), productField.getType());
A termék adattagjainak értéke esetünkben szöveg vagy egyszerű típusú érték, például egy szám. if (productField.getType().getCanonicalName().equals("java.lang.String")) { productMethod.invoke(product, elementValue); 10 } else { productMethod.invoke(product, productField.getType().getMethod("valueOf", String.class).invoke(null, elementValue)); } } 15 if (qName.equalsIgnoreCase("product")) { productList.add(product); } } catch (IllegalArgumentException | SecurityException | IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) { LOGGER.log(Level.SEVERE, null, ex); 20 } }
113 Created by XMLmind XSL-FO Converter.
Adatkezelés
A ContentHandler interfészben definiált characters metódust is újraimplementáljuk. Ennek segítségével olvassuk be a szöveges elemek értékét, amelyet a névtelen osztály elementValue adattagjában tárolunk. A metódus akkor hívódik meg, amikor a SAX olvasó egy szöveges XML elemhez ér. @Override public void characters(char ch[], int start, int length) throws SAXException { elementValue = new String(ch, start, length); }
10 };
Az elemző parse metódusát meghívva előáll a termékeket tartalmazó lista. A parse metódus első paramétere a beolvasandó XML fájl (neve), míg második paramétere az imént létrehozott kezelőobjektum. Az XML fájlt a parse metódusnak többféleképpen is átadhatjuk: fájlobjektumként vagy a fájl nevét sztringként (lásd API dokumentáció). saxParser.parse(xmlFile, handler); //saxParser.parse(xmlFileName, handler); } catch (ParserConfigurationException | SAXException | IOException ex) { LOGGER.log(Level.SEVERE, null, ex); } return productList;
10
}
1.2.2. Az XML dokumentum érvényességének ellenőrzése (validáció) Az XML dokumentumok ellenőrizhetők Java kódból jólformáltság és érvényesség szerint, azaz hogy egy előre meghatározott szerkezeti felépítést követnek-e. A jólformált XML dokumentumok a minden XML dokumentumra érvényes alapvető szintaktikai szabályok szerint épülnek fel. A dokumentumok akkor érvényesok, ha jólformáltak, és egy adott sémanyelv (például a DTD vagy az XML Schema) által előírt szabályoknak is megfelelnek. Ha ellenőrizni szeretnénk a feldolgozandó dokumentum érvényességét, akkor ezt a setValidating metódus segítségével kell a SAX elemzőt előállító gyárral közölnünk. SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(true);
Ekkor a gyár egy validáló elemzőt fog legyártani, amennyiben az lehetséges (vagyis létezik olyan SAX feldolgozó megvalósítás a rendszertulajdonságokkal megadott konfigurációban, amely támogatja a validációt). Azt, hogy az elemző validáló legyen, explicit módon kell kérnünk, mert alapértelmezés szerint az előállított feldolgozók nem validálnak. Ha a validálást XML Schema ellenében végezzük, a fentieken kívül el kell végeznünk még néhány beállítást. Elsőként a gyár állapotának módosításával hatást gyakorolunk a gyártás folyamatára (ebben az esetben azt kérjük tőle, hogy névtérképes és érvényesítő feldolgozót gyártson le számunkra), majd beállítjuk a legyártott SAX elemzővel kapcsolatban elvárt tulajdonságokat. A következő példában ilyen tulajdonság segítségével jelezzük az XML-feldolgozónak, hogy sémanyelvként az XML Schema nyelvet szeretnénk használni (ez JAXP 1.2 alatt SAXNotRecognizedException-t dob). static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setNamespaceAware(true); factory.setValidating(true); SAXParser saxParser = factory.newSAXParser(); try { saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA); } catch (SAXNotRecognizedException x) { throw new RuntimeException("Hiba: JAXP SAXParser tulajdonság nem azonosítható: " + JAXP_SCHEMA_LANGUAGE, x); }
Beállítunk egy megfelelő hibakezelőt is, azaz az ErrorHandler interfészhez saját implementációt adunk. Ehhez a feldolgozót reprezentáló saxParser objektumtól előbb el kell kérnük az XML beolvasó objektumot (6.2. ábra
114 Created by XMLmind XSL-FO Converter.
Adatkezelés
- A SAX API elemei [JAXPTutorial] ábrán SAXReader néven találkoztunk vele), amelynek a setErrorHandler metódusa segítségével állíthatjuk be a hibakezelőt.
Megjegyzés Ugyanezt úgy is megvalósíthatjuk, ha a DefaultHandler-ből származtatott kezelőosztályunkban felüldefiniáljuk ezeket a metódusokat, és az ez alapján létrehozott kezelőobjektumot adjuk a saxParser objektum parse metódusának paraméterként. saxParser.getXMLReader().setErrorHandler(new ErrorHandler() { @Override public void warning(SAXParseException ex) throws SAXException { System.out.println("Figyelmeztetés: " + getParseExceptionInfo(ex)); } @Override public void error(SAXParseException spe) throws SAXException { String message = "Error: " + getParseExceptionInfo(spe); throw new SAXException(message); } @Override public void fatalError(SAXParseException spe) throws SAXException { String message = "Fatal Error: " + getParseExceptionInfo(spe); throw new SAXException(message); } });
Összerendeljük az XML dokumentumot egy sémával. Ez elvégezhető XML dokumentumból éppúgy, mint Java kódból. Utóbbi esetben a sémadokumentum helyét szintén egy property beállításával hozhatjuk a feldolgozó tudomására. static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource"; if (schemaSource != null) { saxParser.setProperty(JAXP_SCHEMA_SOURCE, new File(schemaSource)); }
A példában a schemaSource változó a sémát tartalmazó fájl neve.
1.3. A DOM API használata A DOM API lehetővé teszi XML dokumentumok beolvasását és érvényességük ellenőrzését, valamint létrehozásukhoz is támogatást nyújt.
1.3.1. DOM feldolgozás Az XML dokumentumok DOM feldolgozása során felépített fa csomópontokból áll, amelyek különféle típusúak lehetnek. Az alábbi táblázat a fontosabb csomóponttípusokat foglalja össze.
6.2. táblázat - XML dokumentumok csomópontjai Csomópont
A csomópont neve
A csomópont értéke
Attr
Az attribútum neve
Az attribútum értéke
CDATASection
#cdata-section
A CDATA-szakasz tartalma
Comment
#comment
A megjegyzés tartalma
Document
#document
null
DocumentFragment
#document-fragment
null
DocumentType
A dokumentumtípus neve
null
Element
Az elem neve
null
Entity
Az egyed neve
null 115
Created by XMLmind XSL-FO Converter.
Adatkezelés
Csomópont
A csomópont neve
A csomópont értéke
EntityReference
A hivatkozott egyed neve
null
Notation
A jelölés neve
null
ProcessingInstruction
A feldolgozási utasítás célja
A cél kivételével a teljes tartalom
Text
#text
A szöveges csomópont tartalma
Ezeknek a csomóponttípusoknak az ismerete alapvető fontosságú a DOM-mal történő feldolgozás során. Az alábbi példametódus egy állományból olvas be egy XML dokumentumot, amely termékekről tartalmaz információkat. A fájl feldolgozása a DOM API segítségével történik. A feldolgozás során termékobjektumokat hozunk létre, amelyeket egy listában tárolunk el. public List readXmlWithDOM(File xmlFile) { List productList = new ArrayList<>();
Az XML-feldolgozót (vagyis a DocumentBuilder objektumot) az alapértelmezett gyár állítja elő. A builder olvassa be és elemzi a parse metódusának paramétereként átadott XML dokumentumot, amelyből előállítja a faszerkezetet, majd visszatér a fa gyökércsomópontjával. A gyökér egy Document típusú objektum, amely a belépési pontot jelenti a dokumentumot reprezentáló fába. try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); Document doc = docBuilder.parse(xmlFile); ... Product product = null;
Fontos Az XML dokumentum technikai értelemben történő feldolgozása a fenti kódban látható három sorral kész is van. A gyártó legyártotta a feldolgozót, amely a parse metódus meghívásakor felépítette a DOM fát a memóriában. Innentől kezdve a feldolgozó feladata csupán annyi, hogy a kliens számára a DOM API-n keresztül hozzáférést biztosítson a fa tartalmához és szerkezetéhez. Elképzelhető, hogy az elemzés sikertelen, amennyiben például a dokumentum nem jólformált. Ezt a parse metódus egy SAXException kivételobjektum eldobásával jelzi. A DOM fa gyökérelemén keresztül közvetlenül férhetünk hozzá annak bármely leszármazottjához, tekintet nélkül arra, hogy az hol helyezkedik el a hierarchiában. A getElementsByTagName metódus paraméterként kapja az XML elem nevét, és visszaadja az összes ilyen nevű elemet mint csomópontot. Ezeket egy listába szervezi, amelynek típusa NodeList. Egy csomópontlistát ciklussal járunk be ugyanúgy, mint bármely más kollekciót. A lista elemeinek típusa Node. A Java osztályok és az XML dokumentum elemei közötti leképezés Példánkban – annak érdekében, hogy a reflektív programozás bizonyos elemeit is bemutathassuk – a szakterületi Java osztályaink elemeit különféle annotációkkal ellátva végezzük az XML dokumentumok elemei és a Java objektumaink közötti megfeleltetést. Nem célunk itt az összes szakterületi osztály és definiált annotáció bemutatása, csak a példakódok megértéséhez szükséges mértékben megyünk bele a részletekbe. Definiáltunk többek között az XmlElement és az XmlAttribute annotációtípusokat. Az XmlElement annotációval osztályokat és adattagokat jelölhetünk el, az annotáció pedig azt mondja meg, hogy az adott osztálynak illetve adattagnak az annotációtípus value paraméterének értékével meghatározott XML elem feleljen meg. @Target({ElementType.TYPE, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface XmlElement { public String value(); public String selectorAttribute() default "selector"; }
116 Created by XMLmind XSL-FO Converter.
Adatkezelés
Az XmlAttribute annotáció adattagokat jelölhet, jelentése, hogy az adott adattag az XML dokumentum valamely (a value paraméter értékével meghatározott nevű) attribútumának felel meg. @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface XmlAttribute { public String value(); }
A fentiek alkalmazására példa a Product osztály: @XmlElement("product") public abstract class Product @XmlAttribute("id") private String id;
{
@Collection(keyElement = "*", valueElement = "person", elementName = "contributors", valueType = Person.class, setterMethodName = "addContributors", setterMethodArgTypes = {String.class, Person[].class}) private Map<String, List> contributors; @XmlElement("price") private Double price = 0.0; @XmlElement("discount") private Double discounts = 0.0; @XmlElement("inventory") private Integer inventory; @XmlElement("title") private String title; @XmlElement("originaltitle") private String originalTitle; @XmlElement("yearofpublishing") private Integer yearOfPublishing; @XmlElement("characters") private String characters; @XmlElement("plot") private String plot; @XmlElement("languages") private String languages; @XmlElement("series") private String series; @XmlElement("seriesbeginyear") private Integer seriesFromYear; @XmlElement("category") private String category; @XmlElement("keywords") private String keywords; @XmlElement("publisher") private String publisher; @XmlElement("length") private Integer length; ... }
117 Created by XMLmind XSL-FO Converter.
Adatkezelés
String productTagName = productClass.getAnnotation(XmlElement.class).value(); NodeList productNodeList = doc.getElementsByTagName(productTagName); for (int i = 0; i < productNodeList.getLength(); i++) {
A NodeList objektum item metódusa segítségével kérhetjük le az adott pozíción elhelyezkedő vagy a soron következő elemet (csomópontot). Node productNode = productNodeList.item(i);
A csomópontok típusa eltérő lehet aszerint, hogy az XML dokumentumban milyen szerepet töltenek be. Esetünkben elem típusú csomópontokat dolgozunk fel, ezért megvizsgáljuk, hogy az aktuális csomópont megfelelő típusú-e (elem csomópont-e). if (productNode.getNodeType() == Node.ELEMENT_NODE) {
Ha elem a csomópont, biztonságosan Element típusúra kényszeríthetük. Ez szükséges ahhoz, hogy hozzáférhessünk olyan speciális metódusokhoz, amelyek definíciója az Element interfészben található, nem pedig ősében, a Node interfészben. Element productElement = (Element) productNode;
Az elem csomópontok tartalmazhatnak attribútumokat, amelyeket a getAttribute metódus segítségével kérdezhetünk le. A metódusnak az attribútum nevét adjuk meg paraméterként. A getAttribute az attribútum értékével mint sztringgel tér vissza. Esetünkben, ha az attribútum értéke "B", akkor a szóban forgó termék egy könyv, egyébként egy DVD. if (productElement.getAttribute("selector").substring(0, 1).equalsIgnoreCase("B")) { product = new Book(); } else { product = new DVD(); } Field[] productFields = productClass.getDeclaredFields(); String tagName = null; Method productMethod = null;
Mivel a DOM fa már létrejött, és úgyszólván közvetlen hozzáférésünk van az egyes elemeihez, nem az XML dokumentumban lévő sorrendet vesszük alapul a bejárás során, hanem Java oldalról közelítünk: a Product osztály adattagjain iterálunk végig, és kérjük le a nekik megfelelő XML elemeket. for (Field productField : productFields) { productMethod = productClass.getMethod(getSetterName(productField.getName()), productField.getType());
Az adattagot módosító XmlAttribute annotáció tartalmazza az adattag XML-beli attribútumnevét, amelyet az Element típusú productElement objektum getAttribute metódusának átadva megkapjuk az attribútum értékét, majd a beállító metódust meghívva eltároljuk ezt az értéket a termékobjektum megfelelő adattagjában. if (productField.isAnnotationPresent(XmlAttribute.class)) { tagName = productField.getAnnotation(XmlAttribute.class).value();
Egy termék adattagjainak értéke itt szöveges vagy egyszerű típusú érték lehet, például egy szám. if (productField.getType().getCanonicalName().equals("java.lang.String")) { productMethod.invoke(product, productElement.getAttribute(tagName)); } else { productMethod.invoke(product, productField.getType().getMethod("valueOf", String.class).invoke(null, productElement.getAttribute(tagName))); } }
118 Created by XMLmind XSL-FO Converter.
Adatkezelés
Az adattag XmlElement annotációval van ellátva, amely tartalmazza az adattag XML elem nevét, amelynek alapján az Element típus getElementsByTagName metódusa egy csomópont listát ad vissza. else if (productField.isAnnotationPresent(XmlElement.class)) { tagName = productField.getAnnotation(XmlElement.class).value(); NodeList elementNodeList = productElement.getElementsByTagName(tagName);
Amennyiben az adattagon van XmlElement annotáció, de az XML dokumentum nem tartalmazza az adott elemet, akkor semmit nem teszünk. if (elementNodeList.getLength() == 0) { continue; }
A csomópontokat tartalmazó listának csupán az első elemére van szükségünk, mert egy terméken belül egy adattagot reprezentáló XML elem csak egyszer szerepelhet. Az első elem csomópont szöveges értékét a getTextContent metódus segítségével kérdezhetjük le. if (productField.getType().getCanonicalName().equals("java.lang.String")) { productMethod.invoke(product, elementNodeList.item(0).getTextContent()); } else { productMethod.invoke(product, productField.getType().getMethod("valueOf", String.class).invoke(null, elementNodeList.item(0).getTextContent())); } }
Egy Element típusú objektum közvetlen leszármazottait a getChildNodes metódus segítségével kaphatjuk meg. A csomópontlista bármely eleme közvetlenül elérhető a megfelelő index használatával ( kk). NodeList valueNodeList = keyElement.getChildNodes(); for (int kk = 0; kk < valueNodeList.getLength(); kk++) { Node valueNode = valueNodeList.item(kk); ... productList.add(product); ... } catch (...) { ... } return productList; }
1.3.2. Validáció XML dokumentumok validációja a DOM stílusú feldolgozás során is hasonló módon történik, mint SAX API esetében. Mivel a JAXP elemzői alapértelmezett esetben figyelmen kívül hagyják a névtereket, ezért explicit módon kérni kell (a gyártó objektumtól), hogy olyan elemző jöjjön létre, amely tekintetbe veszi a névtereket is. Ez a beállítás a sémavalidáció megfelelő működéséhez szükséges. factory.setNamespaceAware(true); factory.setValidating(true); //csak XSD-vel szembeni validáció esetén állítjuk be a használni kívánt //elemző nyelvet: static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; try { factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA); } catch (IllegalArgumentException ex) {
119 Created by XMLmind XSL-FO Converter.
Adatkezelés
... }
Különbség a SAX API-hoz képest, hogy ott magán a XML feldolgozón végeztük el a beállítást, míg a DOM API esetében ezt a gyáron végezzük.
1.3.3. XML dokumentum létrehozása DOM API segítségével A DOM API lehetőséget nyújt egy fastruktúra XML fájlba írására is, vagyis Java kódból XML dokumentum létrehozását is elvégezhetjük. A példametódus egy termékeket tartalmazó listát ír ki egy XML fájlba. A terméklistából a DOM API segítségével hozunk létre egy fastruktúrát, amelyet egy transzformációt megvalósító objektum segítségével írunk ki a paraméterként kapott fájlba. A JAXP API az XML dokumentumok stíluslap-transzformációs feladatainak elvégzését segítendő (a W3C XSLT-specifikációja alapján) tartalmaz egy XSLT API-nak nevezett programkönyvtárat is (szokás ezt néha TrAX-nak is nevezni, mint Transformation API for XML). A 6.7. ábra - Az XSLT API elemei [JAXPTutorial] ábra mutatja be ennek főbb elemeit, egy TransformerFactory gyártó osztályt, amely segítségével a transzformációkat elvégző Transformer objektumok legyárthatóak. Az XSLT API általános célja, hog segítségével egy XML dokumentumból egy transzformáció segítségével egy másik XML dokumentumot állítsunk elő, azonban ezt a funkcióját nem csak stíluslap-transzformációk alkalmazása során hasznájuk ki, hanem olyankor is, ha a különféle XML reprezentációk között szeretnénk transzformációt végezni. Példánkban egy DOM faként tárolt forrásdokumentumból egy szöveges állományként tárolt céldokumentumot szeretnénk előállítani.
6.7. ábra - Az XSLT API elemei [JAXPTutorial]
public class TestTest { public void writeXmlWithDOM(File xmlFile, List productList) throws ... {
A XML-feldolgozót reprezentáló DocumentBuilder objektumot az alapértelmezett gyár segítségével állítjuk elő. A builder objektum newDocument metódusával egy üres dokumentumot hozunk létre, amelynek egyetlen eleme van: a most létrejött gyökér. Ehhez a gyökérelemhez adjuk hozzá a későbbiekben a terméklista elemeit, mint a gyökérelem leszármazottait. A termékcsomópontokhoz pedig a termék adattagjait reprezentáló csomópontokat, és így tovább. A lista bejárása és feldolgozása után előáll a termékcsomópontokat tartalmazó fastruktúra, amelyet a transzformer ír ki a megadott XML fájlba. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); Document doc = docBuilder.newDocument();
120 Created by XMLmind XSL-FO Converter.
Adatkezelés
Új csomópontot a Document típus createElement metódusával hozhatunk létre. A metódus paramétereként kapja meg az új csomópont nevét. Itt az XML dokumentum gyökérelemét, a products elemet hozzuk létre. Element rootElement = doc.createElement("products");
A gyökérelemet hozzáadjuk a dokumentumfához. Az appendChild metódus a csomópontot egy új utolsó gyermekkel bővíti (vagyis a gyermekek listájának végére történik segtségével a beszúrás). doc.appendChild(rootElement);
Figyelem Ne feledjük, hogy az XML dokumentumainkban kétféle gyökérről is beszélhetünk: létezik a teljes dokumentumot reprezentáló csomópont (ezt a DOM-ban a Document interfész írja le), illetve szűkebb értelmezésben beszélheünk az elemek hierarchiájának a gyökeréről (amely egy DOM Element típusú csomópont), amely a többi elemhez képest kitüntetett, hiszen az XML dokumentum tényleges tartalma (ideértve a többi elemet és a szöveges adatokat is) csak ezen belül helyezkedhet el. A paraméterként kapott terméklistát bejárva minden termékből csomópontot képezünk, amelyeket hozzáadunk a fához. for (Product product : productList) { Element productElement = doc.createElement("product"); rootElement.appendChild(productElement);
Beállítjuk a termék elem attribútumait. Attr attr = doc.createAttribute("selector"); String selectorValue = (product instanceof Book ? "Book" : "DVD"); attr.setValue(selectorValue);
A beállított attribútumot hozzáadjuk a termék elemet reprezentáló csomóponthoz. Az attribútum nevét az adattagot módosító XmlAttribute annotáció tárolja, míg az attribútumérték az adattag értéke lesz, amelyhez a megfelelő lekérdező metódus segítségével férhetünk hozzá. productElement.setAttributeNode(attr); for (Field productField : ... ) { XmlAttribute xmlattr = productField.getAnnotation(XmlAttribute.class); ... productElement.setAttribute(xmlattr.value(), "" + productMethod.invoke(product)); XmlElement xmlelem = productField.getAnnotation(XmlElement.class); productMethod = ... //getter Element elem = doc.createElement(xmlelem.value()); elem.appendChild(doc.createTextNode("" + productMethod.invoke(product))); productElement.appendChild(elem);
A dokumentum szerkezetét reprezentáló fastruktúra elkészülte után következik a fájlba írás. Ehhez egy Transformer objektumot hozunk létre, amelynek transform metódusa alakítja át az első paramétereként kapott, a faszerkezetet tartalmazó DOMSource objektumot a második paramétereként kapott, a kimeneti fájlt reprezentáló StreamResult objektummá.
Megjegyzés A transform metódus első paramétere egy javax.xml.transform.Source interfészt implementáló osztály példánya, második paramétere pedig egy javax.xml.transform.Result interfészt implementáló osztály példánya, ami azt jelenti, hogy tetszőleges forrásobjektumból állítható elő tetszőleges eredményobjektum (lásd 6.7. ábra - Az XSLT API elemei [JAXPTutorial] ábrát). Ez egyben az XML-feldolgozó API-k közötti átjárhatóságot is biztosítja, lévén, hogy például DOM fából, mint forrásból (DOMSource) SAXResult-ot vagy akár StAXResult-ot is előállíthatunk a transzformáció segítségével, amely tulajdonképpen egy csővezeték (pipe) megvalósítását jelenti, hiszen a DOM fa tartalma szerializálva a SAX- illetve StAX-feldolgozó bemenetére kerül.
121 Created by XMLmind XSL-FO Converter.
Adatkezelés
TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); DOMSource source = new DOMSource(doc); StreamResult result = new StreamResult(xmlFile); // StreamResult result = new StreamResult(System.out); transformer.transform(source, result); } } } }
A transzformációs API-n túlmenően is van lehetőségünk az XML dokumentumok szerializációjára, hiszen a DOM 3-as szintjének LS modulja (Load/Save) szabványos lehetőséget biztosít XML dokumentumok mentésére és betöltésére. A DOM 3 LS API használatának a legfőbb elemei az alábbiak: public static void writeXMLWithDOMLS(Document doc, String xmlFile) throws ClassNotFoundException, InstantiationException, IllegalAccessException, ClassCastException, FileNotFoundException { DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); DOMImplementationLS domImplLS = (DOMImplementationLS) registry.getDOMImplementation("LS"); LSSerializer ser = domImplLS.createLSSerializer(); ser.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); LSOutput out = domImplLS.createLSOutput(); out.setEncoding("UTF-8"); out.setCharacterStream(new OutputStreamWriter(new FileOutputStream(xmlFile))); ser.write(doc, out); }
A DOM implementációk gyűjteményéből le kell kérni egy LS modult megvalósító implementációt, ha ilyen létezik, majd ezen implementáció segtségével egy szerializáló objektum létrehozására van szükség. A szerializáló működését különféle paraméterekkel befolyásolhatjuk, a példában az emberi fogyasztásra is szánt dokumentumok számára kvázi kötelező format-pretty-print paraméter beállításának mikéntje látható. A szerializáló objektum egy DOM csomópont kiírását képes elvégezni, ez példánkban a dokumentumfa gyökere. A kiíráshoz azonban szükség van egy LSOutput-ra, ahová a kimenet megy majd. A kimenetre vonatkozóan előírjuk az UTF-8-as karakterkódolást, és beállítjuk, hogy milyen stream-re menjen a kimenet. Végül a szerializátor objektum write metódusával tudjuk a kívánt csomópontot a kivánt kimenetre írni.
1.4. A StAX API használata A StAX API lehetővé teszi XML dokumentumok beolvasását és érvényességük ellenőrzését, ezenkívül eszközöket biztosít létrehozásukhoz is.
1.4.1. StAX feldolgozás A StAX API-val történő feldolgozás során az iterátor API típusait és metódusait használjuk. A példametódus egy állományból olvas be egy XML dokumentumot az eseményiterátor API segítségével. A dokumentum termékekről tartalmaz információkat. A feldolgozás során termékobjektumokat hozunk létre, amelyeket egy listában tárolunk el. public List readXmlWithStAX(File xmlFile) { List productList = new ArrayList<>();
Az XML eseményeket olvasó objektumot az alapértelmezett gyár állítja elő. A createXMLEventReader gyártó metódus paramétere az XML fájl bemeneti adatfolyam objektumként. Az ily módon létrehozott XMLEventReader objektumot használjuk fel az események beolvasására. try { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); InputStream in = new FileInputStream(xmlFile); XMLEventReader eventReader = inputFactory.createXMLEventReader(in);
122 Created by XMLmind XSL-FO Converter.
Adatkezelés
Product product = null; Method productMethod = null;
Az XML dokumentum beolvasása egy while ciklus segítségével történik. Mindddig beolvassuk a következő eseményt, amíg el nem érjük a dokumentum végét. while (eventReader.hasNext()) { XMLEvent event = eventReader.nextEvent();
Ha
az
esemény
egy
elem
kezdő tagja, interfészt
javax.xml.stream.events.StartElement StartElement típus metódusait.
akkor az eseményt megvalósító objektummá,
átkonvertáljuk hogy elérjük
a a
if (event.isStartElement()) { StartElement productStartElement = event.asStartElement();
Az elem getName metódusa a teljes minősített nevet tartalmazza, de nekünk csak a lokális névre van szükségünk. A név lokális részét a getLocalPart metódus segítségével kérhetjük el. if (productStartElement.getName().getLocalPart().equalsIgnoreCase("product")) {
Egy elem adott nevű attribútumát a getAttributeByName metódussal kaphatjuk meg, amely egy javax.xml.namespace.QName objektumot vár paraméterként. QName objektumot kétféleképpen készíthetünk sztring objektumból: a QName konstruktorával vagy statikus valueOf metódusával. Az attribútum értékét a javax.xml.stream.events.Attribute típus getValue metódusával kérdezhetjük le.
Megjegyzés A QName osztály az XML specifikációban megadott teljesen minősített nevet reprezentál, amely tehát egy névtér-URI-ból, egy lokális részből és egy prefixből áll. // Attribute attribute = startElement.getAttributeByName(new QName("selector")); Attribute attribute = productStartElement.getAttributeByName(QName.valueOf("selector")); if (attribute.getValue().substring(0, 1).equalsIgnoreCase("B")) { product = new Book(); } else { product = new DVD(); }
Ha a StartElement objektum a termékobjektum egy adattagját leíró nyitóelem olvasásának eredményeként keletkezett, akkor továbbmegyünk a következő eseményre, amely az XML elem szöveges értékét tartalmazó csomópont beolvasása, majd eltároljuk ezt az értéket a termékobjektum megfelelő adattagjában. if (event.isStartElement()) { String startElementName = event.asStartElement().getName().getLocalPart(); ... if (xmlToJavaElements.keySet().contains(startElementName)) { event = eventReader.nextEvent(); ... if (productField.getType().getCanonicalName().equals("java.lang.String")) { productMethod.invoke(product, event.asCharacters().getData()); } else { productMethod.invoke(product, productField.getType().getMethod("valueOf", String.class) .invoke(null,event.asCharacters().getData())); } continue; }
Tipp 123 Created by XMLmind XSL-FO Converter.
Adatkezelés
Amennyiben többféle esemény is érdekel, az egyes eseményfajták szerinti olvasást egy switch szerkezettel végezhetjük el legkönnyebben: event = eventReader.nextEvent(); switch (event.getType()) { case XMLStreamConstants.ATTRIBUTE: ... break; case XMLStreamConstants.CDATA: ... break; case XMLStreamConstants.CHARACTERS: ... break; case XMLStreamConstants.COMMENT: ... break; case XMLStreamConstants.DTD: ... break; case XMLStreamConstants.END_DOCUMENT: ... break; case XMLStreamConstants.END_ELEMENT: ... break; case XMLStreamConstants.ENTITY_DECLARATION: ... break; case XMLStreamConstants.ENTITY_REFERENCE: ... break; case XMLStreamConstants.NAMESPACE: ... break; case XMLStreamConstants.NOTATION_DECLARATION: ... break; case XMLStreamConstants.PROCESSING_INSTRUCTION: ... break; case XMLStreamConstants.SPACE: ... break; case XMLStreamConstants.START_DOCUMENT: ... break; case XMLStreamConstants.START_ELEMENT: ... break; }
Ha elérünk egy XML elem vége eseményt, és ez a termék elem vége, akkor az elkészített termékobjektumot hozzáadjuk a listához. if (event.isEndElement()) { EndElement endElement = event.asEndElement(); if (endElement.getName().getLocalPart().equalsIgnoreCase("product")) { productList.add(product); } } } } } } } catch (...) { ... } return productList; }
124 Created by XMLmind XSL-FO Converter.
Adatkezelés
1.4.2. Validálás Az XML dokumentum validálásához itt példaként a javax.xml.validation.Validator osztályt használjuk. Ez az osztály azonban (bizonyos megkötések mellett) nemcsak StAX API-val használható, de akár SAX-szal és DOM-mal is képes együttműködni (ebben az értelemben pedig alternatívát biztosít a korábban már látott validációs lehetőségekhez). Legfontosabb metódusa a validate, amely a korábban a transzformációk során már említett javax.xml.transform csomag Source és Result interfészeit megvalósító egy-egy osztályt kap paraméteréül, és a forráson elvégezve a validációt, a célobjektumba helyezi az (esetlegesen kibővített) XML-t.
Megjegyzés A validate metódus megengedi, hogy – amennyiben a hívót nem érdekli az érvényesítés során esetlegesen kibővülő XML –, a Resultnull értékű legyen (az egyparaméteres változat meghívása ezzel egyenértékű). Ekkor bármilyen forrás ( SAXSource, DOMSource, StAXSource, StreamSource) megadható. Amennyiben azonban megadjuk a Result típusú paraméter értékét is, úgy teljesülnie kell annak a megkötésnek, hogy a Source és a Result tényleges típusának meg kell egyeznie! Vagyis ha a SourceDOMSource, akkor a Result csak DOMResult lehet, és így tovább. Amennyiben ez nem így van, a validate metódus ezt egy IllegalArgumentException típusú kivétel eldobásával jelzi. A Validator osztály setErrorHandler metódusával a érvényesítést végző objektumhoz társíthatunk egy hibakezelőt (ahogyan azt a SAX stílusú feldolgozásról szóló részben már bemutattuk). Abban az esetben, ha a validáció során hiba történik, a validate metódus a kapcsolt hibakezelő megfelelő metódusát végrehajtva tudatja ezt a kezelővel. XMLInputFactory inputFactory = XMLInputFactory.newInstance(); InputStream in = new FileInputStream(xmlFile); XMLEventReader eventReader = inputFactory.createXMLEventReader(in); SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema schema = factory.newSchema(new File("product.xsd")); Validator validator = schema.newValidator(); validator.validate(new StAXSource(eventReader)); //Ha eljutunk eddig a pontig és nem keletkezik kivétel, akkor az XML dokumentum érvényes. System.out.println("Az XML dokumentum érvényes.");
1.4.3. XML dokumentum létrehozása StAX API segítségével Az XML dokumentum StAX API alapján történő létrehozását szintén az eseményiterátor API-n keresztül mutatjuk be. Az alábbi példametódus (writeXmlWithStAX) egy termékeket tartalmazó lista tartalmát írja ki egy XML állományba. public void writeXmlWithStAX(File xmlFile, List productList) throws ... {
Az XML eseményíró objektumot az alapértelmezett gyár állítja elő. A createXMLEventWriter gyártó metódus paramétere az XML fájl kimeneti adatfolyam objektumként. Az ily módon létrehozott XMLEventWriter objektumot használjuk fel az események dokumentumba írására. Az eseményírón kívül egy XMLEventFactory objektumra is szükség van, amely az XML eseményeket állítja elő. Az XMLEventFactory objektum gyártó metódusai által létrehozott eseményeket kapja meg paraméterként az XMLEventWriter objektum hozzáfűző (add) metódusa. XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(new FileOutputStream(xmlFile)); XMLEventFactory eventFactory = XMLEventFactory.newInstance();
Whitespace-karaktert (újsort) reprezentáló esemény előállítása. XMLEvent newLine = eventFactory.createSpace("\n");
125 Created by XMLmind XSL-FO Converter.
Adatkezelés
A dokumentum kezdetét reprezentáló eseményobjektum létrehozása és kiírása. StartDocument startDocument = eventFactory.createStartDocument(); eventWriter.add(startDocument);
Egy-egy XML elem nyitócímkéjét a createStartElement metódus gyártja le. Mivel a példában nem használunk névtereket, a createStartElement metódus QName prefix és QName URI argumentuma is üressztring. StartElement startElement = eventFactory.createStartElement("", "", "products"); eventWriter.add(startElement); eventWriter.add(newLine); ...
A termékobjektumokat tartalmazó listán iterálunk végig, miközben a termékek egyes részeit, vagyis az adattagok értékét egyesével hozzáfűzzük a kiírandó elemekhez, azaz a kiírást végző XMLEventWriter objektumhoz. for (Product product : productList) { eventWriter.add(eventFactory.createStartElement("", "", "product")); String selectorValue = (product instanceof Book ? "Book" : "DVD");
Attribútum létrehozása és hozzáfűzése az aktuális eseményhez, most egy termék kezdőcímkéjéhez. eventWriter.add(eventFactory.createAttribute("selector", selectorValue)); ... eventWriter.add(newLine);
A termékobjektum adattagjait a product elem gyermekeiként írjuk ki, amelyhez a createNode metódust használjuk. for (Field productField : ...) { ... XmlElement xmlelem = productField.getAnnotation(XmlElement.class); productMethod = ... createNode(eventWriter, xmlelem.value(), "" + productMethod.invoke(product)); ... }
A createNode segédmetódus nem az API része, ezt mi magunk hoztuk létre: az egyes csomópontok létrehozására szolgál, amelyeket a paraméterként kapott eseményíró objektumhoz ad hozzá. A metódus második és harmadik argumentuma a kiírandó elem neve és értéke. private void createNode(XMLEventWriter eventWriter, String name, String value) throws XMLStreamException { XMLEventFactory eventFactory = XMLEventFactory.newInstance(); XMLEvent newLine = eventFactory.createDTD("\n"); XMLEvent tab = eventFactory.createDTD("\t"); StartElement startElement = eventFactory.createStartElement("", "", name); eventWriter.add(tab); eventWriter.add(startElement);
A paraméterként kapott XML elem értékét átkonvertáljuk Characters objektummá. Characters characters = eventFactory.createCharacters(value); eventWriter.add(characters);
Létrehozzuk a zárócímkét is. EndElement endElement = eventFactory.createEndElement("", "", name);
Miután az adott termék minden adattagját hozzáadtuk az eseményíróhoz, bezárjuk a terméket leíró elemet, vagyis az eseményíróhoz hozzáadunk egy product zárócímkét. eventWriter.add(endElement); eventWriter.add(newLine); }
126 Created by XMLmind XSL-FO Converter.
Adatkezelés
Miután az adott termék minden adattagját hozzáadtuk az eseményíróhoz, bezárjuk a termék elemet. Az eseményíróhoz hozzáadunk egy product záró tagot. eventWriter.add(eventFactory.createEndElement("", "", productTagName)); eventWriter.add(newLine); }
A dokumentum gyökérelemének zárócímkéjét is hozzáfűzzük az események láncához. eventWriter.add(eventFactory.createEndElement("", "", "products")); eventWriter.add(newLine);
Ezután következik a dokumentum végét reprezentáló objektum.. eventWriter.add(eventFactory.createEndDocument());
Az XML dokumentum tényleges kiírása a megadott fájlba az eseményíró obejktum close metódusának hívásakor történik eventWriter.close(); }
2. Adatbázis-kapcsolatok kezelése A Java nyelvben az adatbázis-kapcsolatok kezelésére a Java Database Connectivity (JDBC) API áll rendelkezésre. Ez egy olyan alkalmazásprogramozói felületet biztosít, amely táblázatos adatok elérésére és manipulálására szolgál, elsősorban olyan adatokéra, amelyek relációs vagy objektumrelációs adatbázis-kezelő rendszerben tárolódnak. Az alkalmazásprogramozói felület adatbáziskezelőrendszergyártó-független összeköttetést biztosít az adatok Java kódból történő elérésére és kezelésére. Minden gyártó saját implementációt ad a Java JDBC API-jához. A JDBC a következő lépésekhez nyújt támogatást: 1. Kapcsolódás adatforráshoz (például adatbázishoz). 2. Olvasási és írási (azaz lekérdezések és módosítási) műveletek végrehajtása az adatforráson. 3. Az adatforrástól visszakapott adatok feldolgozása. A JDBC négy részből épül fel: 1. A JDBC API biztosítja a relációs adatok elérését Java kódból. Az API segítségével SQL utasításokat hajthatunk végre, a kapott eredményeket feldolgozhatjuk, valamint módosításokat kezdeményezhetünk az adatforráson. Elosztott rendszerek esetében a kliens természetesen egyidejűleg több adatforrással is kapcsolatban állhat. A JDBC API a Java platform részeként két csomagból áll, a java.sql-ből és a javax.sqlből. Mindkét csomag része mind a Standard, mind pedig az Enterprise Editionnek.. 2. Egy meghajtókezelő osztály ( DriverManager) definiálja, hogy mely objektumok kapcsolhatják össze a Java alkalmazást a JDBC meghajtóval. A DriverManager osztály képezi a JDBC architektúra gerincét. A Java SE javax.naming és a javax.sql csomagjai teszik lehetővé teszik adatforrások (úgynevezett DataSource-ok) használatát, amelyek a Java név- és címtárszolgáltatásán (Java Naming and Directory Interface, JNDI) keresztül érhetők el. A meghajtókezelő felelőssége, hogy minden adatforráshoz a megfelelő meghajtót vegye használatba. A meghajtókezelő képes arra, hogy egy időben több meghajtót kezeljen, amelyek több (különböző) adatbázishoz kapcsolódnak. A meghajtó maga egy olyan adapter, amely a Java alkalmazás utasításait alakítja át az adatbázis-kezelő rendszer által értelmezhető formára. 3. Egy JDBC-driver tesztkészlet is rendelkezésre áll, amely a JDBC-meghajtóprogramok fejlesztői számára biztosítanak teszteseteket annak ellenőrzésére, hogy az általuk fejlesztett meghajtóprogramok megfelelnek-e a követelményeknek. 4. A JDBC-ODBC híd feladata a JDBC elérés biztosítása ODBC-meghajtókon keresztül. Ezt olyan esetekben érdemes használni, amelyekben nem jelent problémát az ODBC-meghajtó telepítése a kliensekre, valamint háromrétegű architektúra esetén, ha az alkalmazásszerver kódja Javában íródott. Ezzel a híddal 127 Created by XMLmind XSL-FO Converter.
Adatkezelés
tulajdonképpen olyan adatforrásokat is el lehet érni, amelyekhez nem készült JDBC-meghajtó, de van hozzájuk ODBC-driver.
6.8. ábra - Kapcsolódás különféle adatforrásokhoz
http://advancejava.wordpress.com/2012/12/23/jdbc-introduction/ E jegyzet írásakor a legfrissebb JDBC specifikáció a 4.1-es, amely a Java 7-be került be.
2.1. A JDBC felépítése A JDBC API mind a két-, mind a háromrétegű feldolgozási modellt támogatja.
6.9. ábra - Kétrétegű feldolgozási modell [JDBCTutorial]
A kétrétegű modellben az applet vagy alkalmazás közvetlenül az adatforrással kommunikál. A program kódja közvetlen eléréssel rendelkezik az adatforráshoz, és közvetlenül kapja vissza az eredményt. Az adatforrásnak nem szükséges ugyanazon a gépen lennie, mint az alkalmazásnak. Ekkor a két gép hálózaton keresztül kommunikál egymással (kliens-szerver architektúra).
6.10. ábra - Háromrétegű feldolgozási modell [JDBCTutorial]
128 Created by XMLmind XSL-FO Converter.
Adatkezelés
A háromrétegű modellben az utasítások nem közvetlenül az adatforráshoz futnak be, hanem egy köztes szolgáltatási réteghez (middleware-hez), amely továbbítja az utasítást az adatforráshoz. Az adatforrás sem közvetlenül az alkalmazásnak küldi vissza az eredményt, hanem a köztes szolgáltatási rétegnek, amely továbbítja azt az alkalmazás felé. Olyan esetekben érdemes háromrétegű modellt használni, amelyekben felügyelnünk kell az adatokhoz történő hozzáférést és módosítást. A háromrétegű modell a szerverre történő telepítést (deployolást) is egyszerűsítheti. A köztes réteg programozásában egyre nagyobb arányban van jele a Java. Ennek egyik oka az optimalizáló fordítók bevezetése, amelyek a Java bájtkódot gépközeli kódra fordítják, valamint új technológiák megjelenése (például Enterprise JavaBeanek). Így a JDBC használata is növekszik a köztes rétegben. A JDBC számos tulajdonsága jól használható a szerver oldalon, mint például a kapcsolatok gyorsítótárazása (connection pooling), elosztott tranzakciók és sorhalmazok (rowsetek) kezelése.
2.2. Meghajtóprogramok Egy JDBC-meghajtóprogram az a szoftverkomponens, amely lehetővé teszi, hogy egy Java alkalmazás kapcsolatot létesítsen egy adatbázissal. A meghajtóprogramoknak a JDBC specifikáció szerint négy típusa van: 1. JDBC–ODBC híd, 2. Natív–API meghajtó, 3. Hálózat–protokoll meghajtó, 4. Natív–protokoll meghajtó. Az egyes meghajtóprogram-típusokról bővebben a [https://jcp.org/en/jsr/detail?id=221] címen elérhető JDBC specifikációban lehet olvasni. Itt csak az úgynevezett 4-es típusú (Type 4) meghajtóra, vagyis a natív-protokoll meghajtóra térünk ki, mivel a Java alkalmazások túlnyomó része ezt alkalmazza. A 6.11. ábra - 4-es típusú JDBC-driver tisztán Javában íródott, és socketeken keresztül közvetlenül kommunikál az adatbázissal oly módon, hogy a JDBC-hívásokat gyártóspecifikus adatbázis-protokollnak megfelelő hívásokká alakítja. Előnyei közé tartozik a korábbi meghajtófajtákhoz képest értett teljesítménynövekedés (mivel itt nincs szükség sem köztes formátumra, sem köztes rétegre), valamint a könnyebb hibakeresés, hiszen az alkalmazás és az adatbázis közötti kapcsolat minden aspektusát teljes egészében a JVM kezeli. Hátránya, hogy a kliensoldalon minden adatbázis külön meghajtót igényel. Maga a natív-protokoll drivert tipikusan egy jar fájlba csomagolt komponensként szerezzük be, általában azon adatbázis-kezelő rendszer gyártójának honlapjáról, amelyhez csatlakozni szeretnénk. A kapcsolódáshoz szükséges, hogy az így beszerzett jar fájlokat a JVM osztálybetöltője megtalálja, vagyis olyan helyre kell tennünk, amely a CLASSPATH környezeti változó értékében felsorolásra került.
Tipp
129 Created by XMLmind XSL-FO Converter.
Adatkezelés
A jegyzet írásának idején a legelterjedtebb adatbázis-kezelő rendszerek közül néhánynak a JDBCmeghajtói az alábbi táblázatban megadott webcímen voltak elérhetőek:
6.3. táblázat - Adatbázis-kezelő rendszerek JDBC-drivereinek elérhetősége Adatbázis-kezelő rendszer
JDBC-meghajtó helye
Oracle Database
http://www.oracle.com/technetwork/database/features/jdbc/index091264.html
MySQL
http://dev.mysql.com/downloads/connector/j/
PostgreSQL
http://jdbc.postgresql.org/
IBM DB2
https://www-304.ibm.com/support/docview.wss?uid=swg21363866
MS SQL Server
http://msdn.microsoft.com/en-us/sqlserver/aa937724.aspx
SQLite
https://bitbucket.org/xerial/sqlite-jdbc
Az ábrán azt láthatjuk, hogy a 6.11. ábra - 4-es típusú JDBC-driver a hívó (az adatkezelést végezni kívánó) alkalmazás számára egy szabványos JDBC API-t biztosít, míg ez a kliens oldalon elhelyezett komponens socket alapú hálózat kommunikáció segítségével közvetlenül kommunikál majd az elérni kívánt adatbázis-kezelő rendszerrel.
6.11. ábra - 4-es típusú JDBC-driver
130 Created by XMLmind XSL-FO Converter.
Adatkezelés
http://en.wikipedia.org/wiki/JDBC_driver
2.3. A JDBC API főbb elemei Ebben a szakaszban a JDBC API főbb interfészeinek és osztályainak a rövid bemutatása történik meg.
6.12. ábra - A fő JDBC osztályok és interfészek és üzeneteik
131 Created by XMLmind XSL-FO Converter.
Adatkezelés
• DriverManager: Ez az osztály adatbázis-meghajtók kezelésére szolgál. A DriverManager egy Java alkalmazásból egy úgynevezett JDBC URL segítségével érkező kérésre az alprotokollon keresztül választja ki a megfelelő meghajtót a kapcsolat létrehozásához. Az adatbázis-kapcsolat az első olyan meghajtó segítségével jön létre, amely úgy nyilatkozik, hogy felismeri a jdbc protokoll alatt lévő alprotokollt.
Megjegyzés Egy JDBC URL általános szerkezete a következő: <protokoll>::
A protokoll értéke a JDBC-specifikáció szerint mindigjdbc.Az alprotokoll segítségével a meghajtó, illetve a kapcsolódási mechanizmus azonosítható, míg az alnév azon információkat tartalmazza, amelyek az adatbázis-hozzáférés kiépítéséhez szükségesek (gépnév, annak a portnak a száma, amelyen a kapcsolódásfigyelő várakozik a kapcsolatokra, stb.). Általában azt mondhatjuk, hogy az alprotokoll a DriverManager-nek szól, hogy ez alapján válassza ki a meghajtóprogramot, míg az alnév már a kiválasztott Driver-nek szól, hogy megtudja, milyen paraméterekkel építhati ki a kapcsolatot. Az alnév felépítése gyártófüggő. Példa néhány JDBC URL-re: • Oracle Database: • jdbc:oracle:thin:@servername.inf.unideb.hu:1521:ORCL Itt az oracle:thin rész az alprotokoll, ez azonosítja, hogy az Oracle gyártó úgynevezett vékony (thin) meghajtóprogramjára van szükség, amely az Oracle 4-es típusú JDBCmeghajtóprogramja. Az URL további része a szükséges kapcsolódási adatokat – szervernév, port és példányazonosító (SID) –tartalmazza (a felhasználói név és a jelszó nélkül, habár az is belekódolható az URL-be, például a scott nevű, tiger jelszavú felhasználó esetén: jdbc:oracle:thin:scott/[email protected] :1521:ORCL). • jdbc:oracle:oci8:@TEST
132 Created by XMLmind XSL-FO Converter.
Adatkezelés
Ebben az esetben az alprotokoll az oracle:oci8, amely az Oracle 2-es típusú JDBC-driverét azonosítja. Ennek használatához az Oracle Client szoftver kliensoldali telepítésére is szükség van, és ekkor a kapcsolódási információként az alnévben megadott TEST azonosító a vastag kliens tnsnames.ora állományának egyik bejegyzését azonosítja (ez a bejegyzés részletezi a kapcsolódási adatokat). • MySQL: jdbc:mysql://servername.inf.unideb.hu:3306/dbname Az alprotokoll a mysql, az alnév pedig a szervernevet, a portot és annak az adatbázisnak a nevét tartalmazza, amelyhez kapcsolódni szeretnénk. • PostgreSQL: jdbc:postgresql://servername.inf.unideb.hu:5432/dbname Ugyanaz, mint a MySQL esetén, leszámítva, hogy az alprotokoll neve mysql helyett postgresql. • IBM DB2: • jdbc:db2://servername.inf.unideb.hu:50000/SAMPLE A DB2 4-es típusú drivere esetén. • jdbc:db2:sample A DB2 2-es típusú drivere esetén. • MS
SQL
Server:
jdbc:sqlserver://localhost:1433;databaseName=AdventureWorks;integratedSecurit y=true;
Az alprotokoll az sqlserver névre hallgat. • SQLite: jdbc:sqlite:///COMPUTERNAME/shareA/dirB/dbfile • Driver: Ez az interfész az adatbázisszerverrel történő kommunikáció kezeléséért felelős. Általában nem közvetlenül használjuk az implementáló osztályokat: ez a felelősség a fent említett DriverManager osztályé. • Connection: Ez az interfész az adatbázissal kiépített kapcsolatot reprezentálja; egy kommunikációs környezetet biztosít. • Statement, PreparedStatement, CallableStatement: Olyan interfészek, amelyek egy-egy végrehajtandó SQL-utasítás absztrakciói (utasításobjektumok). A felírás sorrendjében szülő-gyermek viszonyban állnak egymással. A PreparedStatement interfész segítségével paraméterezhető SQL-utasítások létrehozását, míg a CallableStatement segítségével adatbázisban tárolt alprogramok hívását (amelyeknek természetesen paraméterek is átadhatók) végezhetjük. • ResultSet: Ez az interfész tartalmazza az utasításobjektumok segítségével végrehajtott lekérdezés eredményrelációját (vagyis a lekérdezés eredményhalmazának absztrakciójáról van szó), amelynek tetszőleges számú sora (nulla, egy vagy több) lehet, attól függően, hogy a lekérdezés feltételeinek hány rekord felelt meg. A ResultSet objektum iterátorként viselkedik az eredményhalmaz sorainak bejárásakor. Egy időben csak egy sort dolgozhatunk fel, amelyre egy kurzor mutat. A kurzor egy olyan mutató, amely az aktuális sort azonosítja. Közvetlenül a lekérdezés végrehajtását követően a létrejövő kurzor az első sor elé mutat, függetlenül a eredményreláció számosságától. Az eredményhalmazt az elejétől a végéig bejárhatjuk a kurzor segítségével, vagyis az API metódust biztosít a kurzor léptetésére. Egy eredményhalmaz alaphelyzetben egyszer, egyirányban, szekvenciális módon járható be, azonban már a JDBC 2 óta van mód ennek az alapértelmezésnek a felülírására, vagyis lehetőség van egy eredményhalmaz nem csak az elejétől a vége felé járhatunk be, hanem fordítva is, a végéről az eleje felé haladva, illetve tetszőlegesen is pozícionálhatunk az ereményhalmaz rekordjai között. • SQLException: Az adatbázis-műveletek végrehajtása során bekövetkező hibákat reprezentáló kivételosztály. Implementálja az Iterable interfészt, amely lehetővé teszi, hogy az adatbázis oldalon létrejövő kivételek közvetlen és közvetett kiváltó kivételeit (okait) elérjük. 133 Created by XMLmind XSL-FO Converter.
Adatkezelés
• DatabaseMetaData: Az adatbázis-kezelő rendszerek nemcsak a felhasználók adatait tárolják, de többek között információt nyújtanak az adatok szerkezetéről, tárolási és hozzáférési módjáról is. Ez utóbbi adatok nevezzük metaadatoknak (adatokról szóló adatok). A metaadatbázisban (vagy katalógusban) tárolt információk, azok szervezési és kinyerési módja adatbázis-kezelőről adatbázis-kezelőre változhat. Éppen ezért a JDBC biztosítja a DatabaseMetaData interfészt, amelyet a meghajtó írójának (vagyis az adatbáziskezelő gyártójának) kötelezően implementálnia kell, így szabványos módon férhetünk hozzá az adatbázis metaadataihoz. • ResultSetMetaData: Nem minden esetben ismerjük a program írásakor, hogy az egyes SELECT-utasítások által visszaadott eredményhalmazok milyen szerkezetűek (gondoljunk különösen a SELECT * FROM táblanév alakú lekérdezésekre, amely eredményhalmazának a szerkezete az oszlopok létrehozását illetve törlését végző DDL-utasítások hatására is megváltozhat). A ResultSetMetaData interfész segítségével a lekérdezés eredményének többek között olyan metaadataihoz férhetünk hozzá, mint amilyen az eredményhalmaz oszlopainak száma és az egyes oszlopok neve és típusa. A leggyakoribb műveletek a JDBC API használatakor természetesen DML utasítások, vagyis a SELECT, az INSERT, a DELETE, és az UPDATE, de lehetőség van DDL-utasítások (például CREATE TABLE, DROP TABLE és ALTER TABLE) végrehajtására is. A főbb típusokat (az SQLException kivételével, amelyet tulajdonképpen bármelyik típus metódusai kiválthatnak) a közöttük lévő viszonyokkal az alábbi osztálydiagram szemlélteti:
6.13. ábra - A java.sql csomag főbb típusai a közöttük lévő kapcsolatokkal
2.4. A JDBC API használata A JDBC API használatához legalább Java 6-os verzió (vagyis JDBC 4) esetén az alábbi ábrán látható öt lépést kell végrehajtanunk.
6.14. ábra - A JDBC API főbb interfészei és használatának lépései
http://www3.ntu.edu.sg/home/ehchua/programming/java/JDBC_Basic.html 134 Created by XMLmind XSL-FO Converter.
Adatkezelés
Tekintsünk egy egyszerű példát, amelyen keresztül ezen lépések bemutathatók!
6.1. példa - Címek adatainak adatbázisból Address-listába olvasása 1 import hu.unideb.inf.progtech.booketdvdstore.entity.Address; import java.sql.Connection; import java.sql.DriverManager; 5 import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; 10 public class SampleJdbc { public List listAddresses() { List resultList = new ArrayList<>(); final String query = "SELECT * FROM ADDRESSES"; 15 try (Connection conn = DriverManager.getConnection( 1 "jdbc:oracle:thin:@servername.inf.unideb.hu:1521:ORCL", "username", "password"); Statement stmt = conn.createStatement(); 2 20 ResultSet rs = stmt.executeQuery(query) 3 ) { 4 Address address = null; while (rs.next() 5 ) { address = new Address(); address.setId(rs.getInt("ADDRESS_ID") 5 ); 25 address.setCountry(rs.getString("COUNTRY") 5 ); address.setCity(rs.getString("CITY") 5 ); address.setZipCode(rs.getString("ZIP_CODE") 5 ); address.setStreet(rs.getString("STREET") 5 ); address.setHouseNumber(rs.getString("HOUSE_NUMBER") 5 ); 30 resultList.add(address); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); 35 } return resultList; } } 1
A kapcsolat kiépítésének első lépéseként a DriverManager osztály statikus getConnection metódusát kell meghívni. Ennek kell paraméterként átadni a JDBC URL-t, amely alapján a DriverManager kiválasztja, és szükség esetén betölti és példányosítja a megfelelő implementáció Driver interfészt megvalósító osztályát. A rendelkezésre álló meghajtóprogramok listáját a jdbc.drivers rendszertulajdonságból olvassa ki. Ez a Driver objektum ezek után a JDBC URL-ben (és a getConnection metódusban) megadott adatok (az alnév, illetve az azonosításra szolgáló adatok) segítsgével kiépíti a socket alapú kapcsolatot a távoli adatbázisszerverrel, a mindkét fél által ismert sepeciális protokollnak megfelelően. Ha a kapcsolat kiépült, akkor létrejön egy, a Connection interfészt implementáló osztály egy objektuma, ez lesz az, amivel a DriverManager.getConnection visszatér.
Fontos A régebbi (4.0-ást megelőző) JDBC verziókban ezt a lépést megelőzte egy meghajtóprogramregisztrációs folyamat (amelyet a fenti ábra 0. lépése szemléltet), amelyre – mivel a DriverManager osztály betöltésekor a rendelkezésre álló (a jdbc.drivers rendszertulajdonságban megadott) meghajtóprogramok adatai betöltődnek – ma már csak abban a nagyon ritka esetben lehet szükség, ha a betöltendő meghajtóprogram a DriverManager osztálybetöltésekor még nem állt rendelkezésre. Szükség esetén ezt a DriverManager osztály statikus registerDriver metódusával tehetjük meg. Alternatív megoldásként a JDK-val kompatibilis virtuális gépeken a JDBC-megvalósítás Driver interfészt implementáló osztályának dinamikus (reflection
135 Created by XMLmind XSL-FO Converter.
Adatkezelés
segítségével történő) betöltése is ellátta ezt a feladatot. Példa ezekre az Oracle megvalósítása esetén: DriverManager.registerDriver (new oracle.jdbc.OracleDriver()); Class.forName("oracle.jdbc.OracleDriver");
2
3
4
5
Mivel a 4.0-ás JDBC a Java 6-ban jelent meg először, ez azt jelenti, hogy ezt a tevékenységet (a fentiek két utasítás közül csak az egyiket!) csak az ennél régebbi Java verziók esetén kell elvégezni. A második lépésben egy SQL-utasítást reprezentáló Statement objektum létrehozására van szükség. Itt természetesen a helyettesíthetőség elve alapján PreparedStatement vagy CallableStatement is szóba jöhet. A harmadik lépésben az utasítás végrehajtása történik meg. Erre a Statement interfész execute metóduscsaládja (az executeQuery, az executeUpdate, illetve az execute) szolgál, attól függően, hogy a végrehajtadó utasítás SELECT vagy egyéb utasítás-e. A következő tevékenység az utasítás végrehajtási eredményének a feldolgozása. Ez nem SELECT utasítás esetén egyszerűen megy, hiszen az executeUpdate-től visszakapott érték mindössze egy egész érték, azonban lekérdezések végrehajtását követően az eredményhalmaz ( ResultSet) objektum feldolgozására van szükség. Végezetül pedig, ha már nincs szükségünk valamely erőforrásra – például ResultSet-re Statement-re vagy akár Connection-re –, azt a close metódus meghívásával be kell zárni. Ez azonban a Java 7-ben bevezetett AutoCloseable interfész és a try-with-resources utasítás segítségével automatizálható is, mivel az említett interfészek mind az AutoCloseable leszármazottai.
2.4.1. Kapcsolat létrehozása A használni kívánt adatforrással létesítendő kapcsolat kiépítése kétféleképpen történhet: a DriverManager osztály vagy a DataSource interfész segítségével. A DriverManager osztály használata elsősorban Java SE környezetben ajánlott. Mivel a DriverManager segítségével végzett kapcsolatkialakítást a fentiekben már bemutattuk, ezért itt már csak a DataSource interfész segítségével kiépítésre kerülő kapcsolatok bemutatása kerül sorra. A DataSource interfész segítségével az adatforrás használata jobban optimalizálható, mint a DriverManager osztály esetében. Egy DataSource objektum lehetővé teszi a kapcsolat a fizikai kapcsolat gyorsítótárazását és ezáltal újrafelhasználhatóságát (connection pooling), valamint elosztott tranzakciók kezelését is, amely vállalati környezetben és Enterprise JavaBeanek (EJB-k) használatakor alapvető fontosságú. DataSource objektum nem csak adatbázis-kezelő rendszert reprezentálhat, hanem egyéb adatforrást is, például egy állományt. Egy DataSource példány létrehozásához névszolgáltatást veszünk igénybe, amely a Java név- és címtárszolgáltatásának, a Java Naming and Directory Interface-nek (a JNDI-nek) az API-ját használja. A létrehozást követően adhatók meg az adatforrás jellemzői.
6.15. ábra - Kapcsolat létrehozása DataSource segítségével
A DataSource további előnye a DriverManager-rel szemben, hogy az adatforrás URL-jét nem szükséges fixen kódolnunk, így alkalmazásunk még inkább hordozhatóvá válik. Ezenkívül a DataSource rugalmas is, mert változás esetén elég csak a tulajdonságait lecserélni, és nincs szükség a változás teljes alkalmazáson keresztül történő végigvezetésére. Például, ha az adatforrást egy másik szerverre költöztetnek, elég csak a serverName tulajdonságot átírni. 136 Created by XMLmind XSL-FO Converter.
Adatkezelés
6.16. ábra - Connection pooling
Ha szeretnénk a kapcsolatot újrafelhasználni, a DataSource interfész ConnectionPoolDataSource alinterfészével dolgozhatunk. Elosztott tranzakciók esetén az XADataSource interfész által biztosított kapcsolatok használata javasolt.
6.17. ábra - Elosztott tranzakciók támogatása
137 Created by XMLmind XSL-FO Converter.
Adatkezelés
A DataSource interfészt – bár a Java SE-nek is része – elsősorban Java EE környezetben alkalmazzuk.
2.4.2. SQL-utasítások létrehozása Az SQL-utasításokat reprezentáló Statement objektumok létrehozásához szükségünk van egy kapcsolatobjektumra, vagyis egy utasítást csakis egy kapcsolathoz kötődően lehet létrehozni (egész pontosan a kapcsolatobjektum hozza létre az utasításobjektumot). Az utasítások háromfélék lehetnek: • Az egyszerű Statement, amellyel egyszerű SQL-utasításokat hajthatunk végre, olyanokat, amelyek teljes szövege ismert, • A PreparedStatement, amely az előfordított SQL-utasítások absztrakciója, a Statement típust altípusa. Az ilyen utasítások paraméterekkel is rendelkezhetnek, amelyekhez nem a fordításkor, hanem csak közvetlenül a végrehajtás előtt rendelődik érték, • A CallableStatement, amely a tárolt programegységek végrehajtására szolgál. A PreparedStatement típus altípusaként a CallableStatement is rendelkezik paraméterekkel, mi több nem csak bemenő, hanem akár kimenő paraméterekkel is (például egy tárolt függvény visszatérési értéke vagy egy PL/SQL tárolt eljárás OUT módú paramétere esetén). 2.4.2.1. Statement Az SQL-utasítások létrehozásának legegyszerűbb módja a Statement típus használata. final String query = "SELECT * FROM ADDRESSES"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query);
Fontos Az SQL-utasítást tartalmazó sztring végén nincs utasítás lezáró pontosvessző! Futásidejű kivétel váltódik ki, ha pontosvesszőt írunk a sztring végére.
Megjegyzés 138 Created by XMLmind XSL-FO Converter.
Adatkezelés
Az SQL-utasítások nem érzékenyek a kis- és nagybetűkre. Ez alól természetesen kivételt képeznek a karakter- és sztringkonstansok. 2.4.2.2. PreparedStatement A PreparedStatement típust akkor célszerű használni, ha ugyanazt az SQL-utasítást többször is végre szeretnénk hajtani, vagy ha az SQL-utasítás szövege (különösképpen a WHERE feltételé) dinamikusan áll elő. Előbbire példa, ha egy beszúró (INSERT) vagy módosító (UPDATE) utasítás esetén az SQL-utasítás törzse ugyanaz, csak a beszúrandó vagy módosítandó értékek változhatnak.(Hasonló helyzet persze a DELETE utasítással is előfordulhat, ha más-más értékkel megegyező attribútumértékű rekordok törlésére volna szükség.) Ilyen esetekben gyakran egy ciklusban adjuk meg az értékeket, mint a PreparedStatement argumentumait, amelyek iterációról iterációra változhatnak. A második eset akkor fordulhat elő, ha egy Statement objektumot az alábbi módon próbálunk összeállítani: String custID = ... // felhasználói felületről érkező érték Statement stmt = conn.createStatement(); ResultSet rset = stmt.excuteQuery("SELECT * FROM customers WHERE customer_id = '" + custID + "')");
Megjegyzés Az SQL nyelv sztringkonstansait aposztrófok határolják, nem pedig idézőjelek, mint a Java nyelv esetén. Ekkor – különösen, ha a custID értéke felhasználói bemenetről való – könnyedén SQL-befecskendezéses támadás áldozataivá válhatunk, hiszen ehhez elegendő csak a custID változónak a "' OR 1=1--", vagy a "'-" értéket adni annak érdekében, hogy a végrehajtandó lekérdezés egyetlen rekord (az adott azonosítóval rendelkező ügyfél) helyett vagy az össszes ügyféladatot visszaadja, vagy nem ad vissza egyet sem, hiszen az aposztróf lezárja a WHERE customer_id = résznél nyitott aposztrófot. A -- az SQL-ben a sorközi megjegyzésekre való, vagyis a -- jelsorozattól a sor végéig tart a megjegyzés. így a custID változó értéke után konkatenált sztring elemei már sem nem osztoznak, sem nem szoroznak. Így a végrehajtandó utasítás szövege rendre így nézne ki: • SELECT * FROM customers WHERE customer_id = '' OR 1=1 Itt az '' (két aposztróf) az üressztringet jelzi. A hozzávagyolt 1=1 feltételrész miatt azonban ez a feltétel mindig igaz lesz, ezért az összes ügyfél adata kiszivároghat. • SELECT * FROM customers WHERE customer_id = '' Ez esetben pedig az üressztring azonosítójú ügyfelet keresnénk, ez feltehetőleg egyetlen ügyfél esetén sem lesz igaz.
Megjegyzés Az SQL-befecskendezéses (SQL injection) támadások olyan kódinjekción alapuló technikák, amelyekkel elsősorban adatközpontú alkalmazásokat támadnak meg. A technika lényege, hogy valamilyen speciálisan előkészített sztringgel rábírják az alkalmazást, hogy az eredetileg szándékolttól eltérő SQL-utasítást futtasson le.
6.18. ábra - Exploits of a mom [http://xkcd.com/327/]
139 Created by XMLmind XSL-FO Converter.
Adatkezelés
A PreparedStatement előnye a Statement-tel szemben, hogy már létrehozásakor megkapja az SQL-utasítást, amely azonnal továbbítódik az adatbázis-kezelő rendszerhez, amely lefordítja azt. Így az utasítás végrehajtása során az adatbázis-kezelőnek nem kell újra és újra elvégezni a nagyon hasonló szövegű (egymástól csak paraméterértékekben eltérő) utasítások értelmezését és az az alapján történő végrehajtásiterv-készítést, mivel mindezek már rendelkezésre állnak Segítségükkel a fenti SQL-befecskendezéses támadások ellen is védekezünk, hiszen az utasítás szövege nem ellenőrizetlen sztringkonkatenáció eredményeként áll elő, hanem egy úgynevezett kötési fázis (binding).során rögzül, amely nagyobb kontrollt biztosít az utasítások paraméterei fölött. A PreparedStatement objektumok argumentumok nélkül is használhatók (ekkor gyakorlatilag egyenértékűek a Statement-tel), jóllehet elsősorban felparaméterezett SQL-utasítások végrehajtására szolgálnak. A JDBCspecifikációnak megfelelően a paramétereket kérdőjelek (?) segítségével jelöljük. A fenti példa helyett a helyes megvalósítás valami ilyesmi lenne:
6.2. példa - Az SQL-befecskendezéses támadás kivédése String custID = ... // felhasználói felületről érkező érték try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM customers WHERE customer_id = ?")) { 1 stmt.setString(1, custID); 2 ResultSet rset = stmt.excuteQuery(); } catch (SQLException e) { .... } 1 2
A kérdőjelek az SQL-utasítás paramétereit jelölik. A paraméter csak literál lehet, vagyis sem az SQLutasítás kulcsszavaival, sem a tábla- és oszlopnevekkel nem lehet paraméterezni. A sor jelentése: az első, kérdőjellel jelölt paraméter értékeként beállítjuk a custID változó értékét.
Megjegyzés Ha megvizsgáljuk, hogy a fentebb SQL befecskendezéses támadást eredményező kód hogyan fest most, ha PreparedStatement-et használunk, azt tapasztaljuk, hogy az ily módon összeállított, és ténylegesen lefuttatandó utasítás szövege az alábbi: SELECT * FROM customers WHERE customer_id = ''' OR 1=1--'
Így a támadás nem sikerül, hiszen ez a lekérdezés azon ügyfél adatait adná vissza, akinek az azonosítója aposztróf-szóköz-nagy O-nagy R-szóköz-egy-egyenlő-egy-mínusz-mínusz. Figyelem! A - természetesen sztringkonstans belsejében nem bír extra jelentéssel (sorvégig tartó megjegyzés kezdete), a sztringkonstans belsejében pedig egy aposztróf megjelenítéséhez kettőt kell írnunk. A következő példa azt mutatja be, hogy egy utasítás több paraméterrel is rendelkezhet. Itt azzal a feltételezéssel élünk, hogy a conn nevű, Connection típusú objektum látható a metódusból. A kérdőjellel jelölt paraméterek sorszámozása 1-től indul. public void updateAddresses(List addrList) { final String update = "UPDATE ADDRESSES SET COUNTRY = ?, CITY = ?, STREET = ?, HOUSE_NUMBER = ? WHERE ADDRESS_ID = ?"; try (PreparedStatement pstmt = conn.prepareStatement(update)) { conn.setAutoCommit(false); for (Address addr : addrList) {
140 Created by XMLmind XSL-FO Converter.
Adatkezelés
pstmt.setString(1, addr.getCountry()); pstmt.setString(2, addr.getCity()); pstmt.setString(3, addr.getStreet()); pstmt.setString(4, addr.getHouseNumber()); pstmt.setInt(5, addr.getId()); pstmt.executeUpdate(); } conn.commit(); } catch (SQLException e ) { // a kivételkezelőt nem részletezzük } } PreparedStatement objektum létrehozása. Egy PreparedStatement objektum létrehozásakor olyan SQL-
utasítást adunk át, amely kérdőjeleket tartalmazhat. Ezek a paramétereket helyettesítik. final String update = "UPDATE ADDRESSES SET COUNTRY = ?, CITY = ?, STREET = ?, HOUSE_NUMBER = ? WHERE ADDRESS_ID = ?"; PreparedStatement pstmt = conn.prepareStatement(update);
Paraméterek átadása PreparedStatement objektumnak. A kód további részében, külön utasításokban adhatunk a paramétereknek aktuális értéket. Az értékadást az SQL-utasítás végrehajtása előtt el kell végeznünk. Erre a PreparedStatement típus setXXX metódusai használhatóak, ahol az XXX egy típusnevet (például String vagy Int) jelöl. pstmt.setString(1, addr.getCountry()); pstmt.setString(2, addr.getCity()); pstmt.setString(3, addr.getStreet()); pstmt.setString(4, addr.getHouseNumber()); pstmt.setInt(5, addr.getId());
A setXXX metódusok első paramétere egy sorszám, amely az SQL-utasításban szereplő adott helyettesítőt (kérdőjelet) azonosítja, a második pedig maga a paraméterhez rendelendő érték. A sorszámozás, mint az SQLutasításokban általában, 1-től indul, nem 0-tól. Az értéknek a setXXX metódus típusnevének megfelelő típusú objektumnak kell lennie. A paraméter értéke addig őrződik meg, amíg le nem cseréljük egy másik értékkel (amelyre a következő kódrészlet mutat példát), vagy meg nem hívjuk a PreparedStatement.clearParameters metódust, amely törli az addig beállított paraméterek értékét. pstmt.setString(1, addr1.getCountry()); pstmt.setString(1, addr2.getCountry()); PreparedStatement objektumok végrehajtása. A Statement objektumok végrehajtásához hasonlóan egy execute utasítást kell meghívnunk az SQL-utasítás futtatásához:
• az executeQuery metódus egyetlen ResultSet objektummal tér vissza (SELECT utasítások esetében használhatjuk); • az executeUpdate egy egész értékkel tér vissza, amely az SQL-utasítás által érintett sorok számát tartalmazza (INSERT, DELETE és UPDATE utasítások esetében), vagy 0-t, ha nem volt érintett sor, illetve ha az SQL-utasítás DDL-utasítás (például CREATE TABLE); • az execute metódus több ResultSet-et eredményező SQL-utasítás végrehajtásakor (amennyiben a driver enged ilyet), vagy olyankor használatos, ha nem tudjuk fordítási időben meghatározni, milyen jellegű utasítás végrehajtása történik meg. Ekkor az execute logikai visszatérési értéke adja meg e kérdésre a választ: true-t ad vissza, ha lekérdezés volt a művelet (ekkor az eredményhalmazhoz a getResultSet metódussal férünk hozzá, illetve false-t, ha egyéb művelet (ekkor a getUpdateCount-tal kérhetjük le az érintett sorok számát). for (Address addr : addrList) { ... int affectedRowsCount = pstmt.executeUpdate(); ... }
A Statement-tel szemben a PreparedStatementexecute metódusai nem vesznek át paramétert, mivel az SQL-utasítás már az objektum létrehozásakor átadódik. Módosító SQL-utasítások esetén célszerű az automatikus véglegesítést kikapcsolni a kapcsolatobjektum setAutoCommit metódusa segítségével, és a 141 Created by XMLmind XSL-FO Converter.
Adatkezelés
véglegesítést kézzel elvégezni (a kapcsolatobjektum commit metódusával). Java-kódból így biztosítható az adatok integritásának és az adatbázis konzisztenciájának megőrzése. conn.setAutoCommit(false); pstmt = conn.prepareStatement(update); for (Address addr : addrList) { ... pstmt.executeUpdate(); } conn.commit();
A véglegesítésről és visszagörgetésről a 2.4.3. szakasz - Tranzakciók kezelése című alfejezetben található további információ. 2.4.2.3. CallableStatement A CallableStatement objektumok tárolt alprogramok hívására szolgálnak. Tárolt alprogramnak nevezzük a logikailag együvé tartozó, egy bizonyos feladat elvégzésére szolgáló SQL-utasítások csoportját, amelyek lekérdezéseket vagy egyéb műveleteket tartalmaznak, amelyek az adatbázisszerveren hajtódnak végre és tárolódnak. A tárolt eljárások paramétereket vehetnek át és adhatnak vissza, amelyeknek három típusát különböztetjük meg: IN (alapértelmezés), OUT és INOUT típusú paramétereket. A következő táblázat a paraméterátadási módokat tartalmazza.
6.4. táblázat - Tárolt alprogramok paraméterátadási módjai IN
OUT
A tárolt alprogram Nem, mert definíciójában meg kell-e alapértelmezés adnunk? A kommunikáció iránya
A formális viselkedése
ez
Igen
A hívótól a hívott A hívott alprogram küld a Mindkét irányú alprogram irányában van hívó számára adatot kommunikácó. A hívó adatmozgás értéket ad át a hívott tárolt alprogramnak, amely a feldolgozás után egy megváltoztatott értéket visszaad.
paraméter Konstansként viselkedik
A formális paraméter Nem kaphat-e értéket?
Az aktuális paraméter
az Igen
INOUT
Inicializálatlan változóként Kezdőértékkel ellátott viselkedik változóként viselkedik Igen, hiszen ez által fejti ki Igen, de nem kötelező hatását. Kötelezően értéket értéket kapnia. (A kell kapnia. legtöbbször persze kap, mert ha sosem kapna, akkor helyettesíthetnénk egy IN módú paraméterrel.)
Konstans, változó, literál Változó lehet vagy kifejezés lehet
Változó lehet
2.4.2.3.1. A CallableStatement használata Mivel a CallableStatement a PreparedStatement típus leszármazottja, ezért példányai paraméterezhetőek. A kapcsolatobjektum prepareCall metódusának segítségével áll elő egy CallableStatement. Ennek paramétere egy hívásspecifikáció, amely az úgynevezett SQL92 escape szintakszis értelmében a következő formájú lehet (a példák minden esetben kétparaméteres alprogramokra vonatkoznak, de értelemszerűen ennél kevesebb illetve több paraméterrel rendelkező esetén is hasonló a helyzet): • Tárolt függvények meghívása esetén {? = call func(?, ?)} • Tárolt eljárások esetén {call func(?, ?)}
142 Created by XMLmind XSL-FO Converter.
Adatkezelés
Az SQL92 escape szintakszis lehetővé teszi, hogy a tárolt alprogramokat gyártófüggetlen módon tudjuk meghívni. Minde meghajtóprogramnak értelmeznie kell tudni a fenti szintakszissal leírt alprogramhívásokat, és át kell tudniuk fordítani azokat a saját protokolljuknak megfelelő hívássá.
Megjegyzés Az Oracle JDBC-driverek például a fenti hívásspecifikációkat rendre az alábbi natív hívásokra fordítják: • begin ? := func (?,?); end; • begin proc (?,?); end; Ezeket is megadhatjuk a prepareCall paramétereként, de ebben az esetben meghajtóprogramspecifikus kódot készítünk, amely nem lesz hordozható. A bemenő (vagyis IN és INOUT módú) paraméterek átadása ugyanúgy történik, mint a PreparedStatement esetében. A kimenő paraméterekre (ide az OUT, INOUT módú paraméterek, és a függvények visszatérési értékei tartoznak) a registerOutParameter metódust kell meghívnunk, amelynak első paramétere a helyettesítő (kérdőjel) sorszáma, a második paramétere egy SQL-típus. A függvény visszatérési értéke ugyanúgy kezelendő, mint egy bármilyen kimenő paraméter. INOUT paraméterek esetében egyrészt a registerOutParameter metódust kell meghívnunk, másrészt a PreparedStatement-től örökölt, megfelelő setXXX metódust. A kimenő paraméterek értékét a tárolt alprogram végrehajtása után a CallableStatement interfész megfelelő getXXX metódusával kérdezhetjük le. CallableStatement végrehajtására a Statement-től örökölt execute, executeUpdate vagy executeQuery metódusokat használhatjuk. Az executeUpdate-et akkor hívjuk, ha nem jön vissza eredményhalmaz, egyébként az executeQuery-t. Mindazonáltal, ha nem vagyunk biztosak a visszaadott ResultSet objektumok számát illetően, az execute metódus alkalmazása javasolt (ez JDBC-driver függő is, hiszen például az Oracle JDBC-driverei nem támogatják több ResultSet objektum egyidejű visszaadását).
A
6.3. példa - Paraméter néküli tárolt függvény meghívása CallableStatement cstmt = conn.prepareCall("{? = call SHOW_NO_OF_PERSONS}"); cstmt.registerOutParameter(1, Types.INTEGER); ResultSet rs = cstmt.executeUpdate(); while (rs.next()) { int id = rs.getInt(1); // kapott érték feldolgozása }
6.4. példa - Egy IN és egy OUT paraméterrel rendelkező kétparaméteres tárolt eljárás meghívása cstmt = conn.prepareCall("{call GET_CUSTOMER_OF_ORDER(?, ?)}"); cstmt.setInt(1, orderId); cstmt.registerOutParameter(2, Types.VARCHAR); cstmt.executeQuery(); String name = cstmt.getString(2);
6.5. példa - Két IN és egy INOUT paraméterrel rendelkező tárolt eljárás meghívása cstmt = conn.prepareCall("{call CALC_TOTAL_DISCOUNTS(?,?,?)}"); cstmt.setInt(1, productId); cstmt.setFloat(2, maxPercentage); cstmt.registerOutParameter(3, Types.NUMERIC); cstmt.setFloat(3, newPrice); cstmt.execute();
2.4.3. Tranzakciók kezelése Az adatbázisban tárolt adatok manipulálása során előfordulnak olyan helyzetek, amikor nem szeretnénk, ha egy SQL-utasítás hatása véglegesítésre kerülne, amíg további utasítások végrehajtása meg nem történik. Ezek olyan, 143 Created by XMLmind XSL-FO Converter.
Adatkezelés
logikailag összetartozó, de több kisebb utasításra széttördelt utasítások, amelyek együttes hatását akarjuk véglegesíteni, vagy egyiket sem közülük. Ezek az összetartozó, egy egységként végrehajtandó utasítások képeznek egy tranzakciót. Egy tranzakciónak vagy minden utasítása véglegesítésre kerül, vagy egyikük sem. 2.4.3.1. Az automatikus véglegesítés letiltása A kapcsolatobjektum létrehozásakor alapértelmezés szerint úgynevezett autocommit mód van érvényben, amely azt jelenti, hogy minden egyes utasítás különálló tranzakcióban fut, és ha sikeres, akkor véglegesítésre kerül. Ez csak nagyon triviális adatkezelési esetben lesz megfelelő számunkra. Nyilván nem alkalmazható ez a megközelítés olyan komplex(ebb) esetekben, amikor például egy rendelést leíró objektum véglegesítését csak akkor volna szabad elvégezni, ha az összes, hozzá tartozó rendelésitétel-objektum is sikeresen az adatbázisba került. Éppen ezért a legtöbb esetben szükségünk lesz arra, hogy az alapértelmezett autocommit módot felülírjuk. Ezt a kapcsolatobjektumra meghívott setAutoCommit(false); metódushívással tehetjük meg. Ezzel letiltjuk a minden SQL-utasítás végrehajtása után történő véglegesítést, így a tranzakciók véglegesítése majd igény szerint fog megtörténni, vagyis explicit módon kell majd meghívnunk a kapcsolatobjektum tranzakcióvezérlő utasításokat reprezentáló metódusait. A véglegesítésre a Connection objektum commit metódusa, a visszagörgetésre pedig a rollback metódus szolgál. public void updateProduct(List prodList) { final String updateProduct = "UPDATE PRODUCTS SET PRICE = ?, DISCOUNTS = ?, INVENTORY = ?," + " TITLE = ?, PUBLISHER = ?, LENGTH = ? WHERE PRODUCT_ID = ?"; final String updateProductPerson = "UPDATE PRODUCTPERSON SET PERSON = ? WHERE PRODUCT = ? AND INVOLVEMENT = ?"; if (conn.getAutoCommit()) conn.setAutoCommit(false); try (PreparedStatement pstmtUpdProd = conn.prepareStatement(updateProduct); PreparedStatement pstmtUpdProdPers = conn.prepareStatement(updateProductPerson)) { for(Product prod : prodList) { pstmtUpdProd.setDouble(1, prod.getPrice()); ... pstmtUpdProdPers.setString(2, prod.getId()); ... pstmtUpdProd.executeUpdate(); pstmtUpdProdPers.executeUpdate(); conn.commit(); } } catch (SQLException e ) { // kivétel kezelése conn.rollback(); // ha hiba volt, a tranzakció sikertelen, visszagörgetünk! } finally { conn.setAutoCommit(true); } }
Ebben az esetben két PreparedStatement objektum képez egy egységet, amelyek egyetlen tranzakció részeként hajtódnak végre és kerülnek véglegesítésre a commit metódus hívásakor.
Figyelem A tranzakciókezelést nagyfokú körültekintéssel végezzük! Kitértünk ugyan rá, hogy mi az autocommit mód hátránya, azonban előnye is van: a tranzakciók nagyon rövidek, így elkerüljük, hogy adatbázisobjektumaink hosszú időre zár alá kerüljenek, amely akár blokkolhatja is az alkalmazás (vagy más alkalmazás) végrehajtását. Éppen ezért érdemes az autocommit módot csak arra az időre kikapcsolni, amikor olyan komplexebb műveleteket végzünk, amelyek konzisztenciájáról nem tudnánk meggyőződni akkor, ha minden utasítás végrehajtását automatikus tranzakcióvéglegesítés követné. A példában látható, hogy ezt legkényelmesebben a finally ágban tehetjük meg, hiszen így biztosítjuk, hogy az autocommit mód újbóli engedélyezése mindenképpen megtörténjen, függetlenül attól, hogy a tranzakció sikeresen avagy sikertelenül futott-e le.
144 Created by XMLmind XSL-FO Converter.
Adatkezelés
2.4.3.2. Az adatintegritás megőrzése A tranzakciók az összetartozó SQL-utasítások egységben történő végrehajtása mellett a táblákban tárolt adatok integritásának megőrzését is szolgálják. Például, ha lekérdezünk egy adatot az egyik táblából, majd erre alapozva elvégzünk egy számítást, és a kapott értéket el akarjuk menteni az adatbázisban, gondoskodnunk kell arról, hogy közben más felhasználó ne módosíthassa ezt az adatot, hiszen akkor az új érték helytelenné válhat. Az ilyen helyzetek elkerülése érdekében használunk tranzakciókat, amelyeknek különböző védelmi szintjei az adatok konkurens (egy időben több felhasználó általi) elérését szabályozzák. Egy tranzakció végrehajtása során fellépő konfliktusok elkerülésére az adatbázis-kezelő rendszer különböző zárakat használ. Ezek olyan mechanizmusok, amelyek a tranzakció által éppen manipulált adatokhoz való hozzáférést blokkolják a többi felhasználó (ember vagy alkalmazás) számára. Az alapértelmezett véglegesítési módban (autocommit) a zárak csak egy SQL-utasítás végrehajtásáig élnek. Ha egy zár felkerül, akkor addig marad érvényben, amíg a tranzakció véglegesítésre vagy visszagörgetésre nem kerül. Azt, hogy egy zár hogyan kerül fel, a tranzakció izolációs szintje dönti el. Az éppen aktuális izolációs szintet lekérdezhetjük a Connection.getTransactionIsolation metódussal, és új szintet állíthatunk be a kapcsolatobjektum setTransactionIsolation metódusával. Nem garantált azonban (sőt, nem is tipikus), hogy egy JDBC-meghajtó minden tranzakcióizolációs szintet támogat (már csak azért sem, mert az adatbáziskezelő rendszerek sem feltétlenül támogatnak minden szintet). Ha egy új szintet akarunk beállítani, de az nem támogatott, akkor a meghajtó automatikusan egy magasabb (szigorúbb) szintet próbál meg beállítani. Ha ez nem sikerül neki, akkor SQLException kivételt dob. A DatabaseMetaData interfész supportsTransactionIsolationLevel metódusa segítségével lekérdezhetjük, hogy egy adott szintet támogat-e az adott adatbázis-kezelő avagy sem.
Megjegyzés A különféle tranzakcióizolációs-szintek tárgyalása kívül esik e jegyzet céljain. Bővebb információkat róluk azonban bármelyik adatbázisok elméletével foglalkozó szakkönyvben talál a kedves olvasó. 2.4.3.3. Mentési pontok használata A Connection.setSavepoint metódus egy mentési pontot állít be a tranzakcióban. A Connection.rollback metódus visszagörgeti a tranzakciót az utolsó véglegesítésig, vagy túlterhelt változatával a paraméterként megadott mentési pontig. if (conn.getAutoCommit()) conn.setAutoCommit(false); // utasítások végrehajtása try (Statement stmt = conn.createStatement()) { Savepoint savepoint1 = conn.setSavepoint("Savepoint1"); stmt.executeUpdate("INSERT INTO PERSON VALUES ('John', 'Doe')"); stmt.executeUpdate("INSERT INTO PERSON VALUES ('Jane', 'Doe')"); conn.commit(); } catch (SQLException ex) { conn.rollback(savepoint1); }
Ha visszagörgetés történt egy mentési pontig, majd véglegesítjük a tranzakciót, akkor csak a nem visszagörgetett módosítások véglegesítődnek az adatbázisban. 2.4.3.3.1. Mentési pontok elengedése A Connection.releaseSavepoint metódus segítségével a metódusnak paraméterként átadott mentésipontobjektum törlődik az aktuális tranzakcióból. Ha eltávolítás után hivatkozni akarunk a mentés pontra egy rollback utasításból, az SQLException kivételt eredményez. A tranzakció véglegesítésével vagy teljes visszagörgetésével a benne létrehozott összes mentési pont elengedésre kerül és érvénytelenné válik. A tranzakció visszagörgetése egy adott mentési pontig az összes olyan mentési pontot érvényteleníti, amelyek a szóban forgó mentési pont beállítása után lettek létrehozva. 2.4.3.4. Mikor használjunk visszagörgetést?
145 Created by XMLmind XSL-FO Converter.
Adatkezelés
Amikor egy tranzakció futása közben SQLException kivétel dobódik, vissza kell görgetnünk a tranzakciót, hogy visszakapjuk az eredeti értékeket, és az adatbázis ismét konzisztens állapotba kerüljön. Ezután megkísérelhetjük a tranzakciót ismételten végrehajtani. Ez az egyetlen módja annak, hogy tudjuk, mi került véglegesítésre, és mi nem. Ha elkapunk egy SQLException kivételt, már tudjuk, hogy valami hiba történt, de arra nem derül fény, hogy a tranzakció mely része hajtódott végre, és melyik nem. Mivel nem lehetünk biztosak abban, hogy semmi nem módosult, ezért a rollback metódus hívásával kell ezt explicit módon visszagörgetnünk. ... } catch (SQLException e ) { if (conn != null) { try { System.err.print("A tranzakció vissza lesz görgetve."); conn.rollback(); } catch(SQLException excep) { ... } } ...
A fenti kódrészletből láthatjuk, ahogyan az SQLException fellépése esetén visszagörgetjük az egész tranzakciót, meghívjuk a rollback metódust a catch blokkból, és így megakadályozzuk, hogy a tranzakció addigi eredményét (amely esetleg inkonzisztens adatokat tartalmazhat) használja a kódunk további része.
2.4.4. Lekérdezések végrehajtása Egy lekérdezés végrehajtására a Statement interfész execute metóduscsaládját használhatjuk. Az execute metódus logikai igaz (true) értékkel tér vissza, ha az SQL-utasítás által visszaadott első objektum egy ResultSet. Logikai hamis (false) értéket akkor kapunk, ha az SQL-utasítás egy DML- vagy DDL-művelet. Elsősorban olyankor használjuk ezt a metódust, ha csak futásidőben dől el, hogy a végrehajtandó művelet lekérdezés vagy egyéb utasítás. Ez például akkor fordul elő, ha egy olyan felhasználói felületet készítünk, ahová lefuttatni kívánt SQL-utasításokat lehet beírni, ami elég tipikus forgatókönyv egy integrált fejlesztőkörnyezet esetén. Másodsorban olyan JDBC-driverek esetén lehet szükség ennek alkalmazására, amelyek támogatják a többszörös eredményhalmazokat (ilyen például az IBM DB2 drivere). Ilyenkor az egyes ResultSet objektumokhoz a getMoreResults és a getResultSet metódusok ismételt hívásával férhetünk hozzá. Az executeQuery metódus csupán egyetlen ResultSet objektummal tér vissza. Ahogyan arról már volt szó, egy ResultSet egy lekérdezés eredményhalmazát reprezentálja. Mindazonáltal ResultSet objektumot előállíthat bármely olyan objektum, amely implementálja a Statement interfészt, beleértve a PreparedStatement-et, és CallabaleStatement-et (sőt, a RowSet-et is, de ezzel nem foglalkozunk). Az eredményhalmaz-objektumban lévő adatokhoz egy kurzor segítségével férhetünk hozzá, amely gyakorlatilag egy mutató, amely az eredményhalmaz egy adott sorára mutat. Kezdetben a kurzor az első sor előtt áll. A ResultSet interfész számos metódust definiál a kurzor mozgatására. A leggyakrabban használt a next metódus, amellyel a kurzort eggyel léptethetjük. Amikor a kurzor eléri a ResultSet végét, vagyis az utolsó utáni sorra áll, a next metódus hamis értékkel tér vissza. ... Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query); Address address = null; while (rs.next()) { address = new Address(); address.setId(rs.getInt("ADDRESS_ID")); address.setCountry(rs.getString("COUNTRY")); address.setCity(rs.getString("CITY")); address.setZipCode(rs.getString("ZIP_CODE")); address.setStreet(rs.getString("STREET")); address.setHouseNumber(rs.getString("HOUSE_NUMBER")); resultList.add(address); } ...
146 Created by XMLmind XSL-FO Converter.
Adatkezelés
A ResultSet objektumokat jellemzi kurzorkezelésük módja, párhuzamos feldolgozási képességük és az, hogy milyen tartósságú kurzorokat kezelnek. Kurzorkezelés módja A kurzor kezelése szerint három típust különböztetünk meg: • TYPE_FORWARD_ONLY: az eredményhalmaz nem görgethető. Kurzora csak elölről hátrafelé mozgatható, az első előtti sortól indulva végig az eredményhalmazon, az utolsó utáni sorig. Az eredményhalmaz azokat a sorokat tartalmazza, amelyek kielégítik a lekérdezést annak végrehajtásának pillanatában, vagy amikor azok visszakerülnek a hívóhoz. • TYPE_SCROLL_INSENSITIVE: az eredményhalmaz görgethető. Kurzora léptethető előre- és hátrafelé is, vagy akár egy abszolút pozícióra. Az eredményhalmaz nem érzékeny az adatforrásban bekövetkező változásokra, mialatt nyitva van, vagyis az adatok változása a már megnyitott eredményhalmazokat nem érinti. • TYPE_SCROLL_SENSITIVE: az eredményhalmaz görgethető. Kurzora léptethető előre- és hátrafelé is, vagy akár egy abszolút pozícióra. Az eredményhalmaz „élővé” válik, a háttérben lévő adatforrásban bekövetkező változások hatására automatikusan frissül. Az alapértelmezett típus a TYPE_FORWARD_ONLY.
Megjegyzés Nem
minden
JDBC
implementáció
támogatja
az
összes
típust.
A
DatabaseMetaData.supportsResultSetType metódus segítségével megtudhatjuk, hogy az
adott típus támogatott-e vagy sem. A kurzorokra az alábbi műveletek értelmezettek: • next: a kurzort a következő pozícióra mozgatja. Igaz értékkel tér vissza, ha az új pozíció egy sor, és hamis értékkel, ha a pozíció az utolsó sor után található. • previous: a kurzort az előző pozícióra mozgatja. Igaz értékkel tér vissza, ha az új pozíció egy sor, és hamis értékkel, ha a pozíció az első sor előtt található. • first: a kurzort az első sorra mozgatja. Igaz értékkel tér vissza, ha az új pozíció az első sor, és hamis értékkel, ha az eredményhalmaz nem tartalmaz sorokat. • last: a kurzort az utolsó sorra mozgatja. Igaz értékkel tér vissza, ha az új pozíció az utolsó sor, és hamis értékkel, ha az eredményhalmaz nem tartalmaz sorokat. • beforeFirst: az induló pozícióra mozgatja a kurzort, vagyis az első sor elé. • afterLast: a ResultSet végére állítja a kurzort, vagyis az utolsó sor mögé. • relative(int rows): a kurzort annak aktuális pozíciójához képest relatív módon mozgatja el, pozitív paraméterérték esetén a vége felé, negatív paraméterérték esetén az eleje felé lép. • absolute(int row): abszolút pozícionálás, a kurzort pontosan a megadott sorra állítja Párhuzamosság A ResultSet párhuzamossága határozza meg a módosítási műveletek támogatottsági szintjét. Két párhuzamossági szintet különböztetünk meg: • CONCUR_READ_ONLY: a ResultSet objektum nem módosítható a ResultSet interfész segítségével. • CONCUR_UPDATABLE: a ResultSet objektum módosítható a ResultSet interfész segítségével. Az alapértelmezett párhuzamos elérési szint a CONCUR_READ_ONLY. 147 Created by XMLmind XSL-FO Converter.
Adatkezelés
Megjegyzés Nem
minden
JDBC
implementáció
támogatja
az
összes
szintet.
A
DatabaseMetaData.supportsResultSetConcurrency metódus segítségével tudhatjuk meg,
hogy az adott szint támogatott-e vagy sem Ha az eredményhalmaz módosítható (CONCUR_UPDATABLE), frissíthetjük az egyes sorok oszlopaiban tárolt értékeket..Ezt az updateXXX metódusok meghívásával érhetjük el, ahogyan az alábbi példák is mutatják: result.updateString("name", "Alex"); result.updateInt("age", 55); result.updateBigDecimal("coefficient", new BigDecimal("0.1323"); result.updateRow();
Ugyanezt a hatást persze oszlopnevek helyett oszlopindexekkel is elérhetjük: result.updateString(1, "Alex"); result.updateInt(2, 55); result.updateBigDecimal(3, new BigDecimal("0.1323"); result.updateRow();
Az updateRow metódus meghívásakor a kurzor által mutatott sor értékeivel megtörténik az adatbázis frissítése. Ha ezt a metódust nem hívjuk meg, akkor az updateXXX metódusok által elért hatás csak lokális lesz, az adatbázis tartalma nem frissül! A módosítás természetesen a tranzakciók kezelésére vonatkozó szabályok maximális betartásával történik, vagyis, ha az automatikus véglegesítés kikapcsolt állapotban van, akkor a módosítás hatása a többi tranzakció számára csak egy esetleges commit után válik elérhetővé. A módosítható eredményhalmaz segítségével új sort is felvihetünk.az alábbi módon: • először egy ResultSet.moveToInsertRow() hívással egy speciális sorra, az úgynevezett puffersorra lépünk, amelyet addig használhatunk, amíg a sor összes oszlopának adatai nem ismertek, • az updateXXX metódusok hívásával beállítjuk a puffersor tartalmát, • és végül a ResultSet.insertRow() metódus meghívásával elvégezzük a beszúrást, majd a kurzort egy érvényes állapotba visszük. Példa: result.moveToInsertRow(); result.updateString(1, "Alex"); result.updateInt(2, 55); result.updateBigDecimal(3, new BigDecimal("0.1323"); result.insertRow(); result.beforeFirst();
Kurzor tartóssága A Connection interfész commit metódus hívása bezárhatja azokat a ResultSet objektumokat, amelyek az aktuális tranzakció során jöttek létre. Néhány esetben viszont ezt el szeretnénk kerülni. A ResultSet típus tartóssági jellemzője lehetővé teszi, hogy az alkalmazás felügyelje a ResultSet objektum (kurzor) véglegesítéskor történő bezárását. Egy kurzor tartóssága az alábbi értékek valamelyike lehet: • HOLD_CURSORS_OVER_COMMIT: a kurzor nem záródik be véglegesítéskor, vagyis tartós. A tartós kurzorok használata olyan alkalmazás esetében ideális, amely csak olvasható ResultSet objektumokat tartalmaz. • CLOSE_CURSORS_AT_COMMIT: a kurzor (vagyis a ResultSet objektum) bezáródik a véglegesítéskor. Az alapértelmezett kurzortartóssági szint adatbáziskezelőrendszer-specifikus.
148 Created by XMLmind XSL-FO Converter.
Adatkezelés
Megjegyzés Nem minden JDBC implementáció támogatja a tartós és nem tartós kurzorokat. A DatabaseMetaData.getResultSetHoldability és a DatabaseMetaData.supportsResultSetHoldability metódusok segítségével kaphatunk információt a tartósság támogatottságáról.
2.4.5. Statement objektumok használata kötegelt feldolgozás esetén A Statement és leszármazottjai segítségével több SQL-utasítást is végrehajthatunk egy ütemben. Egy Statement objektum kezdetben üres, amelyhez az addBatch metódus segítségével újabb SQL-utasításokat adhatunk. A lista kiüríthető a clearBatch metódussal, és ha már minden szükséges utasítást hozzáadtunk, az executeBatch metódussal hatjhatjuk végre. A teljes kötegelt módosítás egy egységként hajtódik végre és véglegesítődik vagy görgetődik vissza (automatikus kommitálás letiltása).
6.6. példa - Kötegelt adatbázis-műveletek végrehajtása public void batchUpdate() throws SQLException { Statement stmt = null; conn.setAutoCommit(false); try (Statement stmt = conn.createStatement()) { stmt.addBatch("INSERT INTO addresses VALUES ('Magyarország', 'Debrecen', '4028', 'Kassai út', '26')"); 1 stmt.addBatch("INSERT INTO addresses VALUES ('Magyarország', 'Debrecen', '4032', 'Egyetem tér', '1')"); 1 stmt.addBatch("INSERT INTO addresses VALUES ('Magyarország', 'Budapest', '1117', 'Neumann János út', '1/C')"); 1 int[] updateCounts = stmt.executeBatch(); 2 conn.commit(); } catch(BatchUpdateException b) { // a kötegelt feldolgozás hibáinak javítása } finally { conn.setAutoCommit(true); } } 1 2
A Statement.addBatch metódus segítségével egy-egy további SQL-utasítást fűzhetünk a Statement objektumban tárolt listához. Az SQL-utasításokat elküldjük végrehajtásra az adatbázisba, majd egy eredménytömbben kapjuk vissza az egyes utasítások által érintett sorok számát az stmt-hez történő hozzáfűzés sorrendjében. Ez azért van így, mert az adatbázis a hozzáadás sorrendjében hajtja végre az utasításokat, így az eredmények is ebben a sorrendben jönnek létre.
Megjegyzés Az SQL-utasítások elküldése után, vagyis az executeBatch metódus hívását követően a Statement objektum utasítás listája kiürül. Ezt explicit módon is kikényszeríthetjük a clearBatch metódus segítségével. Ha Statement helyett PreparedStatement objektumot használunk a kötegelt feldolgozás során, akkor az SQL-utasításoknak paramétereket adhatunk át. public void batchUpdate2() throws SQLException { conn.setAutoCommit(false); try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO addresses VALUES(?, ?, ?, ?, ?)")) { pstmt.setString(1, "Magyarország"); pstmt.setString(2, "Debrecen"); pstmt.setString(3, "4028"); pstmt.setString(4, "Kassai út"); pstmt.setString(5, "26"); pstmt.addBatch(); pstmt.setString(1, "Magyarország"); pstmt.setString(2, "Debrecen");
149 Created by XMLmind XSL-FO Converter.
Adatkezelés
pstmt.setString(3, "4032"); pstmt.setString(4, "Egyetem tér"); pstmt.setString(5, "1"); pstmt.addBatch(); pstmt.setString(1, pstmt.setString(2, pstmt.setString(3, pstmt.setString(4, pstmt.setString(5, pstmt.addBatch();
"Magyarország"); "Budapest"); "1117"); "Neumann János út"); "1/C");
int[] updateCounts = pstmt.executeBatch(); conn.commit(); } catch(BatchUpdateException b) { LOGGER.log(Level.SEVERE, ex.getMessage(), ex); } finally { conn.setAutoCommit(true); } }
2.4.6. A kapcsolat lezárása Miután befejeztük munkánkat a Statement objektummal, célszerű azonnal bezárni a close metódus meghívásával, hogy az erőforrásokat, amelyeket magához köt, engedje el. E metódus hívásakor a Statement összes ResultSet objektuma is bezárul. Az erőforrások felszabadítása lényeges szempont, így arról szintén gondoskodnunk kell, hogy hiba fellépésekor is lezáródjon a Statement objektum. Ezen okból kifolyólag érdemes a lezárást egy finally blokkba tenni, hogy mind sikeres lefutás, mind hiba esetén felszabaduljanak az erőforrások. ... } finally { if (stmt != null) { stmt.close(); } }
A Java SE 7-es verziójától kezdve használhatunk try-with-resources blokkot az erőforrások lezárására. Ez a konstrukció a finally blokkot helyettesíti a try blokk fejében, hatása ugyanaz, mint finally alkalmazásakor: mind sikeres, mind sikertelen futás után bezáródik az objektum. ... try (Statement stmt = conn.createStatement(); ResultSet rset = stmt.executeQuery(query)) { while (rset.next()) { ... } } catch (SQLException e) { ... }
2.4.7. Kivételek kezelése Ha a JDBC az adatforrással történő interakció során hibába ütközik, egy SQLException kivétel váltódik ki. Ez a kivételobjektum segítségünkre lehet a hiba forrásának felderítésében. Egy SQLException objektum a következő információkat tartalmazza: • A hiba leírása, amelyet az SQLException.getMessage metódussal kérdezhetünk le. • Egy SQL állapotkód. Ez egy szabványosított, öt alfanumerikus karakterből álló szting, amelyet az SQLException.getSQLState metódus ad vissza. • Egy hibakód. Ez az egész érték annak a hibának az azonosítója, amely az SQLException-t kiváltotta. A hibakódot az SQLException.getErrorCode metódus hívásával kaphatjuk meg.
150 Created by XMLmind XSL-FO Converter.
Adatkezelés
• Egy ok. Az SQLException ok-okozati kapcsolatban lehet más kivételekkel. A teljes ok-okozati láncot végigjárhatjuk az SQLException.getCause metódus ismételt hívásával, egészen amíg az null-t nem ad vissza. • Hivatkozás láncolt kivételekre. Ha nem csak egy hiba lép fel, e lánc mentén végiglépkedhetünk a kivételeken. A láncolt kivételeket a dobott kivétel SQLException.getNextException metódusán keresztül érhetjük el. Figyelmeztetések. Az SQLWarning objektumok az SQLException leszármazottai, amelyek adatbázis elérési figyelmeztetéseket reprezentálnak. A figyelmeztetések nem szakítják meg az alkalmazás futását úgy, mint a kivételek. A figyelmeztetések mindössze azt tudatják a felhasználóval, hogy valami nem a terv szerint alakult. Például egy jog visszavonásakor az mégsem vonódik vissza, vagy lekapcsolódás közben történik valami váratlan esemény. Egy figyelmeztetés megjelenhet egy Connection, egy Statement vagy egy ResultSet objektumon. E típusok mindegyike rendelkezik egy getWarnings metódussal, amelyet meghívva visszakapjuk az első figyelmeztetést. Ha a getWarnings egy újabb figyelmeztetéssel tér vissza, az SQLWarning osztály getNextWarning metódusának segítségével végigiterálhatunk a további figyelmeztetéseken. Egy Statement végrehajtása automatikusan törli az előző utasítás figyelmeztetéseit, tehát azok nem gyűlnek fel. Ez ugyanakkor azt jelenti, hogy ha szükségünk van a figyelmeztetésekre, akkor egy új SQL-utasítás végrehajtása előtt kell lekérdeznünk azokat. A BatchUpdateException kivétel. BatchUpdateException kivétel akkor dobódik, ha kötegelt módosítás feldolgozása közben lép fel hiba. Egy BatchUpdateException objektum egyebek mellett a módosítások által érintett sorok számait is eltárolja egy egész típusú tömbben.
151 Created by XMLmind XSL-FO Converter.
7. fejezet - Grafikus felhasználói felületek készítése A Java nyelv korai időszakában (már az 1.0-ás verzióban) megjelent egy grafikus felhasználói felületek (graphical user interfaces, GUI) készítésére szolgáló programkönyvtár (library). Ennek eredeti tervezési céljai között az szerepelt, hogy segítségével egy minden platformon jól kinéző felhasználói felületet lehessen létrehozni, azonban ezt a célt nem sikerült elérni. Ez a programkönyvtár az absztrakt ablakozó eszközkészlet (abstract windowing toolkit, AWT) volt, amelynek segítségével minden platformra egyformán középszerűen kinéző felületeket lehetett létrehozni, ráadásul mindemellett sok kényelmetlen korlátozást is tartalmazott: többek között csak négyféle betűtípus használatára volt lehetőség és nem lehetett elérni az egyes operációs rendszerekben létező kifinomult felületelelemeket sem. Ezen túlmenően az AWT programozási modellje is nagyon kényelmetlen és nem eléggé objektumorientált volt. A Java 1.1 már egy sokkal letisztultabb objektumorientált megközelítésű AWT eseménymodellt hozott (a JavaBeans komponensmodellel együtt), azonban az átalakulás azzal lett teljes, hogy a Java 2 (JDK 1.2) kialakításakor létrejött a Java Foundation Classes (JFC), amelynek a grafikus felhasználói felületekkel foglalkozó részét Swingnek nevezték, de emellett lehetőséget biztosít a kinézet és a felhasználói élmény (vagyis a look and feel) cserélhetőségére, API-kat biztosít az akadálymentesítés és kétdimenziós grafikák készítéséhez, és egy nemzetköziesítési (internationalization, röviden i18n) megoldás segítségével lehetővé teszi többnyelvű alkalmazások létrehozását is. Könnyen használható és könnyen érthető JavaBeans komponenseket biztosít a programozók számára, amelyekkel akár fogd és vidd (drag and drop) módszerrel, akár kézzel leprogramozva megfelelő felhasználói felületek hozhatóak létre. Ebben a fejezetben a Swing keretrendszer alapelemeit mutatjuk be. A legtöbb Swing komponens a javax.swing csomagban illetve annak alcsomagjaiban található. A Swing keretrendszer rengeteg API-t, komponenst és egyéb lehetőségeket rejt. Éppen ezért fontosnak tartjuk megjegyezni, hogy e fejezetnek nem célja sem a Swing komponenseinek átfogó bemutatása, sem pedig az egyes tárgyalt komponensek metódusainak mindenre kiterjedő bemutatása. A Swing programkönyvtára hatalmas, ezért a fejezet célja csupán annyi, hogy betekintést nyújtson az alapvető elemekbe és fogalmakba, és kiindulópontként szolgáljon a további ismeretszerzéshez. Ebben nagy segítséget nyújthat a Swing tutoriál [SwingTutorial], illetve a témában íródott számos szakkönyv valamelyike [SCHILDT2006]. Egy Swingben írt felület alapvetően komponensek hierarchiáiból és eseménykezelőkből épül fel. A legfelső szinten lévő komponensek, a tárolóobjektumok adják a többi objektum keretét. Ezek általában ablakok vagy dialógusablakok, amelyeken panelek helyezkednek el. A panelekhez további paneleket vagy egyéb komponenseket (például gombokat, szövegmezőket, táblázatokat, fastruktúrákat, legördülő listákat) adhatunk hozzá.
1. Swing felületek felépítése A Swingben írt felületek valójában komponensek hierarchiái, amelyeket a legmagasabb szinten lévő komponensekbe, konténerekbe helyezhetünk el. Minden Swing komponens konténer is egyben, amelybe újabb komponensek tehetők, de mivel azok szintén konténerek, így újabb komponenseket rendelhetünk hozzájuk. A javax.swing.JComponent osztály a korábbi grafikusfelület-programozási API egyik központi osztályának, a java.awt.Container-nek a leszármazottja. A legmagasabb szinten lévő komponensek, az ablakok a hierarchiák gyökérelemei. Három olyan konténerosztály létezik, amely egy gyökérelemet reprezentálhat: a JFrame, a JDialog és a JApplet. Minden GUI komponens csak egy konténerben szerepelhet egyszerre: ha áthelyezzük egy másik konténerbe, akkor az előzőből automatikusan törlődik. Azonos típusú objektumokból több is szerepelhet egy konténerben. Menüsort csak legfelső szinten lévő komponensekhez adhatunk. A menüsor a tartalompanelen kívül helyezkedik el.
7.1. ábra - Példa komponenshierarchiára [SwingTutorial]
152 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése
A legfelső szinten elhelyezkedő komponensek közül több is képezhet gyökérelemet. Például, amennyiben egy alkalmazás több felületből áll: egy JFrame (főablak) és több JDialog (dialógusablakok) objektumból, akkor ezek mindegyike gyökérelem. Appletek esetében azonban csak egy JApplet objektum alkothat gyökeret.
1.1. Komponens hozzáadása a tartalompanelhez Miután létrehozunk egy új komponenst vagy komponenshierarchiát (amely maga is komponens), hozzáadjuk a keret objektum tartalompaneljéhez. frame.getContentPane().addComponent(jTabbedPane1);
Ez az utasítás egy fülekkel ellátott lapokból álló panelt ad hozzá a tartalompanelhez. A modern integrált fejlesztői környezetekben nem szükséges kézzel programoznunk a grafikus felhasználói felületeket. Az IDE olyan segédeszközöket (például palettát) biztosít, amelyek használatával a szerkesztési területre húzhatók a komponenesek, a környezet pedig legenerálja a megfelelő Java-kódot a háttérben.
Megjegyzés A legelterjedtebb integrált fejlesztőeszközökben lehetőségünk van a felhasználói felület drag-and-drop módon történő fejlesztésére. A Netbeans a Swing GUI Builder (korábban Matisse) nevű felületépítő eszközkészletet tartalmazza, míg az Eclipse-hez a legelterjedtebb ilyen eszköz a WindowBuilder nevű plugin, amely az Eclipse for Java Developers változatban előre telepítve van, az egyéb Eclipse változatokban pedig az Eclipse Marketplace segítségével pluginként telepíthető. Az IntelliJ Idea is rendelkezik Swing GUI Designer-rel, és az Oracle JDeveloper is beépítetten tartalmaz ilyen eszközt.
7.2. ábra - Az Eclipse WindowBuilder GUI programozást segítő palettája
153 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése
1.1.1. A JComponent osztály Minden Swing komponens a JComponent osztály leszármazottja, így örökli annak adattagjait és metódusait. A leszármazott osztályok egyedi jellemzőin túl vannak olyan tulajdonságok, amelyekkel minden Swing komponensnek rendelkeznie kell. Ezek a tulajdonságok a közös ősben, a JComponent osztályban találhatók, és hét területet ölelnek fel, amelyek a következők: • A komponens megjelenítésének testreszabása (keret, előtér, háttér, betűk). • A komponens állapotának lekérdezése és beállítása (engedélyezés, láthatóság, szerkeszthetőség). • A Figyelő (Observer) és a Parancs (Command) tervezési mintát megvalósító eseménykezelők (különböző listener objektumok) hozzárendelése a komponenshez. • A komponens megjelenítése (kirajzolása). • Azon komponenshierarchia kezelése, amelynek a komponens a gyökéreleme. • A tartalmazott komponensek elhelyezkedésének (layout) kezelése.
154 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése • Információk és beállítások a komponens méretével és pozíciójával kapcsolatban. A következő szakaszokban néhány alapvető komponenst ismerhetünk meg. Az összetettebb komponenseknél bepillanthatunk a mögöttük álló modellek és eseménykezelők működésébe is.
1.2. Szöveges komponensek Az első csoportban az egyszerű szövegmezők találhatók. Ezek egysoros, rövidebb szöveget kezelő komponensek. A második és a harmadik csoportba a szövegterületek tartoznak, amelyek hosszabb, többsoros szöveges adatok megjelenítésére és szerkesztésére használhatók. A szövegmezők és –területek értékét a getText metódussal kérdezhetjük le, illetve a setText metódus segítségével állíthatjuk be.
7.3. ábra - Szöveges komponensek osztályozása [SwingTutorial]
String username = jTextField1.getText(); String password = new String(jPasswordField1.getPassword()); jTextField1.setText(""); jPasswordField1.setText("");
Megjegyzés Jelszót tároló szövegmező esetében a getPassword metódus használata javasolt a getText metódus helyett, azonban az String helyett char[]-öt ad vissza.
1.3. Listák és legördülő listák A listák és legördülő listák egy vagy több választási lehetőséget kínálnak a felhasználónak. Egyszerű listákat akkor célszerű használni, ha a felületen sok hely van, vagy fontos, hogy minden pillanatban minél több elem látsszon. Legördülő listákat (comboboxokat) akkor célszerű alkalmazni, ha kevés a hely a felületen, mert ezek a komponensek összezárt állapotban mindössze egyetlen sornyi helyet foglalnak el. A listák és legördülő listák listamodellekkel dolgoznak, amelyek az adatokat tartalmazzák. A következő kódrészlet bemutatja, hogyan készíthetünk saját listamodellt, valamint a Swing keretrendszer által biztosított alapértelmezett listamodellek használatát. A modelleket a JList komponens és a JComboBox komponens setModel metódusával adjuk át a komponenseknek. private List<String> categories;
A saját listamodell létrehozása például névtelen osztállyal történhet. Miután a kategóriákat tartalmazó listát feltöltjük az adatbázisból, átadjuk azt a listamodellnek. A felüldefiniált getSize metódus a kategóriákat tartalmazó lista méretével tér vissza, a getElementAt metódus pedig ennek a listának az adott elemét kérdezi le és adja át.
155 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése private AbstractListModel categoryListModel; List<String> categories = new ArrayList<>(); for (List cols : entityManager.select("select distinct category from products")) { categories.add("" + cols.get(0)); } categoryListModel = new javax.swing.AbstractListModel() { @Override public int getSize() { return categories.size(); } @Override public Object getElementAt(int i) { return categories.get(i); } }; jList2.setModel(categoryListModel);
A következő kódrészletben a legördülő listának egy alapértelmezett modellt adunk át, amely az általunk beállított értékeket tartalmazza. jComboBox1.setModel(new javax.swing.DefaultComboBoxModel(new String[]{"ÜGYFÉL", "ADMINISZTRÁTOR", "VEZETŐ"}));
1.3.1. A kiválasztási modell A listák és legördülő listák egy ListSelectionModel objektumot használnak elemeik kiválasztásának módjához. Három kiválasztási mód lehetséges: 1.3.1.1. Egyszeres kiválasztás
7.4. ábra - Egyszeres kiválasztás
Egy időben csak egy listaelem választható ki. Ha a felhasználó egy másik elemet jelöl ki, az előző automatikusan deszelektálódik. 1.3.1.2. Egyszeres intervallum kiválasztás
7.5. ábra - Egyszeres intervallum kiválasztás
156 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése
Több, egymást közvetlenül követő elem kiválasztása. Amikor a felhasználó egy új intervallumot választ, az előzőleg kijelölt elemek deszelektálódnak. 1.3.1.3. Többszörös intervallum kiválasztás
7.6. ábra - Többszörös intervallum kiválasztás
Ez az alapértelmezett kiválasztási mód. Az elemek bármely kombinációja választható. A felhasználó maga deszelektálja az elemeket, ha szükséges. A kiválasztási modellt a setSelectionMode metódus segítségével állíthatjuk be egy lista komponensen. A lista kiválasztási modellt táblázatobjektumokra is alkalmazhatjuk. Egyszeres kiválasztási mód beállítása. jList2.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); ... jTable1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
1.3.2. Eseménykezelők 7.7. ábra - Eseménykezelők [SwingTutorial]
157 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése
A Swing eseménymodellje hatékony és rugalmas. Bármely eseménytípust tetszőlegesen sok forrásból felismerhet több eseményfigyelő objektum (event listener). Egy esemény figyelésére egy objektum is beállítható, de egy listener objektum minden forrás minden eseményét is figyelheti, valamint egy esemény figyelésére egyetlen forrásból több listener objektum is ügyelhet. Minden esemény egy objektum, amely információkat tartalmaz az eseményről és az esemény forrásobjektumáról. Az események forrása legtöbbször komponens vagy modell, de bármely objektumtípus funkcionálhat eseményforrásként. A Swing az eseményeket csoportosítja is: beszél alacsony szintű és szemantikus eseményekről. Az alacsony szintű események főként az ablakozó rendszer eseményei, mint amilyenek például a közvetlenül a felhasználótól érkező billentyűzet- és egéresemények. A szemantikus események ezeknél magasabb szintűek, és felhasználói bemenet váltja ki: például rákattint egy gombra. Jellemző, hogy egy szemantikus esemény mögött alacsony szintű események egész sorozata húzódik meg: egy gombnyomáshoz, mint szemantikus eseményhez az egérkurzor pozícionálására, az egérgomb lenyomására és felengedésére is szükség van (ezek mind alacsony szintű műveletek). Ráadásul ugyanazt a szemantikus eseményt több alacsony szintű eseménysoorozattal is elérhetjük, hiszen a gombnyomás szemantikus esemény pusztán a billentyűzet segítségével is kiváltható: a TAB billentyűvel kiválasztva a gombot a szóköz illetve az Enter billentyű lenyomásával. A szemantikus eseményeket négy osztály reprezentálja: • az ActionEvent, amely ezek közül a leggyakoribb, gombnyomáskor, menüválasztáskor, listaelemkiválasztáskor, illetve a szövegmezőn Enter leütésekor keletkezik ilyen esemény; • az AdjustmentEvent, amely egy görgetősáv (scroll bar) használata során következik be; • az ItemEvent akkor áll elő, ha a felhasználó jelölőnégyzetek segítségével vagy listaelemekből kiválaszt valamit; • míg végül a TextEvent akkor jön létre, ha egy szövegmező vagy szövegterület tartalma megváltozik. Az alacsony szintű események osztályai az alábbiak: • ComponentEvent, amely amellett, hogy komponensek átméretezése, mozgatása, megjelenítése illetve elrejtése során következik be, az összes alacsony szintű esemény ősosztálya is; • KeyEvent, amely egy billentyű lenyomása és felengedése esetén generálódik; • MouseEvent, amely az egérkurzor mozgatása, az egyes egérgombok lenyomása és felengedése, és vonszolás során jön létre; • FocusEvent, amelynek a segítségével arról értesülhhetünk, ha egy komponens megkapta vagy éppen elvesztette a fókuszt; • WindowEvent, akkor generálódik, ha egy ablakot aktiváltak, deaktiváltak, ikon állapotúra kicsinyítettek, ikon állapotúról felnagyították, vegy éppen bezárták; és végül • ContainerEvent, ami egy-egy komponens konténerhez adásakor vagy onnan való törlésekor jön létre. Ha csak lehet, a szemantikus eseményekre kell feliratkoznunk, nem pedig az alacsony szintűekre. Ennek több előnye is van: egyrészt így tudjuk a kódunkat a leginkább hordozhatóvá és robusztussá tenni, másrészt pedig így 158 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése akkor is értesülünk egy eseményről (például egy nyomógombra kattintásról), ha azt nem az elvárt módon végezték. Egy nyomógomb megnyomását például csak egérrel érhetjük el, hanem a billentyűzettel is. Ha ekkor a szemantikus esemény helyett csupán arra iratkoztunk volna fel, hogy egy adott területen (ahol a gomb van) történik-e kattintás és felengedés egérgomb segítségével, akkor a billentyűzet segítségével végzett „gombnyomásról” bizony lemaradnánk. A következő táblázat a komponensek és a hozzájuk rendelhető figyelő objektumok típusát tartalmazza.
7.1. táblázat - Komponensek és figyelőik [SwingTutorial] Kompone ActionList CaretListe ChangeLi Document ItemListe ListSelecti WindowLi ns ener ner stener Listener,U ner onListene stener ndoableE r ditListene r
További listener fajták
button check box color chooser combo box dialog file chooser frame list
ListData
password field radio button TableModel , TableColum nModel, CellEditor
table
text area text field Egy tevékenységfigyelő (action listener) implementálásával megadhatjuk, hogy mi történjen, ha a felhasználó egy bizonyos tevékenységet végez. A felhasználó által végzett tevékenység lehet például egy gomb megnyomása, egy listaelem kiválasztása, vagy az Enter billentyű megnyomása egy szövegmezőn. Mindennek az az eredménye, hogy egy actionPerformed üzenet kerül elküldésre minden olyan tevékenységfigyelő objektumnak, amely fel van iratkozva az esemény forráskomponensére. Egy tevékenységfigyelő implementálása a következő lépésekből áll: 1. Egy eseménykezelő osztály deklarálása, amely implementálja az ActionListener interfészt. public class MyActionListener implements ActionListener { ... }
2. Az interfész actionPerformed metódusának implementálása. A példában a kombóbox segítségével kiválasztott elemet (egy szerepkört) a currentRole nevű változónak adjuk értékül. public void actionPerformed(java.awt.event.ActionEvent evt) { JComboBox cb = (JComboBox) evt.getSource();
159 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése currentRole = (String) jComboBox1.getSelectedItem(); }
Megjegyzés Amennyiben az eseménykezelőt több eseményforráshoz is csatoljuk (regisztráljuk), és szeretnénk attól függő feldolgozást végezni, hogy melyik volt az az eseményforrás, amelyen az esemény ténylegesen bekövetkezett és amely miatt az eseménykezelő aktivizálódott, akkor az ActionEvent objektumból tudjuk mindezt kinyerni, ahogyan a fenti példa is mutatja ( evt.getSource()). Erre általában nincs szükségünk, mert sokszor vagy csak egy eseményforrással dolgozunk, vagy ha több is van belőlük, nem feltétlenül van szükség forrásfüggő feldolgozásra. Ekkor a paramétert gyakorlatilag figyelmen kívül hagyjuk. 3. Az osztály egy példányának egy vagy több komponens figyelésére történő feliratkoztatása. Ez tulajdonképpen nem más, mint a Megfigyelő (Observer) minta megjelenése, amikor a megfigyelő (ez esetben a MyActionListener példány) regisztrálja magát a megfigyeltnél (ez a jComboBox1 változónévvel hivatkozott kombóbox). jComboBox1.addActionListener(new MyActionListener());
Ezt követően, ha a felhasználó elvégez egy bizonyos tevékenységet, a komponens elindít egy tevékenységeseményt. Ez az ActionListener interfészt megvalósító objektum actionPerformed metódusának meghívását eredményezi. A metódus egyetlen paramétere egy ActionEvent objektum, amely információt adhat az eseményről és annak forráskomponenséről.
Megjegyzés A tevékenységfigyelőt gyakran névtelen osztályként valósítjuk meg. Különösen így van ez akkor, ha csak egyetlen felületelemhez tartozik az eseménykezelő, hiszen akkor elég csak egyetlen helyen, a megfigyeltnél történő regisztációkor (addActionListener hívás) hivatkozni.
1.4. Táblázatok 7.8. ábra - Táblázat beágyazott ComboBox objektumokkal
A JTable osztály objektumainak segítségével táblázatos adatokat tudunk megjeleníteni és kezelni. A táblázatok oszlopokból és sorokból állnak, amelyeknek metszetében a cella található. A táblázat adatait a kódban egy kétdimenziós objektumtömbként vagy Vector-ok Vector-aként adhatjuk meg (adatok paraméter). Object[][] adatok = {...}; Object[] oszlopnevek = {...}; JTable tablazat = new JTable(adatok, oszlopnevek);
Első konstruktor: public JTable(Object[][] adatok, Object[] oszlopnevek);
Második konstruktor:
160 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése public JTable(Vector adatok, Vector oszlopnevek);
Egy táblázat objektum sorain és oszlopain szűrési és rendezési műveleteket is végezhetünk, amelyekhez saját implementációt adhatunk.
1.4.1. Kiválasztás Egy táblázaton összetettebb módon hajthatunk végre kijelöléseket, mint listák esetében, habár az összetettebb kijelölések visszavezethetők az egyszerűbbekre. A táblázatban sorokat, oszlopokat, és cellákat jelölhetünk ki. A kiválasztás módja lehet összefüggő, valamint nem összefüggő többszörös, vagy egyszeres.
1.4.2. Modellek A legtöbb Swing komponens mögött áll egy modell. Például egy nyomógomb ( JButton) objektum rendelkezik egy ButtonModel interfészt megvalósító objektummal, amely a gomb állapotáról tárol információt. Némelyik komponenshez több modell is hozzá van rendelve, például egy JList objektum egy ListModel-t használ a tartalma tárolására és egy ListSelectionModel-t az aktuális kijelölések számára. Legtöbbször nem szükséges tudnunk ezekről a modellekről, elegendő magát a komponenst programoznunk. Mindazonáltal, szükség van modellekre az adatok rugalmas tárolásának és elérésének érdekében. Például, egy egyedi tábla esetében javasolt saját táblamodell implementációt megadnunk, ha az alapértelmezett modell nem felel meg az elvárásoknak. A modellek nem csupán tárolják az adatokat, hanem automatikusan továbbítják az adatokon bekövetkezett változásokat a figyelő objektumok felé. Például egy listaelem hozzáadásánál a komponens helyett a listamodell megfelelő metódusa hívódik meg. Ha változás következik be, a listamodell értesíti a megjelenítő komponenst és a feliratkozott figyelőket, így a felület azonnal frissül.
Fontos A Swing egy módosított modell–nézet–vezérlő (Model–View–Controller) minta mentén épül fel, ahol a modell jól elkülöníthető a nézettől és a vezérlőtől, azonban a megjelenítés és a vezérlő egymástól nem teljesen szétválasztható. 1.4.2.1. Táblamodell létrehozása Minden táblázatobjektum mögött áll egy táblamodell, amely a táblázat adatait kezeli. A táblamodell objektumnak implementálnia kell a TableModel interfészt. Ha nem rendelünk táblamodellt egy táblázat komponenshez, automatikusan hozzárendelődik egy úgynevezett DefaultTableModel típusú objektumpéldány.
7.9. ábra - Táblázat és táblamodelljének kapcsolata [SwingTutorial]
public class MyTableModel extends AbstractTableModel { protected Object[][] tableData; protected List<String> columnNames;
A legördülő lista, amelynek segítségével a felhasználó kiválaszthatja, hogy a könyvet a polcra vagy a kosárba teszi-e. private final JComboBox moreSelection;
161 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése A táblázat sorainak számát visszaadó metódus. @Override public int getRowCount() { return this.tableData.length; }
A táblázat oszlopainak számát visszaadó metódus. @Override public int getColumnCount() { return this.columnNames.size(); }
Az adott cellában található értéket visszaadó metódus. @Override public Object getValueAt(int rowIndex, int colIndex) { return this.tableData[rowIndex][colIndex]; }
Adott oszlop nevét visszaadó metódus. @Override public String getColumnName(int colIndex) { return this.columnNames.get(colIndex); }
Ez a metódus az adott oszlop típusát határozza meg. Ha nem implementáljuk a metódust, akkor minden oszlop értéke szövegként jelenik meg. A példa azt mutatja be, hogy mit tegyünk akkor, ha a logikai értékek megjelenítését jelölőnégyzet (checkbox) segítségével szeretnénk megvalósítani. @Override public Class getColumnClass(int colIndex) { Class c = getValueAt(0, colIndex).getClass(); if (c.equals(Boolean.class)) { if (colIndex != selectionColIndex) { return Object.class; } } return c; }
Ez a metódus megmondja, hogy az adott cella szerkeszthető-e. Csak akkor szükséges implementálni, ha a táblázat szerkeszthető. @Override public boolean isCellEditable(int rowIndex, int colIndex) { if (colIndex < this.selectionColIndex) { return false; } else { return true; } }
Az adott cellában található értéket beállító metódus. Csak akkor szükséges implementálni, ha a táblázat adatai változhatnak. @Override public void setValueAt(Object value, int rowIndex, int colIndex) { this.tableData[rowIndex][colIndex] = value; fireTableCellUpdated(rowIndex, colIndex); selectionValuesById.put(this.tableData[rowIndex][0], value); }
Megjegyzés
162 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése A fireTableCellUpdated metódus hívásának hatására az összes érdeklő figyelő értesítést kap arról, hogy a [rowIndex, colIndex] cella tartalma frissült.
1.4.3. Események kezelése Egyazon táblamodell objektumra akár több figyelővel is feliratkozhatunk, amelyek így a modell adatain (állapotán) bekövetkezett változásokról értesítést kapnak. A táblamodell figyelőinek a TableModelListener interfészt kell implementálniuk. public class MyTableModelListener implements TableModelListener { public MyTableModelListener() { ... table.getModel().addTableModelListener(this); } public void tableChanged(TableModelEvent e) { //a kiválasztás első sora int row = e.getFirstRow(); //a kiválasztott oszlop int column = e.getColumn(); //az esemény forrása TableModel model = (TableModel)e.getSource(); //a kiválasztott oszlop neve String columnName = model.getColumnName(column); //a cellában található érték Object data = model.getValueAt(row, column); ... } }
Amennyiben a felület osztályából elérjük a táblát, nincs szükség TableModelEvent objektumra. Ekkor egy egyszerű actionPerformed metódus kezeli az eseményt. private void jButton14ActionPerformed(java.awt.event.ActionEvent evt) { int selectedRow = jTable1.getSelectedRow();
Amennyiben a táblázat rendezve is van, az alábbi metódust is meg kell hívnunk a helyes index megtalálásához. selectedRow = jTable1.convertRowIndexToModel(selectedRow); // Könyv azonosítója (ISBN) idOfSelectedProduct = (String)jTable1.getModel().getValueAt(selectedRow, 0); }
Az adatok megváltozásának eseménye csak úgy váltható ki, ha a táblamodell tudatában van annak, hogy hogyan kell egy TableModelEvent objektumot előállítani. Ez bonyolult feladat, azonban a DefaultTableModel osztály megfelelő implementációt biztosít hozzá. Így ha saját implementációt írunk, a DefaultTableModel osztály kiterjesztése javasolt. Amennyiben az alapértelmezett táblamodell osztály mégsem felel meg alaposztályként, az AbstractTableModel osztályt kell kiterjesztenünk. Ebben az esetben saját osztályunknak elegendő a következő metódusok egyikét hívni, amikor egy külső forrás módosítja a táblamodell adatait. Metódus
Változás
fireTableCellUpdated
Egy cella módosítása
fireTableRowsUpdated
Sorok módosítása
fireTableDataChanged
A teljes táblázat adatainak változása (csak az adatok)
fireTableRowsInserted
Új sor beszúrása
fireTableRowsDeleted
Sorok törlése
fireTableStructureChanged
A táblázat adatainak és szerkezetének módosítása
A következó kódrészlet egy fireTableCellUpdated metódus hívását mutatja be. @Override public void setValueAt(Object value, int rowIndex, int colIndex) { this.tableData[rowIndex][colIndex] = value;
163 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése fireTableCellUpdated(rowIndex, colIndex); selectionValuesById.put(this.tableData[rowIndex][0], value);}
1.5. Elrendezéskezelők (Layout menedzserek) Egy elrendezéskezelő vagy elhelyezéskezelő a LayoutManager interfészt implementáló objektum, amely a komponensek méretét és pozícióját határozza meg egy tárolón belül.A Swing komponensek alapértelmezett elrendezéskezelővel rendelkeznek, amely például panelek esetében a FlowLayout, míg tartalompaneleknél a BorderLayout. Az alapértelmezett elrendezéskezelők leválthatók, erre azonban legtöbbször nincs szükség. JPanel panel = new JPanel(new BorderLayout()); ... Container contentPane = frame.getContentPane(); contentPane.setLayout(new FlowLayout());
Amennyiben nem használunk elhelyezéskezelőt, abszolút pozícionálást kell végeznünk, amely értelmében minden komponensnek explicit módon kell megadnunk a méretét és a pozícióját a tárolón belül. Ezek az értékek rögzítettek, amelyek például az ablak átméretezésekor sem frissülnek. Éppen ezért ez kerülendő. • BorderLayout: a tartalompanelek (content pane) alapértelmezett elrendezéskezelője, így az összes legfelső szintű konténerben (ablakkeretben, dialógusablakban és appletben) ezt használhatjuk alapból. A rendelkezésre álló területet öt részre osztja fel: fölső, alsó, jobb oldali, bal oldali és középső, ahogyan az az ábrán is látható.
7.10. ábra - BorderLayout[SwingTutorial]
• BoxLayout: a komponenseket egy sorba vagy egy oszlopba helyezi, figyelembe véve a komponens által igényelt maximális méretet.
7.11. ábra - BoxLayout[SwingTutorial]
• CardLayout: segítségével olyan felület valósítható meg, ahol a különböző időpontokban különféle komponensek jelennek meg. Tulajdonképpen fülek segítségével elért lapokként gondolhatnuk rá, ahol sokszor tényleges fülek helyett egy combobox segítségével válthatunk az egyes lapok között.
7.12. ábra - CardLayout [SwingTutorial] 164 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése
• FlowLayout: a JPanel-ek alapértelmezett elrendezéskezelője, sorfolytonosan tölti ki a rendelkezésre álló teret. Ha egy sorban már nincs elegendő helye egy elhelyezendő komponens számára, akkor új sort kezd.
7.13. ábra - FlowLayout[SwingTutorial]
• GridLayout: a komponensek számára azonos méretet határoz meg egy n×m-es rácshálóban.
7.14. ábra - GridLayout[SwingTutorial]
• GridBagLayout: nagyon fejlett és rugalmas elrendezéskezelő, a GridLayout továbbfejlesztéseknét jött létre. A komponenseket szintén egy rácsháló alapján helyezi el, de megengedi, hogy egy-egy komponens több sort, illetve oszlopot is elfoglaljon. A rácsháló egyes sorai akár eltérő magasságúak, oszlopai pedig akár eltérő szélességűek is lehetnek. Nagyfokú rugalmassága mellett az egyik legnehezebben használható elrendezéskezelő is egyben.
7.15. ábra - GridBagLayout[SwingTutorial]
• GroupLayout: a grafikus felhasználói felületet drag-and-drop módszerrel öszeállító integrált fejlesztői keretrendszerek számára jött létre. Külön-külön foglalkozik a vízszintes és a függőleges elrendezéssel, vagyis
165 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése minden komponens helyét kétszer kell definiálni: egyszer vízszintesen, egyszer pedig függőlegesen kell elhelyezni.
7.16. ábra - GroupLayout[SwingTutorial]
• SpringLayout: szintén IDE-támogatásként jött létre. Lehetővé teszi, hogy az egyes komponensek szélei között pontos kapcsolatokkal írhassuk le komponenseink elhelyezkedését. Definiálható segítségével például, hogy adott komponens bal széle és egy másik komponens jobb széle között mekkora legyen a távolság (amely akár dinamikusan is számítható).
7.17. ábra - SpringLayout[SwingTutorial]
1.5.1. A megfelelő elhelyezési stratégia kiválasztása Az elhelyezéskezelőt nem szükséges kézzel beállítanunk, az integrált fejlesztői keretrendszerek biztosítanak olyan eszközöket, amelyek segítségével ez egyszerűen elvégezhető. Amennyiben úgy döntünk, hogy mégis segédprogram nélkül, kézzel állítjuk be a layout menedzsereket, a következő rész nyújt segítséget annak eldöntésében, hogy milyen esetben mely elrendezéskezelőt érdemes használni. • Amennyiben a lehető legnagyobb helyen szeretnénk a komponenst megjeleníteni, akkor a GridLayout vagy a BorderLayout a megfelelő választás, egy komponens esetében. Több komponensnél érdemesebb a GridBagLayout-ot használni. • Amennyiben néhány komponenst akarunk egy sorban megjeleníteni, átméretezés nélkül, akkor a FlowLayout, a BoxLayout vagy a SpringLayout a megfelelő választás. • Amennyiben néhány komponenst akarunk megjeleníteni egyforma méretben oszlopokba és sorokba rendezve, akkor a GridLayout szerinti elrendezést érdemes választanunk. • Amennyiben néhány komponenst szeretnénk egy sorban vagy egy oszlopban megjeleníteni, változó térközzel, igazítással és méretezéssel, akkor a BoxLayout-ot érdemes használni. • Amennyiben rendezett oszlopokat szeretnénk megjeleníteni egy űrlap elrendezéséhez hasonló módon, ahol a címkék az egyes szövegmezők megnevezésére szolgálnak, a SpringLayout a legmegfelelőbb választás.
166 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése • Amennyiben összetett elrendezést kívánunk használni nagyszámú komponenssel, valamely rugalmas elrendezéskezelőt érdemes választanunk, például a GridBagLayout-ot vagy a SpringLayout-ot. Egy másik módszer, ha az összetett elrendezést szétválasztjuk kisebb panelekre, amelyeken önálló elrendezést állítunk be önálló kezelővel, ez általában véve is jó stratégia a bonyolultság csökkentésére.
1.5.2. Elrendezéskezelők működése Az elhelyezéskezelők alapvetően két dolgot tesznek: kiszámolják a tároló minimális, maximális és preferált méretét, és elrendezik a tároló beágyazott komponenseit. Ezek a komponenesek ráadásul rendelkezhetnek saját elrendezéskezelővel, amelyek előnyt élveznek a tartalmazó komponens elrendezéskezelőjével szemben, vagyis a specifikusabb komponensre előírt elhelyezés felülírja az általánosabbra előírtat. Egy konténer állapota lehet érvényes vagy érvénytelen. Ahhoz, hogy egy konténer érvényes állapotba kerüljön, minden gyermekén végbe kell mennie az elrendezésnek, és így érvényes állapotba kell kerülnie. Az elrendezés elvégzését és a komponens érvényes állapotba juttatását a Container osztály validate metódusa végzi. Miután egy komponens elkészült, még érvénytelen állapotban van. Első alkalommal a Window.pack metódus hívása érvényesíti az ablakot, és végzi el az alkomponensek elrendezését.
2. Tervezési minták a Swing keretrendszerben A grafikus objektumok felépítését jobban megismerve láthatjuk, hogy a Swing keretrendszer jónéhány programtervezési mintát megvalósít. Leggyakrabban a Díszítő (Decorator) mintával találkozhatunk, meglehetősen jellemző a keretrendszerre, hogy a komponensei egymást díszítik. Például egy táblázat görgethetővé tehető ugyanúgy, mint egy fastruktúra, ha egy görgethető panel objektummal dekoráljuk. A másik gyakori minta a modell–nézet–vezérlő (Model–View–Controller, MVC). Számos komponenshez rendelhetünk modellobjektumot, amely a komponens adatait tartalmazza és kezeli (ezek felelnek meg a modellnek), míg a komponens maga a megjelenítésért és a bekövetkezett események továbbításáért felel (a nézet és a vezérlő felelősségei). A Swing eseménymodellje nagy mértékben támaszkodik a Megfigyelő (Observer) mintára: a listener osztályok megfigyelők, amelyek példányait az egyes GUI komponenseknél vagy modellobjektumoknál lehet regisztrálni, és így egy bekövetkezett változásról értesítést kapnak. Egy főablak a gyermekablakai (vagy tartalmazott komponensei) számára Közvetítő (Mediator) szerepkört tölthet be, például olyankor, amikor a felület egy részén bekövetkező változás valamely más komponensekre is kihatással van. Ez akkor fordulhat elő, amikor a felület egy komponensének állapotától függ más felületelemek állapota, például egy gomb megnyomása vagy egy listaelem kiválasztása esetén bizonyos felületelemek letiltott (nem kattintható) vagy akár elrejtett állapotba kerülnek. Egy ilyen esetben bonyolult interakciók alakulhatnak ki az egyes elemek között, ezért szükség lehet egy közvetítőre, amely jelentősen leegyszerűsítheti az objektumok közötti viszonyrendszert. A gyermekablakok hozzáférhet a főablak adattagjaihoz és módosíthatja azokat, amelyek a többi gyermekablak számára is láthatóak. Néhány Swing komponens egy több komponensből összeállított, mindig együttesen alkalmazott, bonyolult kód ismétlését hivatott kiváltani. Ilyen komponens például a JOptionPane, amely a rendszertől érkező üzeneteket jelenít meg a felhasználó számára, vagy a JFileChooser, amely egy fájlok kiválasztására szolgáló dialógusablak. Ezek a komponensek Homlokzatot (Façade) képeznek, és elrejtik a mögöttük álló bonyolultabb kódot.
7.18. ábra - Információt megjelenítő JOptionPane komponens
167 Created by XMLmind XSL-FO Converter.
Grafikus felhasználói felületek készítése
A Swing erőteljesen használja az Illesztő (Adapter) tervezési mintát is, ugyanis tipikus, hogy bizonyos listener interfészek egynél több metódust is definiálnak. Például a MouseListener-nek öt metódusa is van, ezek: a mousePressed, a mouseReleased, a mouseEntered, a mouseExited és a mouseClicked. A MouseListener interfész megvalósításakor viszont akkor is implementálnunk kellene mind az öt metódust, ha minket valójában csak az egérkattintás-események érdekelnek. Ekkor a másik négy metódushoz nyilvánvalóan csak üres implementációt adnánk, azonban ettől még a kódunk nehezebben olvashatóvá és nehezebben karbantarthatóvá is válna. // A MouseListener-t közvetlenül megvalósító eseménykezelő osztály public class MyMouseListener implements MouseListener { ... someObject.addMouseListener(this); ... /* Az első négy metódus üres törzzsel kerül implementálásra, mert ezekere az eseményekre nem vagyunk kíváncsiak */ public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mouseClicked(MouseEvent e) { ...//A minket érdeklő esemény kezelését megvalósító metódus } }
Hogy ezzel a bosszússággal ne szembesüljünk, az API beépítetten tartalmazza például a MouseAdapter osztályt, amely üres törzzsel már implementálja a MouseListener összes metódusát, de az adapter tulajdonság miatt az adapternek csupán azt a metódusát (vagy azokat a metódusait) definiáljuk felül, amelyik (amelyek) iránt valóban érdeklődünk. // A MouseListener interfész közvetlen megvalósítása // helyett használjunk adaptert! public class MyMouseListener extends MouseAdapter { ... someObject.addMouseListener(this); ... public void mouseClicked(MouseEvent e) { ...//A minket érdeklő esemény kezelését megvalósító metódus } }
168 Created by XMLmind XSL-FO Converter.
Irodalomjegyzék Könyvek [UNCLEBOB2009] Martin, Robert. Clean Code. A Handbook of Agile Software Craftsmanship. 2009. 1. 9780-13-235088-4. 464. Prentice-Hall. [UNCLEBOB2010] Martin, Robert. Tiszta kód. Az agilis szoftverfejlesztés kézikönyve. 2010. 1. 9789639637696. 512. Kiskapu. [GOF1994] Gamma, Erich, Helm, Richard, Johnson, Ralph, és Vlissides, John. Design patterns. Elements of Reusable Object-Oriented Software. 1994. 1. 978-0201633610. 416. Addison-Wesley. [GOF2004] Gamma, Erich, Helm, Richard, Johnson, Ralph, és Vlissides, John. Programtervezési minták. Újrahasznosítható elemek tervezése objektumközpontú programokhoz. 2004. 1. 9639301779. 448. Kiskapu. [FOWLER1999] Fowler, Martin, Beck, Kent, Brant, John, Opdyke, William, és Roberts, Don. Refactoring. Improving the Design of Existing Code. 1999. 1. 978-0201485677. 464. Addison-Wesley. [FOWLER2006] Fowler, Martin, Beck, Kent, Brant, John, Opdyke, William, és Roberts, Don. Refactoring. Kódjavítás újratervezéssel. 2006. 1. 9789639637139. 384. Kiskapu. [BLOCH2008EN] Bloch, Joshua. Effective Java. 2008. 2. 978-0321356680. 346. Addison-Wesley. [BLOCH2008HU] Bloch, Joshua. Hatékony Java. 2008. 1. 9789639637504. 320. Kiskapu. [ISTQB2007] Graham, Dorothy, Van Veenendaal, Erik, Evans, Isabel, és Black, Rex. Foundations of Software Testing. 2007. 1. 978-8131502181. 258. Thomson Press. [ISTQB2010] Graham, Dorothy, Van Veenendaal, Erik, Evans, Isabel, és Black, Rex. A szoftvertesztelés alapjai. 2010. 1. 978-9630698580. 291. Alvicom. [PRAGPROG1999] Hunt, Andrew és Thomas, David. The Pragmatic Programmer. From Journeyman to Master. 1999. 1. 978-0201616224. 352. Addison-Wesley. [LARMAN2004] Larman, Craig. Applying UML and Patterns. An Introduction to Object-Oriented Analysis and Design and Iterative Development. 2004. 3. 978-0131489066. 736. Prentice-Hall. [SCHILDT2006] Schildt, Herbert. Swing. A Beginner's Guide. 2006. 1. 978-0072263145. 590. McGraw-Hill Osborne. [STÖRRLE2007] Störrle, Harald. UML 2. Unified Modeling Language. 2007. 1. 978-99635454655. 312. Panem. [KACZANOWSKI2013] Kaczanowski, Tomek. Practical Unit Testing with JUnit and Mockito. 2013. 1. 9788393489398. 402. Tomasz Kaczanowski. Webes hivatkozások [EclipseDoc] Eclipse documentation. Web page . [SwingTutorial] Creating a GUI With JFC/Swing (Also known as The Swing Tutorial). Web page . [JAXPTutorial] Java API for XML Processing (JAXP). Web page . [JDBCTutorial] JDBC Database Access. Web page . [OODesign] Object-Oriented Design. Web page . [Sourcemaking] Sourcemaking. Web page .
169 Created by XMLmind XSL-FO Converter.