Az előző néhány bejegyzésben azért jártuk körül részletesen a megfigyelő programtervezési mintát (observer design pattern), mert ezt a mintát lehet használni az MVC betűszóval jelölt programszerkezet megvalósítására is.
Az MVC a Model, View és Controller angol szavakból ered, és egy olyan programfelépítésre utal, aminek lényege, hogy a modellt, vagyis az adatfeldolgozást, annak szabályait és logikáját tartalmazó programrészt függetlenítse az adatok felhasználó számára történő megjelenítését végző programrészektől (View). A kettőt a vezérlő rész (controller) kapcsolja össze olyan módon, hogy fogadja a felhasználói kéréseket, a bemenő adatokat és azok alapján, illetve azokat használva kezdeményezi a modellnél az adatfeldolgozást vagy a modell bizonyos állapotának megváltozását.
Azáltal, hogy a modellt megvalósító és a megjelenítést, azaz a felhasználói interfészt képviselő programrészek függetlenek azzal az előnnyel jár, hogy a program így az igények változása tekintetében rugalmas lehet. Ha új fajta vagy kinézetű megjelenítés szükséges, akkor az úgy valósítható meg, hogy a modellhez nem kell nyúlni. Ez azt is jelenti, hogy a modell és a megjelenítést végző programrészek egymástól függetlenül fejleszthetők, ha a megjelenítendő adatok köre előre ismert.
Az MVC több módon is felépíthető. Miután a Controller fogadta a felhasználói kérést, a bemenő adatot, felkéri a Modellt az adatfeldolgozásra. Innentől viszont két eltérő alapváltozatot eredményezhet az, hogy hogyan kap egy megjelenítő (View) objektum értesítést arról, hogy módosítani kell az adatok felhasználó számára történő megjelenítésén:
- a Controller kapja vissza a Modelltől a megjelenítendő eredményt. Ezt követően a Conroller kéri a View objektumot vagy objektumokat, hogy jelenítésék meg a Modell által átadott eredményt az előírt módon.
- a Controller nem kapja meg a Modelltől az eredményt, hanem a Modell értesíti az egy vagy több View objektumot, hogy történt változás és ezt jelenítésék meg, vagy aktualizálják a felhasználó által látottakat.
A 2. esetben alkalmazható jól a megfigyelő minta. Ilyenkor a modell lesz a publikáló vagy más szóval a megfigyelt objektum, akihez a megjelenítő objektumok mint megfigyelők feliratkoznak. Ha a modellben állapotváltozás történik, akkor az értesítést küld a megfigyelőknek erről, akik ennek alapján aktualizálják a megjelenítést.
Az MVC architekturát általában grafikus felhasználói felülettel (GUI) történő megjelenítés esetén használják. De az elvek nem GUI esetén is alkalmazhatók, vagyis amikor a felhasználó számára az adatmegjelenítés a konzolon kiprintelt formában történik. Erre mutatunk egy példát most.
A példában egy autót, pontosabban annak gyorsítását vagy lassítását modellezzük úgy, hogy a Car nevű osztályban felveszünk egy speed nevű attribútumot, amely az autó mindenkor aktuális sebességét tartja nyilván km/h-ban. Az autó sebességét változtatni, azaz bizonyos értékkel gyorsítani vagy lassítani, az accelerate nevű metódussal lehet, amelynek argumentuma a kívánt sebességváltozás. Pozitív érték gyorsítást, negatív érték lassítást eredményez.
Azt szeretnénk, ha az autó aktuális sebességéről minden gyorsítás/lassítás után két formában is kapjunk tájékoztatást. Egyrészt lássuk egy egyszerű skálán valamilyen módon vizuálisan, másrészt szövegesen is olvashassuk az aktuális km/h sebességet, sőt kapjuk egy kiegészítő szöveges figyelmeztetést arról, ha egy előre beállítható megengedett maximális sebességet a pillanatnyi sebesség túllép.
A fenti igény azt jelenti, hogy minden egyes accelerate() hívás után két kiírás lesz a konzolon, amelyek mindegyike az aktuális sebességet jeleníti meg valamilyen formában. E két eltérő megjelenítésre két külön megjelenítő (View) objektumot definiálunk CarSpeedView1 és CarSpeedView2 osztálynevekkel. E két objektumot az autó sebességállapotának változásáról a megfigyelő mintát alkalmazva értesítjük. Ekkor a Car osztály lesz a megfigyelt objektum és így örökölni fog a korábbi bejegyzésekben is már látott Publisher osztályból. A CarSpeedView1 és CarSpeedView2 megfigyelők lesznek, amelyek a Subscriber absztrakt osztályt fogják örökölni, és így kötelezően implementálják az update_view() metódust, amely argumentumaként a megjelenítendő aktuális sebességet kapják meg a Car értesítésekor.
A Controller osztály feladata egyrészt, hogy a megfigyelőket a megfigyelt objektumhoz feliratkoztassa, másrészt fogadja a felhasználó által megadott gyorsítási értékeket, amivel a Car példány accelerate() metódusát meghívja.
A program teljes kódja és néhány megjelenő kiírás látható alább.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
from abc import ABC, abstractmethod class Subscriber(ABC): """A feliratkozók ezt öröklik, hogy mindegyik példányban egységesen ugyanaz a metódus szolgáljon az értesítés fogadására.""" @abstractmethod def update_view(self, *args, **kwargs): raise NotImplementedError class Publisher: def __init__(self): # A feliratkozókat nyilvántartó halmaz. self._subscribers = set() def add_subscriber(self, subscriber: Subscriber): """Az argumentumként megadott feliratkozót felveszi a nyilvántartásba.""" self._subscribers.add(subscriber) def remove_subscriber(self, subscriber: Subscriber): """Az argumentumként megadott feliratkozót törli a nyilvántartásból.""" self._subscribers.discard(subscriber) def notify_subscribers(self, *args, **kwargs): """ Minden feliratkozó értesítést kap, amelyben átadásra kerül a változásssal érintett érték. """ for subscriber in self._subscribers: subscriber.update_view(*args, **kwargs) # Modell class Car(Publisher): def __init__(self): super().__init__() self.speed: float = 0 def accelerate(self, delta_speed: float = 1): self.speed += delta_speed if self.speed < 0: self.speed = 0 self.notify_subscribers(self.speed) # Megjelenítő (View) objektum class CarSpeedView1(Subscriber): """Az aktuális sebességet vizuálisan jeleníti meg egy skálán.""" def show(self, speed): scale = list("." * 30) for i in range(0,len(scale)): if i % 6 == 0: scale[i] = '|' print(''.join(scale)+'|'+'\n '+int(speed / 5) * chr(0x2212)) def update_view(self, speed): self.show(speed) # Megjelenítő (View) objektum class CarSpeedView2(Subscriber): """Az aktuális sebességet szövegesen jeleníti meg, és figyelmezet akkor, ha a megadott sebességet túllépjük.""" def __init__(self): self.speed_limit = 130 def show(self, speed): print(f"A sebességed {speed} km/h") if speed > self.speed_limit: print(f'Túllépted a megengedett maximális {self.speed_limit} km/h sebességet!') def update_view(self, speed): self.show(speed) # Vezérlőobjektum (Conroller) class Controller: def __init__(self, model: Car, *views): self._model = model # A megfigyelő View példányok felvétele a megfigyelt objektum értesítési listájára. for view in views: self._model.add_subscriber(view) # A gyorsítás mértékére vonatkozó input adatok fogadása. self.receive_accelerate_inputs() def receive_accelerate_inputs(self): while True: s = input('Gyorsítás km/h? >>> ') # Ha nem adunk meg adatot, kilép a programból. if not s: break try: # A modell állapotváltozásának előidézése az input adattal. self._model.accelerate(float(s)) except ValueError: print("Érvénytelen érték.") # TESZT Controller(Car(), CarSpeedView1(), CarSpeedView2()) # Eredmények: # Gyorsítás km/h? >>> 90 # A sebességed 90.0 km/h # |.....|.....|.....|.....|.....| # −−−−−−−−−−−−−−−−−− # Gyorsítás km/h? >>> 50 # A sebességed 140.0 km/h # Túllépted a megengedett maximális 130 km/h sebességet! # |.....|.....|.....|.....|.....| # −−−−−−−−−−−−−−−−−−−−−−−−−−−− |
E program lényeges eleme a megfigyelő tervezési minta. Ha ennek működése esetleg e példából nem teljesen világos, akkor érdemes a korábbi „Értesítés küldés állapotváltozásról” kezdetű címmel írt bejegyzéseket elolvasni.