名称
策略模式(STRATEGY),别名:政策(Policy)
目的
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
适用性
当存在以下情况时使用Strategy模式:
许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
需要使用一个算法的不同变体。例如,会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时,可以使用策略模式。
隐藏不应该让客户知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
结构
策略(Strategy):定义所有支持的算法的公共接口。Context使用这个接口来调用某ConcreteStrategy定义的算法。
具体策略(ConcreteStrategy):以Strategy接口实现某具体算法。
上下文(Context):
用一个ConcreteStrategy对象来配置。
维护一个对Strategy对象的引用。
可定义一个接口来让Stategy访问它的数据。
协作
Strategy和Context相互作用以实现选定的算法。当算法被调用时,Context可以将该算法所需要的所有数据都传递给该Stategy。或者,Context可以将自身作为一个参数传递给Strategy操作。这就让Strategy在需要时可以回调Context。
Context将它的客户的请求转发给它的Strategy。客户通常创建并传递一个ConcreteStrategy对象给该Context;这样,客户仅与Context交互。通常有一系列的ConcreteStrategy类可供客户从中选择。
效果
优点
Strategy类层次为Context定义了一系列的可供重用的算法或行为,继承有助于析取出这些算法中的公共功能。
作为一个替代继承的方法。继承提供了另一种支持多种算法或行为的方法,可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为强行编织到Context中,导致算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法,最后得到一堆相关的类,它们唯一差别是它们所使用的算法或行为。将算法封装在独立的Strategy类的好处就是可以独立于其Context来改变它,使它易于切换、易于理解、易于扩展。
消除了一些条件语句。Strategy模式提供了用条件语句选择所需的行为以外的另一种选择,当不同的行为堆砌在一个类中时,很难避免需要使用条件语句来选择合适的行为,将行为封装在一个个独立的Strategy类中消除了这些条件语句。含有许多条件语句的代码通常意味着需要使用Strategy模式。
可以选择行为的具体实现。Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间/空间权衡取舍要求从不同策略中进行选择。
缺点
客户必须了解不同的Strategy。本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同,此时可能不得不向客户暴露具体的实现问题。
Strategy和Context之间的通信开销。无论各个ConcreteStrategy实现的算法是简单还是复杂,它们都共享Strategy定义的接口。因此很可能某些ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题,那么将需要在Strategy和Context之间更进行紧密的耦合(比如直接context直接传具体参数给strategy,这样扩展性就不高了)。
增加了对象的数目。Strategy增加了一个应用中的对象的数目。有时可以将Strategy实现为可供各Context共享的无状态的对象来减少这一开销,任何其余的状态都由Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去,共享的Stragey不应在各次调用之间维护状态,享元模式(Flyweight)更详细地描述了这一方法。
实现
定义Strategy和Context接口。Strategy和Context接口必须使得ConcreteStrategy能够有效的访问它所需要的Context中的任何数据,反之亦然。有下列三种方式(代码示例中test1、test2、test3分别讲解):
context通过参数传递数据给strategy。(推模型)
context通过context传递数据给strategy。(拉模型)
无需传递数据,context通过ConcreteStrategy中的context引用传递数据给strategy,这样做的前提是ConcreteStrategy必须有context的对象引用,换句话说,也就是他们互相依赖了。(拉模型)
将Strategy作为模板参数。可利用模板机制用一个Strategy来配置一个类,然而这种技术仅当下面条件满足时才可以使用(1)可以在编译时选择Strategy(2)它不需在运行时改变。在这种情况下,要被配置的类(如Context)被定义为以一个Strategy类作为一个参数的模板类。使用模板不再需要定义给Strategy定义接口的抽象类,把Strategy作为一个模板参数也使得可以将一个Strategy和它的Context静态地绑定在一起,从而提高效率。下面是关于这句话的代码示例
/** * 策略接口 */ public interface Strategy<T> { T execute(T data); } /** * 具体策略A */ public class ConcreteStrategyA implements Strategy<Integer> { @Override public Integer execute(Integer data) { return data * 2; // 具体的策略实现 } } /** * 具体策略B */ public class ConcreteStrategyB implements Strategy<Integer> { @Override public Integer execute(Integer data) { return data + 10; // 另一种策略实现 } } /** * 上下文 */ public class Context<T, S extends Strategy<T>> { private final S strategy; public Context(S strategy) { this.strategy = strategy; } public T execute(T data) { return strategy.execute(data); } } /** * 上下文 */ public class Client{ public static void main(String[] args) { // 使用策略A Strategy<Integer> strategyA = new ConcreteStrategyA(); Context<Integer, Strategy<Integer>> contextA = new Context<>(strategyA); System.out.println("Result with Strategy A: " + contextA.execute(5)); // 输出 10 // 使用策略B Strategy<Integer> strategyB = new ConcreteStrategyB(); Context<Integer, Strategy<Integer>> contextB = new Context<>(strategyB); System.out.println("Result with Strategy B: " + contextB.execute(5)); // 输出 15 } }
使Strategy对象成为可选项。如果即使在不使用额外的Strategy对象的情况下,Context也还有意义的话,那么它还可以被简化。Context在访问某Strategy前先检查它是否存在,如果有,那么就使用它;如果没有,那么Context执行缺省的行为。这种方法的好处是客户根本不需要处理Strategy对象,除非它们不喜欢缺省的行为。java中可以使用抽象类来定义缺省行为,这样具体策略相当于if,抽象类中的方法相当于else。
应用
问题
编写一个排序工具类,可以根据不同的策略实现排序(结构算法中排序的方式有很多,比如选择排序、冒泡排序、插入排序),为了实现能任意切换排序策略,我们使用策略模式对这一功能进行设计。首先定义一个排序策略为抽象策略(如果要定义缺省行为使用抽象类,这里是接口),然后定义选择排序策略、冒泡排序策略、插入排序策略为具体策略,为了能拉取到上下文的数据,我们在具体策略中保留一个上下文引用(有些方法用不着);最后定义上下文,结构很简单,一个策略引用,一个存储数据的数组,然后是三个方法分别调用策略的三个方法,根据传入的具体策略决定每个方法执行的具体策略方法。
示例
UML
代码示例
策略:排序策略,定义三个方法,用来演示“实现”小节的传递数据方式
package com.ysj.part5.strategy.strategy;
import com.ysj.part5.strategy.context.ArrayHandler;
/**
* 策略:排序策略
*/
public interface Sort {
/**
* 排序(context通过参数传递数据给strategy)
* @param arr
* @return
*/
int[] sort(int[] arr);
/**
* 排序(context通过context传递数据给strategy)
* @param arrayHandler
* @return
*/
int[] sort(ArrayHandler arrayHandler);
/**
* 排序(无需传递数据,context通过ConcreteStrategy中的context引用传递数据给strategy)
* @return
*/
int[] sort();
}
具体策略:冒泡排序策略、插入排序策略、选择排序策略,在各个策略中分别使用冒泡、插入、选择排序算法实现排序,arrayHandler引用只在演示第三个方法的时候用到了
package com.ysj.part5.strategy.concreteStrategy;
import com.ysj.part5.strategy.context.ArrayHandler;
import com.ysj.part5.strategy.strategy.Sort;
/**
* 具体策略:冒泡排序策略
*/
public class BubbleSort implements Sort {
private ArrayHandler arrayHandler;
public BubbleSort() {
}
public BubbleSort(ArrayHandler arrayHandler) {
this.arrayHandler = arrayHandler;
}
public int[] sort(int[] arr) {
int len = arr.length;
for (int i = 0; i < len; i++) {
for (int j = i + 1; j < len; j++) {
int temp;
if (arr[i] > arr[j]) {
temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
}
System.out.println("冒泡排序");
return arr;
}
@Override
public int[] sort(ArrayHandler arrayHandler) {
return sort(arrayHandler.getArr());
}
@Override
public int[] sort() {
return sort(arrayHandler.getArr());
}
}
package com.ysj.part5.strategy.concreteStrategy;
import com.ysj.part5.strategy.context.ArrayHandler;
import com.ysj.part5.strategy.strategy.Sort;
/**
* 具体策略:插入排序策略
*/
public class InsertionSort implements Sort {
private ArrayHandler arrayHandler;
public InsertionSort() {
}
public InsertionSort(ArrayHandler arrayHandler) {
this.arrayHandler = arrayHandler;
}
public int[] sort(int[] arr) {
int len = arr.length;
for (int i = 1; i < len; i++) {
int j;
int temp = arr[i];
for (j = i; j > 0; j--) {
if (arr[j - 1] > temp) {
arr[j] = arr[j - 1];
} else
break;
}
arr[j] = temp;
}
System.out.println("插入排序");
return arr;
}
@Override
public int[] sort(ArrayHandler arrayHandler) {
return sort(arrayHandler.getArr());
}
@Override
public int[] sort() {
return sort(arrayHandler.getArr());
}
}
package com.ysj.part5.strategy.concreteStrategy;
import com.ysj.part5.strategy.context.ArrayHandler;
import com.ysj.part5.strategy.strategy.Sort;
/**
* 具体策略:选择排序策略
*/
public class SelectionSort implements Sort {
private ArrayHandler arrayHandler;
public SelectionSort() {
}
public SelectionSort(ArrayHandler arrayHandler) {
this.arrayHandler = arrayHandler;
}
public int[] sort(int[] arr) {
int len = arr.length;
int temp;
for (int i = 0; i < len; i++) {
temp = arr[i];
int j;
int samllestLocation = i;
for (j = i + 1; j < len; j++) {
if (arr[j] < temp) {
temp = arr[j];
samllestLocation = j;
}
}
arr[samllestLocation] = arr[i];
arr[i] = temp;
}
System.out.println("选择排序");
return arr;
}
@Override
public int[] sort(ArrayHandler arrayHandler) {
return sort(arrayHandler.getArr());
}
@Override
public int[] sort() {
return sort(arrayHandler.getArr());
}
}
上下文:数组处理器
package com.ysj.part5.strategy.context;
import com.ysj.part5.strategy.strategy.Sort;
import lombok.Getter;
import lombok.Setter;
/**
* 上下文:数组处理器
*/
@Setter
@Getter
public class ArrayHandler {
/**
* 排序策略
*/
private Sort sortStrategy;
/**
* 排序数组(用于传递参数)
*/
private int[] arr;
public int[] sort(int[] arr) {
// 直接将参数传给strategy
sortStrategy.sort(arr);
return arr;
}
public int[] sortByHandler(int[] arr) {
// strategy使用context获取参数必须在context中给arr赋值
this.arr = arr;
sortStrategy.sort(this);
return arr;
}
public int[] sortByReference(int[] arr) {
// strategy使用context引用获取参数必须在context中给arr赋值
this.arr = arr;
sortStrategy.sort();
return arr;
}
}
客户端:客户
package com.ysj.part5.strategy;
import com.ysj.part5.strategy.concreteStrategy.SelectionSort;
import com.ysj.part5.strategy.context.ArrayHandler;
import com.ysj.part5.strategy.strategy.Sort;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
/**
* 策略模式
* 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化
*/
public class Client {
public static ArrayHandler arrayHandler;
private static int[] arr;
/**
* 使用静态代码块初始化数据
*/
static {
arr = new int[]{1, 4, 6, 2, 5, 3, 7, 10, 9};
arrayHandler = new ArrayHandler();
}
/**
* 排序(context通过参数传递数据给strategy)
*/
@Test
public void test1() {
//数据通过参数传递
arrayHandler.setSortStrategy(new SelectionSort());
int[] result = arrayHandler.sort(arr);
// 打印结果
Arrays.stream(result).forEach(System.out::println);
}
/**
* 排序(context通过context传递数据给strategy)
*/
@Test
public void test2() {
arrayHandler.setSortStrategy(new SelectionSort());
int[] result = arrayHandler.sortByHandler(arr);
// 打印结果
Arrays.stream(result).forEach(System.out::println);
}
/**
* 排序(无需传递数据,context通过ConcreteStrategy中的context引用传递数据给strategy)
*/
@Test
public void test3() {
arrayHandler.setSortStrategy(new SelectionSort(arrayHandler)); //设置具体策略
int[] result = arrayHandler.sortByReference(arr);
// 打印结果
Arrays.stream(result).forEach(System.out::println);
}
}
已知应用
Spring Security
Spring Security中很多地方使用了策略模式,比如SessionAuthenticationStrategy、InvalidSessionStrategy,用于在不同情况使用不同的实现,还有授权时使用不同的授权策略AbstractAuthorizationStrategy。
Spring Framework
Spring 支持策略模式,可以通过配置文件或注解来定义和选择不同的策略。比如两个实现类共同实现一个接口,在其他地方引入这个接口的时候可以使用@Qualifier指定具体实现类。
评论区