编程

设计模式之策略(Strategy)模式

810 2024-02-21 23:52:00

意图

策略(Strategy)模式是一种行为设计模式,允许你定义一系列算法,将每个算法放入一个单独的类中,并使它们的对象可互换。

问题描述

有一天,你决定为休闲旅行者创建一个导航应用。该应用以一张美丽的地图为中心,帮助用户在任何城市快速定位。

该应用最受欢迎的功能之一是自动路线规划。用户应该能够输入地址,并在地图上看到到达该目的地的最快路线。

该应用的第一个版本只能在道路上构建路线。开车旅行的人们欣喜若狂。但显然,并不是每个人都喜欢在度假时开车。因此,在下一次更新中,你添加了一个构建步行路线的选项。紧接着,你又增加了一个选项,让人们在自己的路线上使用公共交通工具。

然而,这仅仅是一个开始。后来,你计划为骑自行车的人添加路线构建。甚至在以后,还有另一种选择,可以修建穿过城市所有旅游景点的路线。

The code of the navigator became very bloated
The code of the navigator became bloated.

虽然从商业角度来看,该应用是成功的,但技术部分却让你头疼不已。每次添加新的路由算法时,导航器的主类的大小都会翻倍。在某种程度上,野兽变得难以维持。

对其中一种算法的任何更改,无论是简单的错误修复还是对街头评分的轻微调整,都会影响到整个类,增加在已经工作的代码中产生错误的机会。

此外,团队合作变得效率低下。你的队友在成功发布后就被雇佣了,他们抱怨花了太多时间来解决合并冲突。实现一个新特性需要更改同一个巨大的类,这与其他人生成的代码相冲突。

方案

策略(Strategy)模式建议你选择一个以多种不同方式执行特定操作的类,并将所有这些算法提取到称为策略的单独类中。

叫做 context 的原始类必须有一个字段,用于存储对其中一个策略的引用。上下文(context)将工作委托给一个链接的战略对象,而不是自己去执行。

上下文(context)类不负责选择合适的算法。相反,客户端(client)将所需的策略传递给上下文(context)。事实上,上下文(context)对策略了解不多。它通过相同的通用接口与所有策略一起工作,该接口只暴露一个用于触发所选策略中封装的算法的方法。

通过这种方式,上下文(context)变得独立于具体策略,因此你可以以添加新算法或修改现有算法,而无需更改上下文代码或其他策略。

 

Route planning strategies
Route planning strategies.

在我们的导航应用中,每个路由算法都可以通过一个 buildRoute 方法提取到自己的类中。该方法接受来源和目的地,并返回路由的检查点的集合。

尽管给定相同的参数,每个路由类可能会构建不同的路由,但主导航器类并不真正关心选择了哪种算法,因为它的主要工作是在地图上渲染一组检查点。该类有一个切换活动路由策略的方法,因此客户端(如用户界面中的按钮)可以用另一个替换当前选择的路由行为。

真实世界类比

Various transportation strategies
Various strategies for getting to the airport.

假设要去机场。你可以搭公交车,叫出租车,或者骑自行车。这些是你的交通策略。你可以根据预算或时间限制等因素选择其中一种策略。

结构

上下文(Context)维护对其中一个具体策略的引用,并仅通过策略接口与该对象进行通信。

战略(Strategy)接口是所有具体战略的通用接口。它声明了上下文用于执行策略的方法。

具体策略(Concrete Strategy)实现上下文使用的算法的不同变体。

每次需要运行算法时,上下文(context)都会调用链接策略对象上的执行方法。上下文不知道它使用什么类型的策略,也不知道算法是如何执行的。

客户端(Client)创建一个特定的策略对象并将其传递给上下文。上下文暴露了一个 setter,它允许客户端在运行时替换与上下文相关联的策略。

伪代码

本例中,上下文使用多个策略执行多种算术操作。

// The strategy interface declares operations common to all
// supported versions of some algorithm. The context uses this
// interface to call the algorithm defined by the concrete
// strategies.
interface Strategy is
    method execute(a, b)
// Concrete strategies implement the algorithm while following
// the base strategy interface. The interface makes them
// interchangeable in the context.
class ConcreteStrategyAdd implements Strategy is
    method execute(a, b) is
        return a + b
class ConcreteStrategySubtract implements Strategy is
    method execute(a, b) is
        return a - b
class ConcreteStrategyMultiply implements Strategy is
    method execute(a, b) is
        return a * b
// The context defines the interface of interest to clients.
class Context is
    // The context maintains a reference to one of the strategy
    // objects. The context doesn't know the concrete class of a
    // strategy. It should work with all strategies via the
    // strategy interface.
    private strategy: Strategy
    // Usually the context accepts a strategy through the
    // constructor, and also provides a setter so that the
    // strategy can be switched at runtime.
    method setStrategy(Strategy strategy) is
        this.strategy = strategy
    // The context delegates some work to the strategy object
    // instead of implementing multiple versions of the
    // algorithm on its own.
    method executeStrategy(int a, int b) is
        return strategy.execute(a, b)
// The client code picks a concrete strategy and passes it to
// the context. The client should be aware of the differences
// between strategies in order to make the right choice.
class ExampleApplication is
    method main() is
        Create context object.
        Read first number.
        Read last number.
        Read the desired action from user input.
        if (action == addition) then
            context.setStrategy(new ConcreteStrategyAdd())
        if (action == subtraction) then
            context.setStrategy(new ConcreteStrategySubtract())
        if (action == multiplication) then
            context.setStrategy(new ConcreteStrategyMultiply())
        result = context.executeStrategy(First number, Second number)
        Print result.

适用

当你想在对象中使用算法的不同变体,并能够在运行时从一种算法切换到另一种算法时,请使用策略模式。

策略模式允许你在运行时通过将对象与不同的子对象关联来间接更改对象的行为,这些子对象可以以不同的方式执行特定的子任务。

当你有很多类似的类,但它们执行某些行为的方式不同时,请使用策略模式。

策略模式允许你将不同的行为提取到一个单独的类层次结构中,并将原始类组合为一个,从而减少重复代码。

使用该模式能将类的业务逻辑与算法的实现细节隔离开来,这些细节在该逻辑的上下文中可能不那么重要。

策略模式允许你将代码、内部数据和各种算法的依赖关系与代码的其余部分隔离开来。各种客户端都有一个简单的接口来执行算法并在运行时切换它们。

当类使用复杂的条件语句在同一算法的不同变体之间切换时,请使用该模式。

策略模式允许你通过将所有算法提取到单独的类中来消除这种条件,所有这些类都实现相同的接口。原始对象将执行委托给其中一个对象,而不是实现算法的所有变体。

如何实现

  1. 在上下文类中,识别一个容易频繁更改的算法。它也可能是一个大的条件,在运行时选择并执行相同算法的变体。
  2. 声明算法的所有变体通用的策略接口。
  3. 一个接一个,将所有算法提取到它们自己的类中。它们都应该实现战略接口。
  4. 在上下文类中,添加一个字段,用于存储对策略对象的引用。提供一个用于替换该字段值的setter。上下文只能通过策略接口与策略对象一起使用。上下文可以定义允许策略访问其数据的接口。
  5. 上下文的客户端必须将其与合适的策略相关联,该策略与他们期望上下文执行其主要工作的方式相匹配。

优缺点

  • ✔️你可以在运行时交换对象内部使用的算法。
  • ✔️你可以将算法的实现细节与使用它的代码隔离开来。
  • ✔️你可以使用组合替换继承。
  • ✔️开闭原则。你可以引入新的策略,而不必改变上下文。
  • ❌如果只有几个算法,而且它们很少更改,那就没有真正的理由用模式附带的新类和接口来过度复杂化程序
  • ❌客户端必须了解不同策略的不同之处,才能选择合适的策略。
  • ❌许多现代编程语言都支持函数类型,可以在一组匿名函数中实现不同版本的算法。然后,你可以像使用策略对象一样使用这些函数,但不会因为额外的类和接口而使代码膨胀。

与其他模式的关系

  • 桥接模式、状态模式、策略模式(以及在某种程度上的适配器模式)具有非常相似的结构。事实上,所有这些模式都是基于组合模式的,即将工作委派给其他对象。然而,它们都各自解决不同的问题。模式不仅仅是以特定方式构建代码的配方。你还可以与其他开发人员交流模式所解决的问题。
  • 命令模式策略模式可能看起来很像,因为两者都能通过某些动作来参数化对象。 但是, 它们的意图有非常大的不同。
    • 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
    • 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
  • 装饰器模式可让你更改对象的外表, 策略则让你能够改变其本质。
  • 模板方法模式基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。
  • 状态模式可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。

 

代码示例

index.php: 概念示例

<?php
namespace RefactoringGuru\Strategy\Conceptual;
/**
 * The Context defines the interface of interest to clients.
 */
class Context
{
    /**
     * @var Strategy The Context maintains a reference to one of the Strategy
     * objects. The Context does not know the concrete class of a strategy. It
     * should work with all strategies via the Strategy interface.
     */
    private $strategy;
    /**
     * Usually, the Context accepts a strategy through the constructor, but also
     * provides a setter to change it at runtime.
     */
    public function __construct(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }
    /**
     * Usually, the Context allows replacing a Strategy object at runtime.
     */
    public function setStrategy(Strategy $strategy)
    {
        $this->strategy = $strategy;
    }
    /**
     * The Context delegates some work to the Strategy object instead of
     * implementing multiple versions of the algorithm on its own.
     */
    public function doSomeBusinessLogic(): void
    {
        // ...
        echo "Context: Sorting data using the strategy (not sure how it'll do it)\n";
        $result = $this->strategy->doAlgorithm(["a", "b", "c", "d", "e"]);
        echo implode(",", $result) . "\n";
        // ...
    }
}
/**
 * The Strategy interface declares operations common to all supported versions
 * of some algorithm.
 *
 * The Context uses this interface to call the algorithm defined by Concrete
 * Strategies.
 */
interface Strategy
{
    public function doAlgorithm(array $data): array;
}
/**
 * Concrete Strategies implement the algorithm while following the base Strategy
 * interface. The interface makes them interchangeable in the Context.
 */
class ConcreteStrategyA implements Strategy
{
    public function doAlgorithm(array $data): array
    {
        sort($data);
        return $data;
    }
}
class ConcreteStrategyB implements Strategy
{
    public function doAlgorithm(array $data): array
    {
        rsort($data);
        return $data;
    }
}
/**
 * The client code picks a concrete strategy and passes it to the context. The
 * client should be aware of the differences between strategies in order to make
 * the right choice.
 */
$context = new Context(new ConcreteStrategyA());
echo "Client: Strategy is set to normal sorting.\n";
$context->doSomeBusinessLogic();
echo "\n";
echo "Client: Strategy is set to reverse sorting.\n";
$context->setStrategy(new ConcreteStrategyB());
$context->doSomeBusinessLogic();

Output.txt: 执行结果

Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e
Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a