Objektumorientált tervezési szempontok és minták
- Objektumorientált tervezési szempontok és minták
A tervezés alapelvei (SOLID)
- Single responsibility principle
- Open/Closed principle: a programegységek nyitottak a kiterjeztésre, de zártak a nódosításra
- Liskov substitution principle: az objektumok helyettesíthetőek altípusaik példányával
- Interface segregation principle: egy általános interfész helyett több kliens specifikus interfész
- Dependency inversion principle: az absztrakciótól függünk, nem a konkretizációtól
A tervezés során mintákra hagyatkozunk, a szoftver teljes architektúráját definiáló mintákat nevezzük architektúrális mintáknak (architectural pattern), az architektúra alkalmazásának módját, az egyes komponensek összekapcsolását segítik elő a tervminták (design pattern)
Single responsibility principle
Számos, jól megkülönböztethető felelősségi kör található az alkalmazásokban:
- perzisztencia, üzleti logika, vizualizáció, felhasználói interakció.
- adatformázás, konverzió, validáció
Elősegíti a programegységek közötti laza csatolást, viszont túlzott használata feltöredezheti a programszerkezetet
Open/closed principle
A programegységek nyitottak a kiterjesztésre, de zártak a módosításra.
- A programszerkezetet úgy kell megvalósítanunk, hogy új funkcionalitás bevezetése ne a jelenlegi programegységek átírását igényelje, sokkal inkább újabb programegységek hozzáadását.
A SRP és az OCP kiegészítik egymást, mivel az új funkcionalitás egy új felelősség bevezetését jelenti, így általában egyszerre szegjük meg a két elvet.
Liskov substitution principle
Bármely típus (osztály) példánya helyettesíthető altípusának egy példányával úgy, hogy közben a program tulajdonságai (funkcionális és nem funkcionális követelményei) nem változnak.
Megszorításokat tesz a szignatúrára
- elvárja a paraméterek kontravarianciáját, a visszatérési értékek kovarianciáját
- tiltja a kivételek típusának bővítését
Megszorításokat tesz a viselkedésre
- elvárja az invariánsok megtartását
- tiltja az előfeltételek erősítését, az utófeltételek gyengítését.
Interface segregation principle
Egy kliensnek sem szabad olyan interfésztől függenie, amelynek műveleteit nem használja.
Ezért az összetett interfészeket célszerű több, kliens specifikus interfészre felbontani, így azok csak a szükséges műveleteiket érhetik el.
Dependency inversion principle
Magasabb szintű programegységek nem függhetnek alacsonyabb szintű programegységtől, hanem mindkettőnek az absztrakciótól kell függenie.
Az absztrakció nem függhet a részletektől, a részletek függenek az absztrakciótól.
A megvalósítása számára a következőket jelenti:
- az osztály mezői a konkrét osztályok helyett az absztrakció példányát tartalmazzák
- a konkrét osztályok az absztrakció segítségével lépnek kapcsolatba egymással, a konkrét osztályok szükség esetén átalakíthatóak
- szigorú esetben konkrét osztályokból nem örökölhetünk, és már megvalósított metódust nem definiálunk felül
- az osztályok példányosítását egy külső programegység végzi, pl. függőség befecskendezés, gyártó művelet, vagy absztrakt gyártó segítségével.
Tervezési minták
Dependency injection
A végrehajtás során egy réteg (kliens) által használt réteg (szolgáltatás) egy, az adott körülmények függvényében alkalmazható megvalósítása kerül alkalmazásra.
Erről általában nem dönthet sem a kliens, sem a szolgáltatás, mivel ők nem ismerik a körülményeket, ezért egy külső komponensnek kell megadnia a megvalósítást.
A kliens (client) számára a megfelelő szolgáltatás (service) külső átadásának egy lehetséges módja a függőség befecskendezés.
A kliens biztosít egy átadási pontot a szolgáltatás interfésze számára, ahol a végrehajtás során a szolgáltatás megvalósítását adhatjuk át.
Az átadási pont függvényében 3 típusa lehet: konstruktor, metódus (beállító művelet), interfész (a kliens megvalósítja a beállító műveletet)
A befecskendést végző komponens (injector) lehet egy felsőbb réteg, vagy a rétegződéstől független környezeti elem.
Factory
A gyártó (factory) egy olyan objektum, amelynek feladata más objektumok előállítása műveletek segítségével.
Lehetőséget ad objektumok példányosításánál:
- a konkrét példányosítandó típus kiválasztására
- a példányosítással járó kódismétlés kiküszöbölésére
- fel nem fedhető környezeti információk használatára
- a példányosítási lehetőségek bővítésére
Lehetővé teszi a konkrét legyártott típus cseréjét a gyártó művelet és az absztrakt gyártó segítségével.
Amennyiben több, kapcsolatban álló típus konkrét megvalósítását szabályoznánk, az absztrakt gyártó (abstractfactory) tervmintát használhatjuk.
Command
A háromrétegű architektúrában a nézet továbbra is összetett, de a tevékenységek leválaszthatóak, a parancs (command) tervminta lehetőséget ad egy művelet kiemelésére egy külön objektumba.
- a végrehajtandó tevékenység (Action) formája, paraméterezése tetszőleges lehet, ezért nem lehet egyazon módon különböző környezetekben kezelni
- a parancs (Command) egy egységes formát biztosít egy tevékenység végrehajtására (Execute), a konkrét parancs (ConcreteCommand) pedig végrehajtja a tevékenységet
- a kezdeményező (Invoker) csupán a parancsot látja, így a tényleges végrehajtás előle rejtett marad.
Observer
A figyelő (observer) tervminta célja összefüggőség megadása az objektumok között, hogy egyik állapotváltozása esetén a többiek értesítve lesznek
- a figyelő (Observer) ehhez biztosítja a változás jelzésének metódusát (Update)
- a megfigyelt objektumok (Subject) az értékeikben tett változtatásokat jelzik a felügyelőknek (Notify)
- egy objektumot több figyelő is nyomon kísérhet
Az alapelvek hatása az architektúrára
Az alapelveket a tervezés minden szintjén, így az architektúra szintjén is célszerű betartani.
- Monolítikus architektúra esetén egyáltalán nem érvényesülnek az alapelvek.
- Modell-nézet architektúra esetén is túl nagy a komponensek
felelőssége:
- A modell felel az üzleti logikáért (programállapot kezelése, algoritmusok végrehajtása), valamint az adatok hosszú távú tárolásáért (mentés, betöltés)
- A nézet felel az adatok megjelenítéséért, valamint a vezérlésért (felhasználó tevékenység fogadása és feldolgozása)
Háromrétegű architektúra
A modell felbontásával jutunk a háromrétegű (3-tier) architektúrához, amelyben elkülönül:
- a nézet (presentation, view, display)
- a modell (logic, business logic, application, domain), amely tartalmazza az állapotkezelést, valamint az algoritmusok
- a perzisztencia, vagy adatelérés (data, dataaccess, persistence), amely biztosítja a hosszú távú adattárolás (pl. fájl, adatbázis) kezelését
Adatok állapotai
A háromrétegű alkalmazásokban az adatok három állapotban jelennek meg.
- megjelenített állapot (display state): a felhasználói felületen megjelenő tartalomként, amelyet a felhasználó módosíthat
- munkafolyamat állapot (session state): a memóriában, amely mindaddig elérhető, amíg a program és felhasználója aktív
- rekord állapot (record state): az adat hosszú távon megőrzött állapota az adattárban (perzisztenciában)
Adatkötés
Az állapotok között a programnak transzformációkat és szinkronizációt kell végeznie
- a nézet állapot és a munkafolyamat állapot sokszor megegyezik, a munkafolyamat és a perzisztens állapot (adattárolási forma) sokszor különbözik, ezért külön adatelérést kell biztosítanunk
- a munkafolyamat és a perzisztens állapot között általában ritkán történik szinkronizáció (pl. program indítása, leállítása)
- a nézet állapot és a munkafolyamat állapotot rendszerint állandó szinkronban kell tartanunk, ezt lehetőség szerint automatizálnunk kell
Az állapotok automatikus szinkronizálását adatkötés (data binding) segítségével érhetjük el, amely két lehetséges módon biztosíthatja a szinkronizációt.
Az adatkötésért egy olyan adatkötés objektum (Binding) felel, amelyet a megjelenített állapot tárolója (nézet) és a munkafolyamat állapot tárolója (modell) közé helyezünk
- az adatkötés ismeri mind a nézetet mind a modellt, ezért lehívhatja az értékeket (GetState), és elvégezheti a módosítást (SetState)
- elvégezheti az átalakítást (Convert) a munkafolyamat állapot és a nézet állapot között
- általában minden szinkronizálandó adathoz külön adatkötés tartozik, többszörös előfordulás esetén akár előfordulásonként is tartozhat adatkötés
A nézet ismerheti az adatkötést, így közvetlenül kezdeményezheti a módosítást az adatkötésen keresztül.
A modell azonban nem ismerheti az adatkötést, sem a vezérlőt, csak jelzést tud adni az állapot változásáról, ennek feldolgozását figyelő tervminta segítségével végezzük.
Az adatkötés megvalósítja a figyelőt, ám mivel közvetlenül nem ismerheti a modell, a változás jelzése esemény formájában történik (NotifyStateChanged).
Mivel a jelzésnek egységes formában kell megvalósulnia, célszerű, ha minden modell egy közös, az esemény kiváltását biztosító interfészt valósít meg (Subject).
Előnyei:
- a megjelenített és a munkafolyamat állapot mindig szinkronban tartható, automatikusan
Hátrányai:
- a szinkronizáció erőforrásigényes, sokszor feleslegesen hajtódik végre
- a rendszernek ügyelnie kell a körkörös módosítások elkerülésére
- a modellnek biztosítania kell a megfelelő interfész megvalósítását, és az esemény kiváltását a szükséges pontokon
MVC architektúra
A megjelenítés és a vezérlés leválasztását biztosítja a Model-View-Controller (MVC) architektúra, amelyben
- a vezérlő (Controller) fogadja közvetlenül a kérést a felhasználótól, feldolgozza azt (a modell segítségével), majd előállítja a megfelelő (új) nézetet
- a nézet (View) a felület (jórészt deklaratív) meghatározása, amely megjeleníti, illetve átalakítja az adatokat
- lehívás alapú (pull-based): ismeri a modellt, az adatokat a modelltől kéri le
- betöltés alapú (push-based): a vezérlő adja át számára az adatokat
A modell mellett perzisztációval is bővíthető a szerkezet.
Az MVC architektúra különösen webes környezetben népszerű, ahol könnyen leválasztható a nézet (HTML) a vezérléstől (URL) Általában egy vezérlőhöz több nézet is tartozik.
Előnyei:
- a nézetnek nem kell foglalkoznia az események feldolgozásával, mivel azokat közvetlenül a vezérlő kapja meg
Hátrányai:
- a nézet továbbra is felel az adatok megfelelő átalakításáért, tehát tartalmaz valamennyi logikát