Modelet e dizajnit janë zgjidhje të paçmueshme për problemet e zakonshme në dizajnimin e softuerit. Ndërsa ato mund të mos ofrojnë zgjidhje të gatshme që mund të kopjohen dhe ngjiten drejtpërdrejt në kodin tuaj, ato shërbejnë si projekte të fuqishme që mund të personalizohen dhe përshtaten për të adresuar sfidat specifike të dizajnit.

Një nga avantazhet domethënëse të përdorimit të modeleve të dizajnit është qartësia që ato sjellin në bazën tuaj të kodeve. Duke përdorur modele të vendosura mirë, ju komunikoni në mënyrë efektive me zhvilluesit e tjerë problemin që po trajtoni dhe qasjen që po merrni. Shpesh thuhet se kodi shkruhet një herë, por lexohet shumë herë, duke theksuar rëndësinë e lexueshmërisë së kodit. Në procesin e projektimit të softuerit, vendosja e theksit më të madh në lexueshmërinë e kodit mbi lehtësinë e shkrimit është thelbësor. Përfshirja e modeleve të dizajnit të testuar nga beteja në bazën tuaj të kodeve është një mjet shumë efektiv për të përmirësuar lexueshmërinë e kodit.

Në këtë artikull, ne do të gërmojmë në tre modele të dallueshme të projektimit dhe do të ofrojmë shembuj të detajuar se si ato mund të aplikohen në Python. Duke kuptuar dhe shfrytëzuar këto modele, do të pajiseni me mjete të fuqishme për të përmirësuar strukturën, mirëmbajtjen dhe qartësinë e kodit tuaj.

Dekorator

Modeli i dizajnit të dekoruesit ju mundëson të mbështillni objektet ekzistuese në objekte të veçanta mbështjellëse, duke shtuar sjellje të reja pa modifikuar objektin origjinal.

Për të ilustruar këtë model, imagjinoni veten në një ditë të ftohtë dimri, duke ecur jashtë me një bluzë. Ndërsa filloni të ndjeni të ftohtin, vendosni të vishni një pulovër, duke vepruar si dekoruesi juaj i parë. Megjithatë, nëse jeni ende të ftohtë, mund të zgjidhni gjithashtu të mbështilleni me një xhaketë, duke shërbyer si një dekorues tjetër. Këto shtresa shtesë "zbukurojnë" sjelljen tuaj bazë për të veshur vetëm një bluzë. E rëndësishmja, ju keni fleksibilitetin për të hequr çdo veshje sa herë që nuk keni më nevojë për të, pa ndryshuar gjendjen tuaj themelore.

Duke përdorur modelin e dekoruesit, ju mund të përmirësoni në mënyrë dinamike objektet me funksionalitet të ri duke e mbajtur objektin bazë të pandryshuar. Ai ofron një qasje fleksibël dhe modulare për zgjerimin e sjelljes, njësoj si shtimi ose heqja e shtresave të veshjeve bazuar në nivelin tuaj të rehatisë.

Produkte të ndryshme të kafesë, mënyra e gabuar

Imagjinoni që keni për detyrë të zhvilloni një sistem të porositjes së kafesë për një kafene lokale. Sistemi duhet të mbështesë lloje të ndryshme kafeje me shije dhe mbushje të ndryshme. Le të shqyrtojmë variacionet e mëposhtme të kafesë:

  • Kafe e thjeshtë
  • Kafe me qumesht
  • Kafe me majë vanilje
  • Kafe me qumësht dhe sipër vanilje

Pa ditur për modelin e dizajnit të dekoruesit, thjesht mund të zbatoni të gjitha llojet e ndryshme të kafesë me mbushjet e tyre si një variabël më vete.

Në këtë qasje, ne fillojmë duke përcaktuar një ndërfaqe për të gjitha llojet e kafesë. Kjo ndërfaqe specifikon vetitë e zakonshme si emri i kafesë, çmimi, lista e përbërësve dhe nëse ajo është e përshtatshme për veganët. Ne përfshijmë gjithashtu një metodë order për të lejuar klientët të kërkojnë një kafe specifike dhe të specifikojnë sasinë.

from dataclasses import dataclass


@dataclass
class Coffee:
    name: str
    price: float
    ingredients: list[str]
    is_vegan: bool

    def order(self, amount: int) -> str:
        if amount == 1:
            return f"Ordered 1 '{self.name}' for the price of {self.price}€"
        return f"Ordered {amount} times a '{self.name}' for the total price of {self.price * amount:.2f}€"SIMPLE_COFFEE = Coffee(name="Simple Coffee", price=1.0, ingredients=["Coffee"], is_vegan=True)

Më pas, le të përcaktojmë katër llojet tona të ndryshme të kafesë:

SIMPLE_COFFEE = Coffee(name="Simple Coffee", price=1.0, ingredients=["Coffee"], is_vegan=True)
SIMPLE_COFFEE_WITH_MILK = Coffee(name="Simple Coffee + Milk", price=1.2, ingredients=["Coffee", "Milk"], is_vegan=False)
SIMPLE_COFFEE_WITH_VANILLA = Coffee(name="Simple Coffee + Vanilla", price=1.3, ingredients=["Coffee", "Vanilla"], is_vegan=True)
SIMPLE_COFFEE_WITH_MILK_AND_VANILLA = Coffee(name="Simple Coffee + Milk + Vanilla", price=1.5, ingredients=["Coffee", "Milk", "Vanilla"], is_vegan=False)

Tani, një klient mund të bëjë një porosi si më poshtë:

orders = [
    SIMPLE_COFFEE.order(amount=1),
    SIMPLE_COFFEE_WITH_MILK.order(amount=3),
    SIMPLE_COFFEE_WITH_MILK_AND_VANILLA.order(amount=2)
]
for order in orders:
    print(order)

# Output:
# Ordered 1 'Simple Coffee' for the price of 1.0€
# Ordered 3 times a 'Simple Coffee + Milk' for the total price of 3.60€
# Ordered 2 times a 'Simple Coffee + Milk + Vanilla topping' for the total price of 3.00€

Do të shihni shpejt se si ky kod do të rritet në mënyrë eksponenciale kur lloje të reja kafeje dhe më shumë kombinime me lloje të ndryshme mbushjesh dhe shijesh. Në vend që të ndjekim këtë spirale të errët të kodit të pafund, le të eksplorojmë se si mund të na ndihmojnë dekoruesit këtu.

Produkte të ndryshme kafeje, mënyra e duhur

Siç e diskutuam më herët, modeli i dekoruesit na lejon të mbështjellim një objekt të thjeshtë kafeje me bazë me funksionalitet shtesë për të krijuar produktin e dëshiruar përfundimtar të kafesë. Në këtë qasje të përmirësuar, ne do të shfrytëzojmë fuqinë e dekoruesve për të shtuar mbushje dhe shije të ndryshme në kafenë tonë bazë.

Ne do të fillojmë duke ruajtur klasën tonë të ndërfaqes Coffee dhe variablin tonë SIMPLE_COFFEE si bazë për të gjitha variacionet e kafesë. Megjithatë, në vend që të përcaktojmë klasa të veçanta për çdo lloj kafeje, ne do të zbatojmë dekorues që mbështjellin objektin tonë bazë të kafesë.

Për të filluar, le të përcaktojmë një ndërfaqe bazë për dekoruesit tanë të kafesë. Kjo ndërfaqe do të sigurojë qëndrueshmëri midis të gjitha klasave të dekoruesve dhe do të sigurojë një grup të përbashkët metodash për të punuar.

from abc import ABC
from dataclasses import dataclass


@dataclass
class BaseCoffeeDecorator(ABC):
    coffee: Coffee

    @property
    @abstractmethod
    def extra_cost(self) -> float:
        raise NotImplementedError

    @property
    @abstractmethod
    def extra_name(self) -> str:
        raise NotImplementedError

    @property
    @abstractmethod
    def extra_ingredients(self) -> list[str]:
        raise NotImplementedError

    def __call__(self) -> Coffee:
        name = f"{self.coffee.name} + {self.extra_name}"
        price = self.coffee.price + self.extra_cost
        ingredients = self.coffee.ingredients + self.extra_ingredients
        is_vegan = self.coffee.is_vegan and not any(
            ingredient in NON_VEGAN_INGREDIENTS for ingredient in self.extra_ingredients
        )
        return replace(self.coffee, name=name, price=price, ingredients=ingredients, is_vegan=is_vegan)

Këtu themi se si çdo dekorues kafeje duhet të përcaktojë një kosto shtesë, një emër shtesë dhe përbërës të mundshëm shtesë që do t'i shtohen kafesë.

Duke zbatuar klasa të veçanta dekoruesish, të tilla si MilkDecorator ose VanillaDecorator, ne mund t'i shtojmë me lehtësi mbushjet ose shijet e dëshiruara në kafenë tonë. Çdo klasë dekoruesi do të përmbledhë objektin bazë të kafesë dhe do të modifikojë sjelljen e tij duke shtuar funksionalitetin e dëshiruar.

class MilkDecorator(BaseCoffeeDecorator):
    extra_name = "Milk"
    extra_cost = 0.2
    extra_ingredients = ["Milk"]


class VanillaDecorator(BaseCoffeeDecorator):
    extra_name = "Vanilla"
    extra_cost = 0.3
    extra_ingredients = ["Vanilla"]

Dhe klienti ynë mund të bëjë porosinë si më poshtë:

coffee_with_milk = MilkDecorator(SIMPLE_COFFEE)()
coffee_with_milk_and_vanilla = VanillaDecorator(MilkDecorator(SIMPLE_COFFEE)())()
orders = [
    SIMPLE_COFFEE.order(amount=1),
    coffee_with_milk.order(amount=3),
    coffee_with_milk_and_vanilla.order(amount=2),
]


for order in orders:
    print(order)

# Output:
# Ordered 1 'Simple Coffee' for the price of 1.0€
# Ordered 3 times a 'Simple Coffee + Milk' for the total price of 3.60€
# Ordered 2 times a 'Simple Coffee + Milk + Vanilla' for the total price of 3.00€

Avantazhi i rëndësishëm i kësaj qasjeje është se ju duhet të përcaktoni vetëm një klasë dekoruesi për llojin e majës ose aromës, në vend që të krijoni nënklasa për çdo kombinim të mundshëm. Merrni parasysh skenarin ku klientët mund të kombinojnë çdo majë ose shije me çdo majë ose shije tjetër. Në këtë rast, me vetëm 10 lloje të ndryshme mbushjesh, do të kishte 1023 kombinime të mundshme. Do të ishte jopraktike dhe e rëndë të krijoheshin 1023 variabla për çdo kombinim.

Duke adoptuar këtë qasje, ne arrijmë një dizajn më fleksibël dhe modular. Mbushje ose shije të reja mund të shtohen duke krijuar klasa shtesë dekoruesish, pa nevojën për të modifikuar klasat ekzistuese të kafesë. Kjo lejon personalizimin dhe zgjerimin e lehtë të ofertave tona të kafesë.

Si përmbledhje, duke përdorur modelin e dekoruesit, ne krijojmë një sistem koheziv dhe të shtrirë të porositjes së kafesë, ku kombinime të ndryshme të mbushjeve dhe shijeve mund të aplikohen në kafenë tonë bazë, duke rezultuar në një përvojë të këndshme dhe të personalizueshme kafeje për klientët tanë.

Produkte të ndryshme kafeje, të bëra drejt me dekorues Python

Duke e çuar sistemin tonë të porositjes së kafesë në një nivel tjetër, ne mund të shfrytëzojmë funksionalitetin e fuqishëm të dekoruesit të integruar të ofruar nga vetë Python. Me këtë qasje, ne mund të arrijmë të njëjtin funksionalitet si më parë duke përqafuar elegancën dhe thjeshtësinë e dekoruesve Python.

Në vend që të krijojmë klasa të veçanta dekoruesish si MilkDecorator dhe VanillaDecorator, ne mund të përdorim simbolin @ dhe të aplikojmë dekorues drejtpërdrejt në funksionet tona të kafesë. Kjo jo vetëm që thjeshton kodin, por gjithashtu rrit lexueshmërinë dhe mirëmbajtjen e tij.

Le të zhytemi në një shembull se si mund të arrijmë të njëjtat rezultate duke përdorur dekoruesit e integruar të Python:

from typing import Callable
from functools import wraps


def milk_decorator(func: Callable[[], Coffee]) -> Callable[[], Coffee]:
    @wraps(func)
    def wrapper() -> Coffee:
        coffee = func()
        return replace(coffee, name=f"{coffee.name} + Milk", price=coffee.price + 0.2)
    return wrapper


def vanilla_decorator(func: Callable[[], Coffee]) -> Callable[[], Coffee]:
    @wraps(func)
    def wrapper() -> Coffee:
        coffee = func()
        return replace(coffee, name=f"{coffee.name} + Vanilla", price=coffee.price + 0.3)
    return wrapper


@milk_decorator
def make_coffee_with_milk():
    return SIMPLE_COFFEE


@vanilla_decorator
@milk_decorator
def make_coffee_with_milk_and_vanilla():
    return SIMPLE_COFFEE


# Output:
# Ordered 1 'Simple Coffee' for the price of 1.0€
# Ordered 3 times a 'Simple Coffee + Milk' for the total price of 3.60€
# Ordered 2 times a 'Simple Coffee + Milk + Vanilla' for the total price of 3.00€

Vini re se si këtu, ne arritëm saktësisht të njëjtin rezultat si më parë, por në vend që të përdornim klasën tonë BaseCoffeeDecorator, ne përdorëm funksionalitetin e integruar të dekoruesit të python duke përdorur funksionin wraps nga paketa functools (https://docs.python.org/ 3/library/functools.html#functools.wraps).

Zinxhiri i Përgjegjësisë

Modeli i projektimit të Zinxhirit të Përgjegjësisë (CoR) ofron një qasje fleksibël dhe të organizuar për të trajtuar operacionet ose kërkesat e njëpasnjëshme mbi një objekt duke e kaluar atë përmes një serie mbajtësish. Çdo mbajtës në zinxhir ka aftësinë të kryejë veprime ose kontrolle specifike mbi objektin dhe të marrë një vendim për të përpunuar kërkesën ose për ta deleguar atë tek mbajtësi tjetër në linjë.

Imagjinoni që ju, si punonjës, dëshironi të keni një monitor të dytë për konfigurimin e punës tuaj. Për ta bërë këtë të ndodhë, ju duhet të kaloni përmes një zinxhiri miratimi. Së pari, ju i paraqisni një kërkesë drejtuesit të ekipit tuaj të menjëhershëm, i cili vlerëson nëse përputhet me politikat dhe buxhetin e departamentit. Nëse drejtuesi i ekipit tuaj miraton, ata ia kalojnë kërkesën departamentit të financave, i cili verifikon disponueshmërinë e fondeve. Më pas, departamenti i financave mund të konsultohet me departamente të tjera përkatëse, si p.sh. infrastruktura e prokurimit ose e TI-së, për t'u siguruar që kërkesa mund të përmbushet. Përfundimisht, kërkesa arrin tek vendimmarrësi përfundimtar, si shefi i departamentit ose drejtori i financës, i cili merr vendimin përfundimtar.

Në këtë skenar, çdo nivel miratimi korrespondon me një hallkë në zinxhirin e përgjegjësisë. Çdo person në zinxhir ka një përgjegjësi dhe autoritet specifik për të trajtuar pjesën e tij të kërkesës. Zinxhiri lejon një rrjedhë të strukturuar dhe të njëpasnjëshme të informacionit dhe vendimmarrjes, duke siguruar që çdo nivel i hierarkisë së organizatës është i përfshirë dhe ka mundësinë të kontribuojë në vendimin përfundimtar.

Kryerja e kontrolleve në porositë e kafesë

Le të thellojmë më tej në shembullin tonë të sistemit të porositjes së kafesë dhe të eksplorojmë kontrolle shtesë që mund të aplikohen. Në një skenar të jetës reale, sistemi mund të bëhet më i ndërlikuar se zbatimi ynë aktual. Imagjinoni që pronari i lokalit të kafesë të specifikojë se asnjë kafe e vetme nuk duhet të kushtojë më shumë se 10 € për të ruajtur një reputacion të përballueshëm të markës. Për më tepër, duhet të ekzistojë një mekanizëm për të kontrolluar dyfish statusin vegan të një kafeje bazuar në përbërësit e saj.

Për këtë seksion, ne do të përcaktojmë tre lloje të ndryshme kafeje: një kafe e thjeshtë, një kapuçino dhe një kapuçin e shtrenjtë:

SIMPLE_COFFEE = Coffee(name="Simple Coffee", price=1.0, ingredients=["Coffee"], is_vegan=True)
CAPPUCCINO = Coffee(name="Cappuccino", price=2.0, ingredients=["Coffee", "Milk"], is_vegan=True)
EXPENSIVE_CAPPUCCINO = Coffee(name="Cappuccino", price=12.0, ingredients=["Coffee", "Milk"], is_vegan=False)

Vini re se si e kemi shënuar kapuçinën si vegan, ndërsa qumështi është i përfshirë në përbërësit e tij. Gjithashtu, çmimi i kapuçinës së shtrenjtë është më i lartë se 10 €.

Më pas, ne krijojmë një ndërfaqe të unifikuar për të gjithë mbajtësit në Zinxhirin tonë të Përgjegjësisë (CoR):

@dataclass
class BaseHandler(ABC):
    next_handler: BaseHandler | None = None

    @abstractmethod
    def __call__(self, coffee: Coffee) -> Coffee:
        raise NotImplementedError

Në këtë fragment kodi, ne përcaktojmë klasën BaseHandler si një klasë bazë abstrakte (ABC) duke përdorur dekoruesin @dataclass. Klasa përfshin një atribut next_handler që përfaqëson mbajtësin tjetër në zinxhir. Secili mbajtës në Kornizë zbaton këtë ndërfaqe dhe anashkalon metodën __call__ për të përcaktuar veprimin e tij specifik ose për të kontrolluar objektin e kafesë.

Nëse jepet një next_handler kur krijoni një shembull mbajtës, kjo nënkupton që ka një mbajtës tjetër për të përpunuar objektin e kafesë pasi mbajtësi aktual të përfundojë funksionimin e tij. Anasjelltas, nëse nuk ofrohet asnjë mbajtës tjetër, mbajtësi aktual shërben si pika fundore e zinxhirit.

Kjo ndërfaqe e përbashkët siguron që të gjithë mbajtësit t'i përmbahen një strukture konsistente, duke lejuar lidhjen pa probleme dhe fleksibilitet në shtimin, heqjen ose riorganizimin e mbajtësve sipas nevojës.

Tani mund të vazhdojmë të përcaktojmë dy mbajtësit tanë që do të kontrollojnë çmimin maksimal prej 10 € dhe do të verifikojnë nëse asnjë kafe nuk është shënuar gabimisht si vegan:

NON_VEGAN_INGREDIENTS = ["Milk"]


@dataclass
class MaximumPriceHandler(BaseHandler):
    def __call__(self, coffee: Coffee) -> Coffee:
        if coffee.price > 10.0:
            raise RuntimeError(f"{coffee.name} costs more than €10?!")
        return coffee if self.next_handler is None else self.next_handler(coffee)


@dataclass
class VeganHandler(BaseHandler):
    def __call__(self, coffee: Coffee) -> Coffee:
        if coffee.is_vegan and any(ingredient in NON_VEGAN_INGREDIENTS for ingredient in coffee.ingredients):
            raise RuntimeError(f"Coffee {coffee.name} is said to be vegan but contains non-vegan ingredients")
        if not coffee.is_vegan and all(ingredient not in NON_VEGAN_INGREDIENTS for ingredient in coffee.ingredients):
            raise RuntimeError(f"Coffee {coffee.name} is not not labelled as vegan when it should be")
        return coffee if self.next_handler is None else self.next_handler(coffee)

Le të testojmë mbajtësit tanë me kodin e mëposhtëm:

handlers = MaximumPriceHandler(VeganHandler())

try:
    cappuccino = handlers(CAPPUCCINO)
except RuntimeError as err:
    print(str(err))

try:
    cappuccino = handlers(EXPENSIVE_CAPPUCCINO)
except RuntimeError as err:
    print(str(err))

# Output:
# Coffee Cappuccino is said to be vegan but contains non-vegan ingredients
# Expensive Cappuccino costs more than €10?!

Ne vëzhguam sesi sistemi i porositjes e trajton saktë shtimin e shtesave në një porosi të thjeshtë kafeje. Megjithatë, përpjekja për të krijuar një urdhër Cappuccino ose ExpensiveCappuccino rezulton në ngritjen e një përjashtimi. Kjo sjellje nxjerr në pah logjikën e rreptë të përpunimit të zbatuar nga zinxhiri i përgjegjësisë.

Ajo që vlen të përmendet është se sa lehtë mund ta zgjerojmë këtë kod duke përcaktuar mbajtës të tjerë për të kryer operacione të tjera specifike për porositë e kafesë. Për shembull, le të themi se dëshironi të ofroni një zbritje prej 10% në porositë e ushqimit. Mund të krijoni pa mundim një mbajtës të ri dhe ta shtoni atë në zinxhir. Ky mbajtës do të ulte çmimin e porosisë me 10% nëse është shënuar si porosi me dorëzim.

Një nga avantazhet kryesore të modelit të projektimit të Zinxhirit të Përgjegjësisë është respektimi i tij ndaj parimit të hapur-mbyllur në zhvillimin e softuerit. Trajtuesit ekzistues mbeten të mbyllur për modifikim, duke promovuar stabilitetin dhe ripërdorimin e kodit. Megjithatë, modeli i dizajnit lejon zgjerim të lehtë duke futur mbajtës të rinj në zinxhir kur është e nevojshme. Ky fleksibilitet i fuqizon zhvilluesit të plotësojnë kërkesat në ndryshim pa ndërprerë bazën ekzistuese të kodit.

Pema e Përbërë / Objekt

Modeli i dizajnit të përbërë hyn në lojë kur përballeni me një skenar që përfshin një përzierje të objekteve të thjeshta fundore (gjethe) dhe kontejnerëve më kompleksë që mund të mbajnë kontejnerë të tjerë (degë) ose objekte fundore (gjethe).

Një analogji e botës reale e modelit të përbërë mund të gjendet në një sistem skedari. Konsideroni një strukturë drejtorie ku dosjet (degët) mund të përmbajnë dosje ose skedarë të tjerë (gjethe), ndërsa skedarët individualë (gjethe) ekzistojnë në mënyrë të pavarur. Ky rregullim hierarkik ju lejon të trajtoni të gjithë strukturën si një objekt të unifikuar, pavarësisht nëse jeni duke punuar me një skedar të vetëm ose një drejtori komplekse.

Porositë e përbëra të kafesë

Në sistemin tonë të porositjes së kafesë, ne mund të aplikojmë modelin e dizajnit të përbërë për të trajtuar porositë e thjeshta individuale të kafesë (gjethe) dhe strukturat më komplekse të porosive të kafesë (degët).

Merrni parasysh një skenar ku një porosi e vetme kafeje përfaqëson një linjë individuale në faturë, si p.sh. "Dy kafe të thjeshta për një çmim total prej 2 €". Megjithatë, ne gjithashtu duhet të akomodojmë porosi komplekse kafeje që përbëhen nga porosi të shumta individuale apo edhe porosi të tjera të plota kafeje. Për shembull, klientët që porosisin nga tarraca mund të vendosin të shtojnë artikuj shtesë në porosinë e tyre ekzistuese të përpunuar.

Për të menaxhuar këtë kompleksitet, ne mund të përdorim modelin e dizajnit të përbërë, i cili siguron një ndërfaqe të unifikuar si për porositë individuale të kafesë ashtu edhe për strukturat e porosive të përbëra. Kjo ndërfaqe e përbashkët përcakton metoda thelbësore, si llogaritja e çmimit total të porosisë përfundimtare, pavarësisht nëse është një kafe e vetme apo një kombinim kompleks.

Duke përdorur modelin Composite, ne mund të thjeshtojmë trajtimin e porosive të kafesë, të sigurojmë operacione të qëndrueshme në lloje të ndryshme porosish dhe të mundësojmë integrimin e pandërprerë të funksionaliteteve të reja në sistemin e porositjes së kafesë.

from dataclasses import dataclass


@dataclass
class Coffee:
    name: str
    price: float
    ingredients: list[str]
    is_vegan: bool


SIMPLE_COFFEE = Coffee(name="Simple Coffee", price=1.0, ingredients=["Coffee"], is_vegan=True)
CAPPUCCINO = Coffee(name="Cappuccino", price=2.0, ingredients=["Coffee", "Milk"], is_vegan=False)


class CoffeeOrderComponentBase(ABC):
    @property
    @abstractmethod
    def total_price(self) -> float:
        raise NotImplementedError

    @property
    @abstractmethod
    def all_ingredients(self) -> list[str]:
        raise NotImplementedError

    @property
    @abstractmethod
    def is_vegan(self) -> bool:
        raise NotImplementedError

    @property
    @abstractmethod
    def order_lines(self) -> list[str]:
        raise NotImplementedError

Këtu, ne kemi përcaktuar klasën tonë të të dhënave Coffee, e cila specifikon vetitë e kërkuara për çdo lloj kafeje bazë. Ne kemi krijuar dy raste të kafesë bazë, përkatësisht SIMPLE_COFFEE dhe CAPPUCCINO.

Më pas, ne prezantojmë CoffeeOrderComponentBase, i cili shërben si ndërfaqe e përbashkët për të dy gjethet (porositë e vetme të kafesë) dhe kontejnerët kompleksë (porositë e përbëra të kafesë). Kjo ndërfaqe përcakton metodat e mëposhtme që duhet të zbatohen nga të dy llojet:

  • total_price: Llogarit çmimin total të porosisë.
  • all_ingredients: Merr të gjithë përbërësit e përfshirë në porosi.
  • is_vegan: Tregon nëse rendi i plotë është vegan apo jo.
  • order_lines: Gjeneron një përmbledhje të rendit si rreshta teksti.

Tani, le të përqendrohemi në zbatimin e komponentit të gjetheve, i cili përfaqëson një porosi të vetme kafeje:

@dataclass
class CoffeeOrder(CoffeeOrderComponentBase):
    base_coffee: Coffee
    amount: int

    @property
    def total_price(self) -> float:
        return self.amount * self.base_coffee.price

    @property
    def all_ingredients(self) -> list[str]:
        return self.base_coffee.ingredients

    @property
    def is_vegan(self) -> bool:
        return self.base_coffee.is_vegan

    @property
    def order_lines(self) -> list[str]:
        if self.amount == 1:
            return [f"Ordered 1 '{self.base_coffee.name}' for the price of {self.total_price}€"]
        return [
            f"Ordered {self.amount} times a '{self.base_coffee.name}' for the total price of {self.total_price:.2f}€"
        ]

Tani, le të kalojmë në zbatimin e porosisë më komplekse të kafesë, e cila është në gjendje të mbajë një listë të fëmijëve. Kjo lejon folezën e të dy gjetheve (porositë e vetme të kafesë) dhe kontejnerët e tjerë kompleksë.

from dataclasses import field
from more_itertools import flatten


@dataclass
class CompositeCoffeeOrder(CoffeeOrderComponentBase):
    children: list[CoffeeOrderComponentBase] = field(default_factory=list)

    @property
    def total_price(self) -> float:
        return sum(child.total_price for child in self.children)

    @property
    def all_ingredients(self) -> list[str]:
        return list(set(flatten([child.all_ingredients for child in self.children])))

    @property
    def is_vegan(self) -> bool:
        return all(child.is_vegan for child in self.children) or not len(self.children)

    @property
    def order_lines(self) -> list[str]:
        return list(flatten([child.order_lines for child in self.children]))

Tani mund të përfaqësojmë një rend kompleks, të përbërë si ky:

order = CompositeCoffeeOrder(
    children=[
        CoffeeOrder(amount=2, base_coffee=CAPPUCCINO),
        CoffeeOrder(amount=1, base_coffee=SIMPLE_COFFEE),
        CompositeCoffeeOrder(
            children=[CoffeeOrder(amount=3, base_coffee=SIMPLE_COFFEE)]
        ),
    ]
)

for order_line in order.order_lines:
    print(order_line)

print("-" * 40)
print(f"The total price of the order is {order.total_price:.2f}€")
print(f"These are all the ingredients included in this order: {', '.join(order.all_ingredients)}")

# Output:
# Ordered 2 times a 'Cappuccino' for the total price of 4.00€
# Ordered 1 'Simple Coffee' for the price of 1.0€
# Ordered 3 times a 'Simple Coffee' for the total price of 3.00€
# ----------------------------------------
# The total price of the order is 8.00€
# These are all the ingredients included in this order: Milk, Coffee

Kombinimi i modeleve të ndryshme të projektimit

Tani që kemi eksploruar individualisht modelet e dizajnit të Dekoratorit, Zinxhirit të Përgjegjësisë dhe Kompozitit, le të shohim se si mund t'i bashkojmë të gjitha për të ndërtuar një porosi komplekse kafeje me mbushje dhe shije të ndryshme. Ky shembull do të demonstrojë se si ne mund të aplikojmë modelin e zinxhirit të përgjegjësisë për të kryer hapat e vërtetimit në porosinë tonë të kafesë, ndërsa përdorim modelin Kompozit për të krijuar një hierarki të strukturuar të rendit.

Duke i kombinuar këto modele, ne mund të krijojmë një sistem të fuqishëm dhe fleksibël të porositjes së kafesë që i lejon klientët të personalizojnë porositë e tyre me mbushje dhe shije të ndryshme, duke siguruar që të gjitha kontrollet e vërtetimit të kryhen pa probleme.

handlers = MaximumPriceHandler(VeganHandler())
coffee_with_milk_and_vanilla = VanillaDecorator(MilkDecorator(SIMPLE_COFFEE)())()

order = CompositeCoffeeOrder(
    children=[
        CoffeeOrder(amount=2, base_coffee=handlers(CAPPUCCINO)),
        CoffeeOrder(amount=1, base_coffee=handlers(coffee_with_milk_and_vanilla)),
        CompositeCoffeeOrder(
            children=[CoffeeOrder(amount=3, base_coffee=handlers(VanillaDecorator(CAPPUCCINO)()))]
        ),
    ]
)
for order_line in order.order_lines:
    print(order_line)

print("-" * 40)
print(f"The total price of the order is {order.total_price:.2f}€")
print(f"These are all the ingredients included in this order: {', '.join(order.all_ingredients)}")
print(f"This order is {'' if order.is_vegan else 'not'} vegan")

# Output:
# Ordered 2 times a 'Cappuccino' for the total price of 4.00€
# Ordered 1 'Simple Coffee + Milk + Vanilla' for the price of 1.5€
# Ordered 3 times a 'Cappuccino + Vanilla' for the total price of 6.90€
# ----------------------------------------
# The total price of the order is 12.40€
# These are all the ingredients included in this order: Coffee, Vanilla, Milk
# This order is not vegan

Së pari, ne përcaktojmë zinxhirin tonë të përgjegjësisë, i cili përbëhet nga VeganHandler është përgjegjës për të kontrolluar nëse ndonjë produkt është etiketuar gabimisht si vegan, dhe MaximumPriceHandler përgjegjës për verifikimin që asnjë kafe e vetme nuk e kalon çmimin prej 10 € .

Më pas, ne përdorim VanillaDecorator dhe MilkDecorator për të transformuar një kafe të thjeshtë në një kafe me qumësht dhe vanilje, përkatësisht.

Së fundi, ne përdorim CompositeCoffeeOrder për të krijuar një porosi që përfshin dy porosi të vetme kafeje dhe një porosi tjetër komplekse.

Kur ekzekutojmë skenarin, mund të vëzhgojmë dekoruesit që modifikojnë emrat dhe çmimet e porosive të ndryshme. CompositeCoffeeOrder llogarit saktë çmimin total të porosisë përfundimtare. Për më tepër, ne mund të shohim listën e plotë të përbërësve dhe të përcaktojmë nëse e gjithë porosia është vegane apo jo.

konkluzioni

Si përfundim, modelet e dizajnit luajnë një rol vendimtar në zhvillimin e softuerit, duke ofruar zgjidhje për problemet e zakonshme dhe duke promovuar ripërdorimin, fleksibilitetin dhe mirëmbajtjen e kodit. Në këtë postim në blog, ne eksploruam tre modele të fuqishme të dizajnit në Python: Decorator, Zinxhiri i Përgjegjësisë dhe Kompozit.

Modeli Decorator na lejoi të shtojmë në mënyrë dinamike sjellje të re në objektet ekzistuese, duke demonstruar dobinë e tij në zgjerimin e funksionalitetit të porosive të kafesë me mbushje dhe shije të ndryshme. Mësuam se si mund të zbatohen dekoruesit si me klasat e mbështjellësve të personalizuar ashtu edhe me funksionalitetin e integruar të dekoruesit të Python, duke ofruar opsione fleksibël për organizimin e kodit.

Modeli i Zinxhirit të Përgjegjësisë u tregua i vlefshëm në kryerjen e operacioneve të njëpasnjëshme në porositë e kafesë, duke emuluar një skenar të botës reale të proceseve të miratimit në një organizatë. Duke krijuar një zinxhir përpunuesish, secili përgjegjës për një detyrë specifike, ne arritëm modularitet dhe shtrirje, ndërsa siguruam që kërkesat të kalojnë saktë përmes zinxhirit.

Modeli Kompozit na mundësoi të krijonim hierarki të strukturuara të porosive të kafesë, duke përfshirë si porositë e thjeshta ashtu edhe kompozime më komplekse. Duke përcaktuar një ndërfaqe të përbashkët, ne arritëm qëndrueshmëri në aksesin dhe manipulimin e porosive, pavarësisht nga kompleksiteti i tyre.

Gjatë gjithë shembujve tanë, ne dëshmuam fuqinë e modeleve të projektimit në përmirësimin e lexueshmërisë, mirëmbajtjes dhe shtrirjes së kodit. Duke adoptuar këto zgjidhje të provuara, ne mund të përmirësojmë bashkëpunimin midis zhvilluesve dhe të ndërtojmë sisteme softuerësh që janë të fortë, fleksibël dhe të adaptueshëm ndaj kërkesave në ndryshim.

Ju mund të gjeni të gjithë kodin burimor të dhënë në këtë postim në blog këtu: https://github.com/GlennViroux/design-patterns-blog

Mos ngurroni të më kontaktoni për çdo pyetje ose koment!

Referencat

https://refactoring.guru/design-patterns/composite

https://refactoring.guru/design-patterns/chain-of-responsibility

https://refactoring.guru/design-patterns/decorator