在面向对象程序设计中,设计者在决定所包含的内容与数量时,往往很难下决心。面向对象设计遵循的一个基本原则是责任分离,另一个原则是高内聚与低耦合。高内聚意味着一个类所能提供的功能应该是相关的,即一个类不要设计得包括很多互不相干的功能。但是,像其他设计原则一样,当高内聚原则运用到实际设计场合中时,也是一个相对的概念。到底“内聚”到什么程度才算“高内聚”?高内聚是否与可扩展性有矛盾?
01
策略模式的概念与机制
策略模式定义了一系列的算法,将每一个算法封装起来,并且使它们之间可以相互替换。策略模式让算法的变化不会影响到使用算法的客户。
下图是正式的策略模式设计类图。
策略模式中各组成部分的含义说明如下。
Strategy:定义了一个共同的接口。所有具体的算法类实现这个接口。环境(上下文)类 Context 使用这个接口调用具体的算法类。
ConcreteStrategy:封装了具体的算法,实现同一个接口。
Context:环境(上下文)类。用于配置一个具体的算法策略对象,维持一个策略接口类型的参考(Reference),并且可以定义一个让接口 Strategy 的具体对象访问的接口。在简单情况下,Context 类可以省略。
在以下情况之一发生时可以使用策略模式。
当有多个仅在行为上不同但是相关的类存在时,策略模式提供了一个为一个类配置多种行为之一的方法。
当一个算法使用用户不应该知道的数据时,使用策略模式可以将算法的实现细节隐藏起来,避免暴露与算法相关的复杂细节。注意,虽然可以将算法的实现细节封装起来,但是客户程序必须知道各个策略子类的接口。
当一个类有多种行为,这些行为以大块的条件语句实现时可以使用策略模式,这时可以将条件块移入它们自己的 Strategy 类。
Context 类具有如下作用。
作为客户类(Client)和策略类(Stragety)的“传话筒”,将客户类的请求与由客户类所提供的各种参数传递给策略类。客户类通常创建并且传递一个 ConcreteStrategy 的对象给 Context,然后客户类即可和 Context 类单独交互。
可以将算法所需要的所有数据传递给策略类。
可以将自己以参数的形式传递给策略的运算,然后当需要时,让策略类调用 Context。
使用策略模式的优点如下。
得到一系列可以复用的算法,这些算法继承一个共同的抽象类,因此共有的功能可以放到超类中。
将不同的算法封装在不同的策略子类中,使逻辑更加清晰,各个算法可以独立地变化。
使功能改变或者扩展更容易。具体地说,修改一个算法不必重新编译“Client”与“Context”类。增加一个新算法时,在应用程序暂时还不想使用该新算法的情况下,不必重新编译“Client”与“Context”类。
使用策略模式的缺点为,客户程序必须知道不同策略接口的各个子类的行为,必须理解每个子类有哪些不同。因此,在客户类中通常存在许多与策略类各个分支相关的条件语句,用于选择产生策略子类对象,然后将这些对象传递给 Context 类,而 Context 类则直接使用此对象调用策略模式的策略子类的方法。
02
关于策略模式的讨论
使用策略模式的出发点主要考虑如下。
将一组相关的算法封装为各个策略分支,从而将策略分支相关的代码隐藏起来。
希望可以提供程序的扩展性。
那么,策略模式的可扩展性究竟怎么样呢?下面将就该模式的可扩展性进行一些基本讨论。
实际上,策略模式的初衷是要减少与各个分支下的行为相关的条件语句。这已经通过将一个具有条件相关的多种行为的类拆分成一个策略超类与若干个策略子类得到了解决。也就是说,将原来的一个单独的但是包含多个条件语句的类改变为一个没有条件语句的策略层次类。在这里,条件语句消失了。但是在客户程序与 Context 中是否也不存在与策略子类相关的条件语句了呢?答案当然不是。
事实上,通常在策略模式的设计中,客户类根据不同的条件负责创建不同的策略子类对象,然后将该对象传递给 Context 类,Context 类的作用为:可以为被调用的策略子类的一些方法提供一些参数,以及使用该由 Client 类传入的对象调用 Strategy 类的方法。这说明,在客户类 Client 中,存在许多与策略分支子类相关的条件语句,而在 Context 类中,没有这样的语句,那么,是否可以将创建子类对象的责任交给 Context 类,而客户类 Client 只为 Context 类提供一些代表客户请求的参数呢?这在策略模式的描述中没有回答。
下面就这两种情况的可扩展性进行一些讨论。
(1)客户类负责创建策略子类对象的情况。
客户类根据用户提供的不同的请求,负责创建不同的策略子类的对象,然后将该对象传递给 Context 类。在这种情况下,客户类通常包含与策略相关的条件语句,而在 Context 类中不必使用任何与策略有关的条件语句,因此,修改或者添加一个策略子类都不必修改 Context 类。但是,在添加一个新的策略子类的情况下,如果客户类需要使用该子类,往往需要在客户类中添加一个新的条件语句。也就是,客户类需要修改。
(2)Context 类负责创建策略子类对象的情况。
将创建策略子类对象的责任交给 Context 类,而客户类 Client 只为 Context 类提供一些代表客户请求的参数。在此情况下,Context 类在创建策略子类的对象时,必然会使用与策略子类有关的条件语句。此时,修改一个策略子类不需要修改客户类与 Context 类。而在添加一个新的策略子类时,如果此时客户类暂时不想使用该新的子类,则新子类的添加不会影响客户类与 Context 类的源代码。但是,如果客户类要使用新的策略子类,则必须同时在客户类与 Context 类中添加新的条件分支,也就是说,需要同时修改客户类与 Context 类。
在以上两种情况下,当需要修改策略子类的代码时,客户类与 Context 类都不需要进行修改。
综上所述,由客户类创建的对象的设计可扩展性好一些。这样,可以做到在 Context 类中出现与策略子类相关的条件语句,从而可扩展性也得到了提高。
from abc import ABC, abstractmethod
class Strategy(ABC):
@abstractmethod
def algorithm_interface(self, context):
pass
class ConcreteStrategyA(Strategy):
def algorithm_interface(self, context):
print('ConcreteStrategyA')
class ConcreteStrategyB(Strategy):
def algorithm_interface(self, context):
print('ConcreteStrategyB')
class ConcreteStrategyC(Strategy):
def algorithm_interface(self, context):
print('ConcreteStrategyC')
class Context:
def __init__(self, strategy):
self.strategy = strategy
def context_interface(self):
self.strategy.algorithm_interface(self)
class Client:
@staticmethod
def main():
strategy = eval(f'ConcreteStrategy{input()}()')
context = Context(strategy)
context.context_interface()
if __name__ == '__main__':
Client.main()
今天的文章有不懂的可以加群,群号:822163725,备注:小陈学Python,不备注可是会被拒绝的哦~!