Az előző három bejegyzésben azt a helyzetet modelleztük le programban, 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. A vásárlókat természetesen nem minden árucikk érdekeli, de ha igényük van rá, akkor is csak egy bizonyos árszint alatt hajlandóak az adott terméket megvenni.
Az előzőekben hallgatólagosan azt feltételeztük, hogy több vásárló és csak egyetlen áruház van, tehát minden vásárló ehhez az egy áruházhoz iratkozik fel. A programmodellt a megfigyelő (Observer) vagy más néven publikáló-feliratkozó (Publisher-Subscriber) tervezési minta alapján építettük fel. Ez két alapváltozatban (Push és Pull) is be lett bemutatva.
A feladatot most kibővítjük azzal az új igénnyel, hogy ne csak egy, hanem tetszőleges számú áruház lehessen, és a vásárlók bármelyikre, vagy akár az összes áruház hírlevelére feliratkozhatnak.
Ezt a feladatot is elvben meg lehet oldani az eddigi módszerrel. Azonban sok vásárló és sok áruház esetében a regisztrálások és leiratkozások kezelése nehézkessé válik. Ezért ezt a feladatot célszerű kiszervezni egy külön objektumba, aminek az a feladata, hogy a hozzá beregisztrált áruházak neki megküldött hírlevelét minden olyan potenciális vásárlónak kiküldi, akik az adott áruház hírlevelére kíváncsiak és azt jelezték. E feladatkörből adódóan ezt az új objektumot nevezhetjük üzenettovábbító központnak (messaging center).
Ha az előző bejegyzésekből kiinduló alapnak a Push változatot vesszük, akkor az új igény kielégítéséhez nem kell sokat változtatni a kódon. Alább mutatjuk azon kódokat, amelyek nem változnak a korábbihoz képest.
|
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 |
from __future__ import annotations from collections import namedtuple from abc import ABC, abstractmethod 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) PromotionMessage = namedtuple('PromotionMessage', 'sender product_name product_price') class Subscriber(ABC): @abstractmethod def update(self, message: PromotionMessage): raise NotImplementedError class Customer(Subscriber): def __init__(self, name: str, **demanded_products_with_max_price): self.name = name self._demanded_products_with_max_price = demanded_products_with_max_price self.purchased_products = set() def update(self, message: PromotionMessage): """Termékpromóciós tájékoztató fogadása és a benne foglalt termék megvásárlása, ha az igenyeknek megfelel.""" if message.product_name in self._demanded_products_with_max_price: if message.product_price <= self._demanded_products_with_max_price.get(message.product_name): self.buy(message.sender, message.product_name, message.product_price) else: print(f'{self.name}: érdekel a {message.product_name}, de számomra még túl drága.') def buy(self, shop: Supermarket, product_name: str, buy_price: float): """A megadott nevű terméket megvásárolja az adott áron, az adott áruháztól, majd a megvásárolt termékek közé teszi.""" product = shop.sell(product_name, buy_price) if product is not None: self.purchased_products.add(product) print(f'{self.name}: {product.name} megvéve {buy_price}Ft áron.') |
Itt pedig az üzenettovábbító központ objektum MessagingCenter nevű osztálydefinícióját láthatjuk:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class MessagingCenter: def __init__(self): # Az üzenetküldők és azokhoz feliratkozott üzenetfogadók nyilvántartása. self.publishers_subscribers = dict() def add_publisher(self, publisher): """A megadott üzenetküldőt felveszi a nyilvántartásba.""" self.publishers_subscribers.setdefault(publisher, set()) def add_subscriber(self, subscriber: Subscriber, *publishers): """A megadott feliratkozót (üzenetfogadót) hozzárendeli a megadott üzenetküldőkhöz, ha azok már szerepelnek a nyilvántartásban. """ for publisher in publishers: self.publishers_subscribers.get(publisher).add(subscriber) def notify_subscribers(self, message: PromotionMessage): """A kapott üzenetben megadott üzenetküldőhöz feliratkozottakhoz elküldi az üzenetet.""" for subscriber in self.publishers_subscribers.get(message.sender): subscriber.update(message) |
Ennek felépítése és metódusai nagyon hasonlóak a korábban látott Publisher osztályéhoz, de tartalmi eltérésekkel. Most nem csupán a vásárlókat mint üzenetfogadókat kell nyilvántartani, hanem az üzenetküldőket is, ráadásul ezekhez eltérő vásárlók tartozhatnak. Ezért ezt egy olyan szótárban tartjuk nyilván, amelynek kulcsai az üzenetküldő áruházak, a kulcshoz tartozó értékek pedig konténerek (jelen esetben halmaz), amelyek az egyes feliratkozó vásárlók referenciáit tárolják. Értesítési kéréskor a MessagingCenter a kapott üzenetből kinyeri a küldőt, és annak ismeretében kikéri a szótárból az ehhez tartozó konténert. Ha ez megvan, akkor az abban tárolt összes feliratkozónak kiküldi az üzenetet jelentő hírlevelet. /Az áttekinthetőség kedvvért a metódusokon belül a szükséges ellenőrző kódokat mellőztük/
A MessagingCenter ismeretében már csak egy feladat marad, a Supermarket osztály módosítása ahhoz, hogy az új rendszer szerint tudjon működni és küldeni a hírleveleit az üzenettovábbító központnak. Ennek definíciója:
|
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 |
class Supermarket: def __init__(self): super().__init__() self._products = [Product('liszt', 360), Product('cukor', 270), Product('bor', 1500)] self._new_product = None # Az üzenettovábbító központ referenciáját tároló attribútum. self._post_office: MessagingCenter = None def register_in_messaging_center(self, messaging_center: MessagingCenter): """Beregisztrálja az áruházat a megadott üzenettovábbító központba, és eltárolja annak referenciáját.""" self._post_office = messaging_center messaging_center.add_publisher(self) @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 tájékoztató üzenet küldés a feliratkozóknak az ismert üzenetközponton keresztül. if self._post_office: self._post_office.notify_subscribers( PromotionMessage(sender=self, product_name=product.name, product_price=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 legfeljebb ilyen áron van termék készleten. Az eladott termék lesz a visszatérési érték. Ha nem lehet eladni, akkor None. Az eladott termék kikerül az áruház termékraktárából. """ for prod in self._products: if prod.name == product_name and prod.price <= sell_price: product_to_sell = prod self._products.remove(product_to_sell) product_to_sell.price = sell_price return product_to_sell |
A korábbi bejegyzésekben szereplő osztálydefinícióhoz képesti új kódokat a 8-14 és 25-28 sorok tartalmazzák.
Láthatjuk, hogy egy új privát attribútumot vettünk fel az üzenettovábbító központ objektum nyilvántartásához. Az attribútumhoz értéket egy új, register_in_messaging_center nevű metódussal lehet adni, amennyiben az áruház szeretné igénybe venni az üzenettovábbító központ szolgáltatását. Ezek eddig kódkiterjesztések voltak. Az egyetlen valódi kódmódosítást a new_product() metódusban kell megtenni, mert most nem maga az áruház küldi ki a feliratkozóknak a hírlevelet, hanem erre a MessagingCenter példányt kell megkérni.
Vegyük észre, hogy az áruházat modellező Supermarket osztálynak most nem kell örökölnie a korábban látott Publisher osztályt, hiszen most már nem ő a felelős a vásárlók értesítéséért, ezért ilyen képességre nincs szüksége.
A rendszer működését bemutató tesztkódsorokat és azok eredményét alább követhetjük:
|
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 |
# TESZT # Az üzenettovábbító létrehozása. messaging_center = MessagingCenter() # Az áruházak létrehozása, majd pedig beregisztrálása az üzenetközpontba. shops = [Supermarket(), Supermarket()] for shop in shops: shop.register_in_messaging_center(messaging_center) # A vásárlók létrehozása megadva a termékigényeiket és azok max árát, amit hajlandók még kifizetni. 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 szupermarketek hírlevelére az üzenetközponton keresztül. for customer in customers: messaging_center.add_subscriber(customer, *shops) # A szupermarketek új termékeket szereznek be, és adott áron kínálják. Erről rögtön tájékoztatják a # felirtakozókat, akik megveszik azokat, ha az igényeikkel összhangban vannak. for product in (Product('kenyér', 950), Product('sör', 590)): shops[0].new_product = product for product in (Product('párna', 8100), Product('bor', 1800)): shops[1].new_product = product # Az egyes vásárlók által vett termékek kiírása. 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(kenyér, 950) Product(bor, 1800) # Ádám megvásárolt termékei: Product(sör, 590) Product(bor, 1800) |
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 „Osztály vigyázz! – típuslétrehozás osztályokkal” fejezet, a „Készétel fogyasztás a szabványos könyvtár moduljainak használata” fejezet „Absztrakt osztályok” alfejezet, valamint a „Speciális konténer típusok” alfejezetén belül a „Mezőneves tuple – namedtuple” cím, az „Attribútumműveletek kontrollált végrehajtása” fejezeten belül a „Tulajdonságok létrehozása” és „Dekorátorral létrehozott tulajdonságok” alfejezetek.