【Design Pattern】Behavioural - Strategy

Posted by 西维蜀黍 on 2018-11-11, Last Modified on 2023-08-23

动机

  • 完成一项任务可以有多种不同的方法,每一种方法都可称为一个策略。在特定环境下,我们可以根据不同的需求和具体的环境来决定采用哪一个策略来完成特定的任务;
  • 在软件开发中也是类似的,我们可以使用不同的策略来实现一项功能。比如排序,可以**采用快排(Quick Sort),插入排序(Insert Sort)或选择排序(Selection Sort)**等等。这些不同的排序算法都能实现排序功能,只是他们进行排序时的效率不同。因此,我们要根据具体的情况,来具体选择一个排序算法,以使得在当前情况中排序效率最高;
  • 为了使系统具有较强的扩展性(可能在未来,我们还需要增加新的排序算法),我们的代码需要遵循开闭原则(The Open Closed Principle)
  • 同时,我们编写的这个类对应的调用者(Client),不应该依赖于这个类具体的实现(Implementation),而仅仅依赖于对应的抽象(Abstraction)。或者说,面向接口编程(Program to an interface, not an implementation);
  • 可能有人会提出,我们可以编写一个SortingHelper类(这个类实现了ISortingHelper接口),这个类中包含不同的方法,每个方法都是一种具体排序算法的实现(比如快排、插入排序等等)。很棒!这是一种很好的实现(具有扩展性,同时面向接口编程)。这种实现事实上对应了另一个设计模式,称为模板方法模式(Template Method Pattern)。但这样的实现无法做到在运行时可以任意地切换不同的排序算法;
  • 我们也可以设计一个策略接口(对应于上面例子,则为排序帮助接口),同时包含多个实现了该接口的策略实现类(对应于上面例子,则为XX排序帮助类,如插入排序帮助类)。

Problem

One day you decided to create a navigation app for casual travelers. The app was centered around a beautiful map which helped users quickly orient themselves in any city.

One of the most requested features for the app was automatic route planning. A user should be able to enter an address and see the fastest route to that destination displayed on the map.

The first version of the app could only build the routes over roads. People who traveled by car were bursting with joy. But apparently, not everybody likes to drive on their vacation. So with the next update, you added an option to build walking routes. Right after that, you added another option to let people use public transport in their routes.

However, that was only the beginning. Later you planned to add route building for cyclists. And even later, another option for building routes through all of a city’s tourist attractions.

The code of the navigator became bloated.

While from a business perspective the app was a success, the technical part caused you many headaches. Each time you added a new routing algorithm, the main class of the navigator doubled in size. At some point, the beast became too hard to maintain.

Any change to one of the algorithms, whether it was a simple bug fix or a slight adjustment of the street score, affected the whole class, increasing the chance of creating an error in already-working code.

In addition, teamwork became inefficient. Your teammates, who had been hired right after the successful release, complain that they spend too much time resolving merge conflicts. Implementing a new feature requires you to change the same huge class, conflicting with the code produced by other people.

Solution

The Strategy pattern suggests that you take a class that does something specific in a lot of different ways and extract all of these algorithms into separate classes called strategies.

The original class, called context, must have a field for storing a reference to one of the strategies. The context delegates the work to a linked strategy object instead of executing it on its own.

The context isn’t responsible for selecting an appropriate algorithm for the job. Instead, the client passes the desired strategy to the context. In fact, the context doesn’t know much about strategies. It works with all strategies through the same generic interface, which only exposes a single method for triggering the algorithm encapsulated within the selected strategy.

This way the context becomes independent of concrete strategies, so you can add new algorithms or modify existing ones without changing the code of the context or other strategies.

In our navigation app, each routing algorithm can be extracted to its own class with a single buildRoute method. The method accepts an origin and destination and returns a collection of the route’s checkpoints.

Even though given the same arguments, each routing class might build a different route, the main navigator class doesn’t really care which algorithm is selected since its primary job is to render a set of checkpoints on the map. The class has a method for switching the active routing strategy, so its clients, such as the buttons in the user interface, can replace the currently selected routing behavior with another one.

定义

策略模式中定义了一组可以相互替换(interchangeable)的策略(Strategy),每一个独立的策略都封装了一个算法(如排序)。

特点

  • 包括一系列可以相互替换(interchangeable)的算法,这些算法的功能都是相同的,但实现的思路不同。如不同的排序算法(插入排序Insertion Sort,选择排序Selection Sort,合并排序Merge Sort),而排序后的结果都是相同的
  • 每一个算法都可以被单独使用
  • 调用者(Client)可以根据自己的需求决定采用哪一个具体的策略
  • 只有在运行阶段(rum time),才确定哪个具体的策略被采用

构成

  • 一个策略接口(Strategy Interface)
  • 一个或多个具体的策略实现类(Concrete Strategy),且实现了策略接口
  • 一个策略上下文对象,以允许调用者指定特定的策略

UML图

示例

实现思路

  • 定义一个策略接口(Strategy Interface),这个接口定义的策略类的行为
  • 定义一个或多个具体的实现策略类(Concrete Strategy),这些类均实现了策略接口
  • 定义一个策略上下文对象,其中包含settergetter方法用于指定一个具体的实现策略类

UML图

SortingStrategy.java

public interface SortingStrategy {
    public void sort(int[] numbers);
}

SelectionSort.java

public class SelectionSort implements SortingStrategy {

    @Override
    public void sort(int[] numbers) {
        System.out.println("Selection Sort!");

        int i, j, first, temp;
        for (i = numbers.length - 1; i > 0; i--) {
            first = 0;
            for (j = 1; j <= i; j++) {
                if (numbers[j] > numbers[first])
                    first = j;
            }
            temp = numbers[first];
            numbers[first] = numbers[i];
            numbers[i] = temp;
        }
        
        System.out.println(Arrays.toString(numbers));
    }
}

InsertionSort.java

public class InsertionSort implements SortingStrategy {

    @Override
    public void sort(int[] numbers) {
        System.out.println("Insertion Sort!");

        for (int i = 1; i < numbers.length; i++) {
            int temp = numbers[i];
            int j;
            for (j = i - 1; (j >= 0) && (numbers[j] > temp); j--) {
                numbers[j + 1] = numbers[j];
            }
            numbers[j + 1] = temp;
        }

        System.out.println(Arrays.toString(numbers));
    }
}
public class SortingContext {
    
    private SortingStrategy strategy;
    
    public void setSortingMethod(SortingStrategy strategy) {
        this.strategy = strategy;
    }
    
    public SortingStrategy getStrategy() {
        return strategy;
    }
    
    public void sortNumbers(int[] numbers){
        strategy.sort(numbers);
    }
}

TestMain.java

调用者使用策略模式的方式:

public class TestMain {
    public static void main(String[] args) {
        int numbers[] = {20, 50, 15, 6, 80};
        
        SortingContext context = new SortingContext();
        context.setSortingMethod(new InsertionSort());
        context.sortNumbers(numbers);
        
        System.out.println("***********");
        context.setSortingMethod(new SelectionSort());
        context.sortNumbers(numbers);
        
        // 输出:
        Insertion Sort!
        [6, 15, 20, 50, 80]
        ***********
        Selection Sort!
        [6, 15, 20, 50, 80]
    }
}

讨论

可能有人会提出,为什么不采用最简单的switch表达式的方式来替代上面的策略模式:

enum SortingMethod{
    InsertionSort,
    SelectionSort
}

public class SortingHelper {
    public void sort(int[] numbers, SortingMethod sortEnum) {
        switch(sortEnum){
            case SortingMethod.InsertionSort:
                // 进行插入排序
                break;
            case SortingMethod.SelectionSort:
                // 进行选择排序
                break;
        }
    }

事实上,如果采用这种方案实现,当我们需要增加新的排序策略时,不得不在switch中增加新的case这样会违背开闭原则(Open-Closed Principle)。

优缺点

优点

  • 在运行时任意切换策略
  • 可轻易地增加新的策略(通过增加一个新的策略实现类)

缺点

  • 客观上增加了代码的复杂性,有几个策略,至少就有几个类

策略模式在Java中的应用

使用策略模式的例子主要在Java.awt和Swing库中看到。

AWT中的LayoutManager

Java.awt类库需要在运行期间动态的由客户端决定一个Container对象怎样排列它所有的GUI构件。Java语言提供了几种不同的排列方式,包装在不同的类里:

  • BorderLayout
  • FlowLayout
  • GridLayout
  • GridBagLayout
  • CardLayout

其类图如下:

Swing中的Border

在任何一个Swing构件上都可以画上边框(Border),比如panel、button等,而Swing库提供了很多的边框类型,包括bevel、line、titled以及CompoundBorder类等,Swing构件的基类是JComponent类,而这个类负责为Swing构件画上边框。

JComponent类实现了paintBorder()方法,并且保持一个私有的对边框对象的引用。由于Border是一个接口而不是具体类,因此,这个引用可以指向任何实现了Border借口的边框对象。其类图如下:

在什么情况下使用策略模式

  1. 如果在一个系统里面有许多类,他们之间的区别仅仅在于他们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  2. 一个系统需要动态的在几种算法中选择一种。那么这些算法可以包装到一个个的具体算法里面,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,由于多态性原则,客户端可以选择使用任何一个具体算法类,并只持有一个数据类型是抽象算法类的对象。
  3. 一个系统的算法使用的数据不可以让客户端知道。策略模式可以避免让客户端涉及到不必要接触到的复杂的和只与算法有关的数据。
  4. 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。使用策略模式可以避免使用难以维护的多重条件选择语句。

Reference