设计模式是软件设计中常见问题的宝贵解决方案。尽管它们可能无法提供可以直接复制并粘贴到您的代码中的现成解决方案,但它们可以作为强大的蓝图,可以自定义和量身定制以应对特定的设计挑战。
使用设计模式的重要优势之一是它们带给您的代码库的清晰度。通过利用良好的模式,您可以有效地向您的开发人员沟通您要解决的问题和所采用的方法。经常说代码是曾经编写的,但读了很多次,强调了代码可读性的重要性。在软件设计过程中,将更加重视代码可读性而不是写作是必不可少的。将战斗测试的设计模式纳入您的代码库是提高代码可读性的一种非常有效的方法。
在本文中,我们将深入研究三种不同的设计模式,并提供有关如何在Python中应用它们的详细示例。通过理解和利用这些模式,您将配备强大的工具,以增强代码的结构,可维护性和清晰度。
装饰师
装饰器设计模式使您可以将现有对象包装在特殊包装器对象中,添加新的行为而无需修改原始对象。
为了说明这种模式,请想象一下自己在一个寒冷的冬日,穿着T恤出门。当您开始感到寒冷时,您决定穿上毛衣,充当您的第一个装饰器。但是,如果您仍然很冷,您也可以选择将自己包裹在外套中,并用作另一个装饰师。这些额外的层“装饰”您仅穿着T恤的基本行为。重要的是,您可以灵活地在不再需要改变基本状态的情况下卸下任何服装。
通过使用装饰器图案,您可以在保持核心对象不变的同时动态增强对象。它为扩展行为提供了灵活而模块化的方法,就像根据您的舒适度添加或脱掉衣服层。
不同的咖啡产品,错误的方式
想象您的任务是为当地咖啡店开发咖啡订购系统。该系统应以不同的口味和浇头支持各种类型的咖啡。让我们考虑以下咖啡变化:
- 简单咖啡
- 牛奶咖啡
- 咖啡与香草馅料
- 用牛奶和香草馅料咖啡
在不知道装饰器设计模式的情况下,您可以将所有不同类型的咖啡用它们的浇头作为单独的变量实施。
在这种方法中,我们首先定义所有类型的咖啡的接口。该界面指定了常见的属性,例如咖啡的名称,价格,成分清单以及对素食友好型。我们还包括 order
方法,允许客户请求特定的咖啡并指定数量。
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)
接下来,让我们定义我们的四种不同类型的咖啡:
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)
现在,客户可以按以下方式下订单:
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€
您很快就会看到该代码在新型咖啡以及与不同类型的浇头和口味的更多组合时如何成倍增长。让我们探索装饰者如何在这里帮助我们。
不同的咖啡产品,正确的方式
正如我们前面讨论的那样,装饰器的图案使我们能够用其他功能包装一个简单的基础咖啡对象,以创建所需的最终咖啡产品。在这种改进的方法中,我们将利用装饰工的力量在我们的碱咖啡中添加各种浇头和口味。
我们将首先维护 Coffee
接口类别,我们的 SIMPLE_COFFEE
变量是所有咖啡变化的基础。但是,我们不是为每种类型的咖啡定义单独的类,而是实现包裹基础咖啡对象的装饰师。
要开始,让我们为咖啡装饰器定义一个基本界面。该界面将确保所有装饰符课程之间的一致性,并提供一组使用的方法来使用。
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)
在这里,我们说每个咖啡装饰者应如何定义额外的成本,额外的名称以及将添加到咖啡中的额外成分。
通过实现特定的装饰类课程,例如 MilkDecorator
或 VanillaDecorator
,我们可以轻松地在咖啡中添加所需的浇头或口味。每个装饰器类都将封装基本咖啡对象并通过添加所需功能来修改其行为。
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"]
,我们的客户可以按照以下方式下订单:
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€
这种方法的显着优势是,您只需要以每种浇灌或风味定义一个装饰级别的类别,而不是为每种可能的组合创建子类。考虑客户可以将任何浇头或风味与任何其他浇头或风味结合起来的情况。在这种情况下,只有10种不同类型的浇头,将有多达1023种可能的组合。为每种组合创建1023个变量将是不切实际的和麻烦的。
通过采用这种方法,我们实现了更灵活,更模块化的设计。可以通过创建其他装饰符类,而无需修改现有咖啡类,可以添加新的浇头或口味。这可以轻松自定义和扩展我们的咖啡产品。
总而言之,通过使用装饰仪图案,我们创建了一个具有凝聚力且可扩展的咖啡订购系统,可以将各种浇头和口味组合用于我们的基础咖啡,从而为我们的客户提供令人愉悦且可定制的咖啡体验。 P>
不同的咖啡产品,与Python装饰师一起完成
将我们的咖啡订购系统提升到一个新的水平,我们可以利用Python本身提供的强大内置装饰器功能。通过这种方法,我们可以在拥抱Python装饰器的优雅和简单性时获得与以前相同的功能。
我们可以使用 MilkDecorator
和 VanillaDecorator
,而是使用 VanillaDecorator
,而是可以利用 @
符号,并直接将装饰符直接应用于我们的咖啡功能。这不仅简化了代码,还可以增强其可读性和可维护性。
让我们介绍一个示例,说明如何使用内置的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€
请注意,我们如何实现与以前完全相同的结果,但是我们没有使用python的内置装饰器功能,而不是使用wraps
函数(https://docs.python.org/3/library/functools.html#functools.wraps)。 P>
责任链
责任链(COR)设计模式提供了一种灵活而有条理的方法,可以通过将其传递一系列处理程序来处理对象对对象的顺序操作或请求。链中的每个处理程序都能执行特定的操作或检查对象,并决定处理请求或将其委派给了下一个处理程序。
想象您作为员工,想为您的工作设置提供第二个显示器。为了实现这一目标,您需要经过一系列认可。首先,您将请求提交您的直接团队负责人,后者评估其是否与部门的政策和预算保持一致。如果您的团队负责人批准,他们将请求传递给财务部门,这将验证资金的可用性。然后,财务部可以与其他相关部门(例如采购或IT基础设施)进行协商,以确保可以满足该请求。最终,该请求达到了最终决策者,例如部门负责人或财务总监,他们做出了最终决定。
在这种情况下,每个批准级别都对应于责任链中的链接。连锁店中的每个人都有特定的责任和权力来处理他们的一部分。该链允许信息和决策的结构化和顺序流动,以确保涉及组织的层次结构的每个级别,并有机会为最终的决策做出贡献。
对咖啡订单进行检查
让我们进一步深入研究我们的咖啡订购系统示例,并探索可以应用的其他检查。在现实生活中,该系统可能比我们当前的实施更复杂。想象一下,咖啡馆的所有者指出,保持价格合理的品牌声誉不得超过10杯。此外,应该有一种机制,可以根据其成分仔细检查咖啡的素食状态。
在本节中,我们将定义三种不同类型的咖啡:一种简单的咖啡,一张卡布奇诺咖啡和昂贵的卡布奇诺咖啡:
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)
请注意,我们如何将卡布奇诺咖啡标记为素食,而牛奶的成分包括在内。此外,昂贵的卡布奇诺咖啡的价格高于10 —
接下来,我们为责任链中的所有处理程序建立了一个统一的界面(COR):
@dataclass
class BaseHandler(ABC):
next_handler: BaseHandler | None = None
@abstractmethod
def __call__(self, coffee: Coffee) -> Coffee:
raise NotImplementedError
在此代码段中,我们使用 BaseHandler
类将抽象基类(ABC)定义为使用 @dataclass
装饰器。该类包括代表链中下一个处理程序的 next_handler
属性。 Cor中的每个处理程序都实现了此界面,并覆盖了 __call__
定义其特定动作或检查咖啡对象的方法。
如果在创建处理程序实例时提供了 next_handler
,则表示在当前处理程序完成其操作后还有另一个处理程序可以处理咖啡对象。相反,如果没有提供下一个处理程序,当前处理程序将用作链的终点。
此通用界面确保所有处理人员都遵守一致的结构,从而允许根据需要添加,删除或重新安排处理程序的无缝链条和灵活性。
现在,我们可以继续定义我们的两个处理程序,这些处理程序将检查最高价格€10,并将验证是否不正确地将咖啡标记为纯素食:
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)
让我们用以下代码测试我们的处理程序:
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?!
我们观察到订购系统如何正确处理在简单的咖啡订单中添加浇头的方法。但是,尝试创建 Cappuccino
或 ExpensiveCappuccino
订单会导致例外。这种行为强调了由责任链实施的严格处理逻辑。
值得注意的是,我们可以通过定义其他处理程序来在咖啡订单上执行其他特定操作来轻松扩展此代码。例如,假设您想为外卖订单提供10%的折扣。您可以毫不费力地创建一个新的处理程序并将其添加到链条中。如果将订单的价格标记为外卖订单,则该处理程序的价格将降低10%。
责任链设计模式的关键优势之一是它在软件开发中遵守 Open-Closed principle 。现有的处理程序保持关闭,以进行修改,促进代码稳定性和可重复性。但是,设计模式可以轻松扩展,并在必要时向链条引入新处理程序。这种灵活性使开发人员能够适应不断变化的需求而不会破坏现有代码库。
复合 /对象树
当您面对一个涉及简单端对象(叶)和更复杂的容器的情况时,复合设计模式就会发挥作用>
可以在文件系统中找到复合模式的现实类比。考虑一个目录结构,其中文件夹(分支)可以包含其他文件夹或文件(叶),而单个文件(叶子)则独立存在。这种层次结构布置使您可以将整个结构视为统一对象,无论您是使用一个文件还是复杂的目录。
复合咖啡订单
在我们的咖啡订购系统中,我们可以应用复合设计模式来处理简单的单个咖啡订单(叶子)和更复杂的咖啡订单结构(分支)。
考虑一个场景,其中单个咖啡订单代表法案上的单个线路,例如“两种简单的咖啡,总价格为−2”。但是,我们还需要容纳由多个单独订单甚至其他完整咖啡订单组成的复杂咖啡订单。例如,从露台订购的客户可能会决定在其现有处理的订单中添加其他项目。
为了管理这种复杂性,我们可以利用复合设计模式,该模式为单个咖啡订单和复合订单结构提供了统一的界面。这种常见的界面定义了基本方法,例如计算最终顺序的总价格,无论它是单个咖啡还是复杂的组合。
通过采用复合模式,我们可以简化咖啡订单的处理,确保跨不同订单类型的一致操作,并使新功能无缝集成到咖啡订购系统中。
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
在这里,我们定义了我们的 Coffee
数据类,该数据类别指定了每种基础咖啡所需的属性。我们创建了两个基础咖啡的实例,即 SIMPLE_COFFEE
和 CAPPUCCINO
。
接下来,我们介绍了 CoffeeOrderComponentBase
,它是两个叶子(单咖啡订单)和复杂容器(复合咖啡订单)的常见界面。此接口定义了以下两种类型应实现的方法:
-
total_price
:计算订单的总价格。 -
all_ingredients
:检索顺序中包含的所有成分。 -
is_vegan
:指示完整的顺序是否为素食。 -
order_lines
:以文本行的形式生成订单的摘要。
现在,让我们专注于代表单个咖啡订单的叶片组件的实现:
@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}€"
]
现在,让我们继续实施更复杂的咖啡订单,该咖啡订单能够持有儿童名单。这允许两片叶子(单咖啡订单)和其他复杂容器筑巢。
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]))
现在我们可以代表这样的复杂的复合顺序:
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
结合不同的设计模式
现在,我们已经分别探索了装饰商,责任链和复合设计模式,让我们看看如何将它们一起融合在一起,以使用不同的浇头和风味来建立复杂的咖啡订单。此示例将演示我们如何应用责任链模式来在我们的咖啡订单上执行验证步骤,同时使用复合模式创建结构化顺序层次结构。
通过结合这些模式,我们可以创建一个功能强大且灵活的咖啡订购系统,使客户可以通过各种浇头和口味自定义其订单,同时确保所有验证检查都无缝执行。
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
首先,我们定义了我们的责任链,由 VeganHandler
组成,负责检查任何产品是否错误地标记为素食主义者,而 MaximumPriceHandler
负责验证验证没有一杯咖啡超过10字的价格。
接下来,我们分别利用 VanillaDecorator
和 MilkDecorator
分别将简单的咖啡转变为带有牛奶和香草的咖啡。
最后,我们采用 CompositeCoffeeOrder
创建一个包括两个单一咖啡订单和另一个复杂订单的订单。
运行脚本时,我们可以观察装饰师修改不同订单的名称和价格。 CompositeCoffeeOrder
正确计算了最终订单的总价格。此外,我们可以查看成分的完整列表,并确定整个订单是否为素食。
结论
总而言之,设计模式在软件开发中起着至关重要的作用,为常见问题提供解决方案并促进代码可重复性,灵活性和可维护性。在这篇博客文章中,我们在Python中探索了三种强大的设计模式:装饰者,责任链和综合。
装饰器模式使我们能够动态地为现有对象添加新的行为,并证明了其在扩展具有各种浇头和风味的咖啡订单功能方面的有用性。我们了解了如何使用自定义包装器类和Python的内置装饰器功能来实现装饰器,从而为代码组织提供了灵活的选项。
责任链模式被证明在对咖啡订单上进行连续操作非常有价值,从而模仿了组织中批准过程的现实情况。通过创建一系列处理程序,每个负责特定任务,我们实现了模块化和可扩展性,同时确保请求正确地通过了链条。
复合模式使我们能够创建咖啡订单的结构化层次结构,并结合了简单的订单和更复杂的组成。通过定义一个共同的界面,我们在访问和操纵订单方面取得了一致性,无论其复杂性如何。
在我们的示例中,我们目睹了设计模式在增强代码可读性,可维护性和可扩展性方面的力量。通过采用这些验证的解决方案,我们可以改善开发人员之间的协作,并建立强大,灵活且适应不断变化的需求的软件系统。
您可以在此处找到此博客文章中提供的所有源代码:https://github.com/GlennViroux/design-patterns-blog
随时与我联系以进行任何问题或评论!
参考
https://refactoring.guru/design-patterns/composite
https://refactoring.guru/design-patterns/chain-of-responsibility