Modellezzük le azt a helyzetet, amikor egy szupermarket hírlevelére a vásárlók feliratkoznak azért, hogy ha új termék jelenik meg az áruház kínálatában, akkor arról értesítést kapjanak. Természetesen nem minden árucikk érdekel minden vásárlót. A vásárlóknak eltérő igényeik vannak, és ráadásul hiába érdekli a termék, egy bizonyos árszint felett nem tudja vagy nem akarja megvásárolni. Viszont, ha olyan termékről kap tájékoztatást, amely ára számára megfelelő, akkor azt meg is veszi.
Az ilyen helyzetek programbeli modellezésére leggyakrabban egy jól bevált sablont, az úgynevezett megfigyelő tervezési mintát (observer design pattern) szokták alkalmazni, amelyet – épp a fenti feladatban vázolt helyzet alapján – publikáló-feliratkozó mintának (publisher-subscriber pattern) is szokták hívni.
Ennek logikai lényege, hogy a publikáló objektum (jelen példánkban a szupermarket) nyilvántartást vezet egy kontérnerobjektumban (pl. halmaz) a feliratkozókról, amennyiben azok ezt kezdeményezik. Természetesen le is lehet iratkozni, azaz lehet az áruháztól kérni, hogy töröljön a hírlevél címzettjei közül. Amennyiben a publikáló objektum állapota, vagyis valamelyik adatattribútumának értéke megváltozik, akkor egy saját, belső metódusát meghívja, amely a nyilvántartásából sorban kiveszi a feliratkozókat, és egy, adott nevű metódust (pl. update) meghív minden egyes feliratkozó objektumra. Ez a metódushívás fogja tudatni az adott feliratkozóval, hogy a publikáló objektum állapotában valami változás történt. Jelen példában azt, hogy egy új termék érkezett. A feliratkozott potenciális vásárlóknak az új termék nevét és árát kell megtudniuk. Ehhez az információhoz kell valahogy hozzájutniuk.
A releváns információhoz több módon is hozzájuthat a feliratkozó.
Egyik lehetőség, hogy a publikáló áruház az értesítéskor az update metódusnak argumentumként átadja az új termék nevét és árát. Ezt Push változatnak nevezik, mert a publikáló mintegy „odalöki” a feliratkozónak az új információkat.
Másik lehetőség, hogy a vásárló a feliratkozáskor egyúttal megkapja a szupermarket elérhetőségét is, és amikor az áruháztól értesítést kap (mindenféle kiegészítő infó, azaz argumentum nélkül), akkor az áruháztól maga kéri le az adott attribútumának aktuális értékét. Ezt Pull változatnak hívják, mert a feliratkozó mintegy „kihúzza” a számára érdekes értéket a publikáló objektumból.
Akármelyik változatról is van szó, az értesítés és annak sikeres fogadása csak akkor tud megvalósulni, ha az értesítéskor a feliratkozókra meghívott metódus ugyanolyan fejléccel implementálva van mindegyik feliratkozó objektumban. Ezért ehhez célszerű egy absztrakt osztályt definiálni (pl. Subscriber néven), amelyet minden feliratkozó (Customer osztály példányai) örököl és az adott körülményeinek megfelelően implementál.
A publikáló objektumnak jól meghatározott működésű általános metódusai vannak (feliratkozók nyilvántartásba vétele, illetve abból való törlése, valamint a nyilvántartott feliratkozók értesítése), amelyek valójában minden publikálóra azonosak. Ezért ezt célszerű kiszervezni egy osztályba, amelyet aztán minden publikáló osztály (jelen esetben az áruházat modellező Supermarket osztály) örököl.
A fenti elvek alapján egy külön modulban definiáltuk az általános publikálást megvalósító Publisher osztályt és a Subscriber absztrakt osztályt, amelynek egy absztrakt metódusa az update(). Ezt láthatjuk 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 |
# modul publisher_push 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(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(*args, **kwargs) |
Egy másik modulban követhetjük nyomon a Supermarket és Customer osztályok definícióit, amelyek működésének megértéséhez a kommentek segítséget nyújtanak. Mivel ezen osztályoknak kell örökölnie a Publisher, illetve a Subscriber osztályt, ezért az ezeket tartalmazó modult beimportáljuk.
|
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 |
from publisher_push import Publisher, Subscriber from collections import namedtuple # A terméket nevével és árával modellező osztály Product = namedtuple('Product', 'name price') class Supermarket(Publisher): def __init__(self): super().__init__() # Az áruház kezdő árukészlete. Ezekre nincs hirdetés. self._products = [Product('liszt',360), Product('cukor', 270)] self._new_product = None @property def new_product(self): return self._new_product @new_product.setter def new_product(self, product: Product): self._new_product = product self._products.append(product) # Új termék érkezéséről értesítés küldés a feliratkozóknak megadva a termék nevét és árát. self.notify_subscribers(product.name, product.price) def sell(self, product_name, sell_price): """Az áruház a megadott nevű terméket a megadott áron eladja, feltéve, hogy ilyen néven és áron van termék készleten. Az eladott termék lesz a visszatérési érték. Ha nem lehet eladni, akkor None.""" try: return self._products.pop(self._products.index(Product(product_name, sell_price))) except ValueError: return None class Customer(Subscriber): def __init__(self, name: str, **demanded_products_with_max_price): self._name = name # A vásárló termékigényei a még elfogadható árakkal. self._demanded_products_with_max_price = demanded_products_with_max_price def update(self, product_name: str, price:float): if product_name in self._demanded_products_with_max_price: if price <= self._demanded_products_with_max_price.get(product_name): self.buy(product_name, price) else: print(f'{self._name}: érdekel a {product_name}, de számomra még túl drága.') def buy(self, product_name: str, buy_price: float): print(f'{self._name}: {product_name} megvéve {buy_price}Ft áron.') |
A következő kódsorban látható az áruház, valamint két vásárló példányának létrehozása, majd a vásárlók feliratkozása. Ezt követően új termékek jönnek a szupermarketbe, amit a new_product attribútumnak történő értékadás mutat. Minden egyes új termék megjelenésekor a vásárlók értesítést kapnak. Ha az adott vásárló termékigénye között szerepel az új termék, és annak ára nem magasabb, mint amit az adott vevő azért fizetni hajlandó, akkor megveszi, amit egy kiírás jelez.
|
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 |
# TESZT supermarket = Supermarket() customer1 = Customer('Éva', alma=380, szappan=450, párna=8000, kenyér=1000, bor=1900) customer2 = Customer('Ádám', kolbász=5000, borotva=1500, sör=600, kenyér=900, bor=2000) # A vevők feliratkoznak a szupermarket hírlevelére. for customer in (customer1, customer2): supermarket.add_subscriber(customer) # A szupermarket új termékeket szerez be, és adott áron kínálja. # Erről rögtön tájékoztatja a hírlevélre feliratkozókat. supermarket.new_product = Product('kenyér', 950) supermarket.new_product = Product('sör', 590) supermarket.new_product = Product('párna', 8100) # Az egyik vevő leiratkozik a hírlevélről, így újabb, számára # kedvező ajánlatokról nem értesül és ezért nem is vásárol. supermarket.remove_subscriber(customer1) supermarket.new_product = Product('bor', 1800) # Eredmények: # Éva: kenyér megvéve 950Ft áron. # Ádám: érdekel a kenyér, de számomra még túl drága. # Ádám: sör megvéve 590Ft áron. # Éva: érdekel a párna, de számomra még túl drága. # Ádám: bor megvéve 1800Ft áron. |
Ebben programban a megfigyelő tervezési minta Push változatát láthatjuk, minthogy a Supermarket mint publikáló a terméknév és ár adatokat az értesítéskor átadja a Customer példányok update() metódusának.
Vegyük észre, hogy a termékvásárlást megvalósító buy() metódusban csak egy tájékoztató üzenet jelzi a vásárlást. Ez egy kis csúsztatás, mivel valójában nem történik vásárlás. Mégpedig azért nem, mert a vevő csak a terméknévről és árról kapott tájékoztatást, de azt az információt nem kapta meg itt, hogy kitől lehet megvenni. Más szóval nem kapta meg a supermarket objektum referenciáját.
Ebből is látható, hogy a Push változat ebben a formában nem az ilyen szituációkhoz illik, hanem olyanokhoz, ahol nincs szükség az értesítést küldő kilétére.
Mindazonáltal a példabeli tényleges vásárlás megvalósítható a Push változattal is, de egy kis módosítással, amit a következő bejegyzésben nézünk meg.
E bejegyzés témájához a Python tudásépítés lépésről lépésre című e-könyv következő részei kapcsolódnak: Az „Öröklődés” fejezet, valamint a „Készétel fogyasztás a szabványos könyvtár moduljainak használata” fejezet „Absztrakt osztályok” alfejezete.