Az előző néhány bejegyzésben a megfigyelő tervezési minta (observer design pattern) alkalmazásával és megvalósítási változataival foglalkoztunk. Láthattuk a push és pull változatok közötti elvi különbséget, majd pedig az üzenetküldés feladatát kiszerveztük egy külön objektumba. Legutóbb megnéztük, hogy öröklés helyett hogyan lehet dekorátorokkal biztosítani a publikáló és feliratkozó objektumok azon képességeit, hogy a publikáló, vagy más néven a megfigyelt objektum, állapotváltozásakor a megfelelő üzenetet el lehessen küldeni, és azt a feliratkozó vagy más néven megfigyelő objektumok fogadni tudják.
Most visszatérünk a megfigyelő tervezési minta push változatát alkalmazó korábbi áruházas példánkhoz, ahol az áruházat modellező Supermarket osztály a Publisher osztályt, a vásárlókat reprezentáló Customer osztály a Subscriber osztályt örökli. A termékeket a Product osztály, az új termék érkezésekor kiküldött értesítést (promóciós hírleveleket) a PromotionMessage osztály modellezi.
Most az értesítésküldést fogjuk az eddigiekhez képest máshogyan megvalósítani. Mégpedig attribútumleíró (attribute descriptor), vagy röviden csak leíró (descriptor) alkalmazásával.
A leíró egy olyan objektum, amely rendelkezik a __get__, __set__, __delete__ speciális metódusok közül legalább eggyel. /Ezek mellett opcionálisan még a __set_name__ metódus is definiált lehet az adott leíróra./
A leírók működésével kapcsolatos egyik fontos kitétel, hogy a bennük megvalósult __get__, __set__, __delete__ metódusok csak akkor érvényesülnek, ha az ezeket definiáló leíró osztály példánya szerepel egy osztályban. Ezt az osztályt, amely tehát a leírópéldányt tartalmazza tulajdonos (owner) osztálynak nevezik. Még pontosabban fogalmazva, a leírópéldánynak vagy a tulajdonos osztály __dict__ konténerében, vagy a tulajdonos valamely ősének __dict__ konténerében kell szerepelnie. A lényeg, hogy a leíróobjektum osztályattribútum kell, hogy legyen.
Ha így járunk el, akkor a normál értékkinyerési, értékadási és törlési folyamathoz képest más fog a háttérben lezajlani. Ha az osztályattribútum leíró, akkor egy attribútumérték meghatározásakor az interpreter megvizsgálja, hogy az adott attribútumnév által hivatkozott objektum leíró-e, vagyis rendelkezik-e a __get__, __set__, __delete__ metódusok bármelyikével. Ha nem, akkor visszaadja magát a hivatkozott objektumot. Ha viszont igen, akkor a kezdeményezett attribútumműveletnek (értékkinyerés, értékadás vagy törlés) megfelelően lefuttatja a leíróban definiált __get__, __set__ vagy __delete__ metódust.
A mintapéldánkhoz ezt, azaz konkrétan az értékadáskor lefutó __set__ metódust fogjuk kihasználni.
Gondoljuk át mit is szeretnék! Azt, hogy ha a Supermarket példány, vagyis az áruház new_product attribútuma új értéket kap (új termék érkezik), akkor az összes feliratkozó vásárló kapjon értesítést. Ezt eddig úgy valósítottuk meg, hogy a new_product a Supermarket osztályban tulajdonságként (property) volt definálva, és annak setter metódusában lett meghívva a vevők értesítését végző notify_subscribers() metódus. Tehát, ha a new_product új értéket kapott, akkor ez automatikusan lefutott.
Ugyanezt úgy is meg tudjuk valósítani, ha a Product osztályt leíróvá tesszük olyan módon, hogy definiáljuk benne a __set__ metódust, valamint a new_product a Supermarket osztályban – tulajdonság helyett – osztályattribútumként szerepel egy Product példánnyal mint értékkel.
Ebben az esetben a vevők értesítése a __set__ metódusban történhet meg a notify_subscribers() meghívásával, hiszen, ha a new_product egy új Product példányt kap értékként, akkor az interpreter érzékeli, hogy a Product egy attribútumleíró, és automatikusan meghívja a benne definiált __set__ metódust.
Alább láthajuk mindazon osztályokat, amelyeket nem érint az újfajta üzenetküldési megoldás. A Customer osztály kódja a helykímélés miatt nincs feltüntetve, az a korábbi „Értesítés küldés állapotváltozásról üzenetobjektum átadásával – megfigyelő minta, push változat” című bejegyzésben látható itt.
|
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 |
from __future__ import annotations from collections import namedtuple 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: """A publikáló osztály ezt örökli, hogy az állapotváltozásról minden feliratkozó értesítést kaphasson.""" 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) # A kiküldött promóciós üzenetet (pl. hírlevél, újság) modellező osztály. PromotionMessage = namedtuple('PromotionMessage', 'sender product_name product_price') class Customer(Subscriber):... |
A következő kódsorokban a változással érintett Product és Supermarket osztályokat mutatjuk.
|
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 |
# A terméket nevével és árával modellező osztály, ami egyben attribútumleíró is. class Product: def __init__(self, name, price): self.name = name self.price = price def __repr__(self): return '{}({}, {})'.format(type(self).__name__, self.name, self.price) def __set__(self, owner_instance: Supermarket, product: Product): # A product példányt, az áruházpéldány new_product nevű attribútumához értékként hozzárendeljük. owner_instance.__dict__['new_product'] = product # Az új termék hozzáadása az áruház termékkínálatához. owner_instance._products.append(product) # Új termék érkezéséről tájékoztató üzenet küldése a feliratkozóknak. A hírlevélben szerepel a # az áruház elérhetősége (referenciája), valamint a termék neve és ára. owner_instance.notify_subscribers(PromotionMessage(owner_instance, product.name, product.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), Product('bor', 1500)] # Azt az attribútumot, amely értékének változásáról értesíteni akarjuk a feliratkozókat # attribútumleíróval hozzuk létre az osztályban. Ehhez az adott attribútumot osztályattribútumként # definiáljuk és értékként egy leíró példányt rendelünk hozzá. new_product = Product('', 0) def sell(self, product_name, sell_price):... |
A Supermarket esetében látható, hogy a new_product most nem tulajdonságként, hanem osztályattribútumként van definiálva. /A jobb áttekinthetőség biztosítása érdekében az osztály egyéb kódjai nincsenek megjelenítve, azok a fentebb említett bejegyzésben megtalálhatók/
A Product osztály a korábbihoz képest a __set__ metódussal lett kibővítve. Ennek törzsében a fentebb kifejtett logika jelenik meg néhány kódsorban. Az itt olvasható kommentek segítik a megértést.
A működés tesztprogramja látható alább a megjelenő eredménykiírásokkal együtt.
|
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 |
# TESZT supermarket = Supermarket() customers = (Customer('Éva', alma=380, szappan=450, párna=8000, kenyér=1000, bor=1900), 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 customers: 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 felirtakozókat. for product in (Product('kenyér', 950), Product('sör', 590), Product('párna', 8100), Product('bor', 1800)): supermarket.new_product = product # 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(customers[0]) supermarket.new_product = Product('párna', 7000) for customer in customers: print(f'{customer.name} megvásárolt termékei:', *customer.purchased_products) # Eredmények: # Ádám: érdekel a kenyér, de számomra még túl drága. # Éva: kenyér megvéve 950Ft áron. # Á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. # Éva: bor megvéve 1800Ft áron. # Éva megvásárolt termékei: Product(bor, 1800) Product(kenyér, 950) # Ádám megvásárolt termékei: Product(bor, 1800) Product(sör, 590) |
E bejegyzés az attribútumleírók gyakorlati alkalmazására mutatott egy példát. Az ebben definiált leíró nagyon egyszerű felépítésű, viszont a leírók ennél sokrétűbbek. A leírókról, azok működéséről sokkal részletesebben olvashatunk a Python tudásépítés lépésről lépésre című e-könyv „Attribútumleírók és használatuk” című fejezetben, ahol más alkalmazási példákat is láthatunk.