在平时的编码过程中,我们往往会遇到一个接口的实现会有多种,不同的实现(不同的策略)使用在不同的场景中。举个简单的场景,项目中使用到的站内消息,站内消息的存储介质我们可以是关系型数据库(又分不同的厂商),也可以是非关系型数据库。比如新增插入一条消息这个方法,他的实现方式可能就会很多,如我们现实为了满足不同的客户,关系型数据库使用的有mysql、oracel有些客户则使用的是mongonDB非关系型数据库,客户根据自己的需求,我们动态去调整切换,而无需重新开发。
不单单是代码层面,比如每年的双十一,我们买一件商品;这几年商家搞得五花八门的优惠方案供你选择。优惠券的组合方案,什么跨店满减、定金抵现金、成为本店会员享受商品几折的优惠等等,往往我们会综合考虑,考某种方式最省钱,我们会倾向于某种方式。今天介绍的策略模式,能很好解决我们上面生活中,或者软件设计的过程中所遇到这些问题。
定义及结构特点
策略模式,英文全称是Strategy Design Pattern。在GoF的《设计模式》一书中,它是这样定义的: 定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客戶端(这里的客戶端代指使用算法的代码)。对应我们前面所讲的例子就是我们的消息写入实现。 策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的结构与实现
策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性,现在我们来分析其基本结构和实现方法。
- 模式的结构
- 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
2.策略模式结构图
代码案例
1.案例1
- 抽象策略
public interface AbstractStrategy {
// 可以是接口或者抽象类的抽象方法
void operation();
}
- 具体策略1
public class ConcreteStrategy1 implements AbstractStrategy{
@Override
public void operation() {
System.out.println("策略1具体实现");
}
}
- 具体策略2
public class ConcreteStrategy2 implements AbstractStrategy{
@Override
public void operation() {
System.out.println("策略2具体实现");
}
}
- 策略环境
public class Context {
private AbstractStrategy strategy;
public AbstractStrategy getStrategy() {
return strategy;
}
public void setStrategy(AbstractStrategy strategy) {
this.strategy = strategy;
}
public void doStrategy(){
strategy.operation();
}
}
- 客户端
public class Client {
public static void main(String[] args) {
ConcreteStrategy1 concreteStrategy1 = new ConcreteStrategy1();
// 设置指定策略
Context context = new Context();
context.setStrategy(concreteStrategy1);
// 调用
context.doStrategy();
System.out.println("-------重新设置--------");
ConcreteStrategy2 concreteStrategy2 = new ConcreteStrategy2();
context.setStrategy(concreteStrategy2);
context.doStrategy();
}
}
执行结果:
-------重新设置--------
策略2具体实现
通过在环境上下文中设置不同的策略实现,来切换策略的实现,对应前面的概念定义:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。体会一下
2.案例2
案例2,来举一个实际场景的案例,假如现在需要做一份虾。每个人的口味不同,可能去吃饭时候点的做法也不同。这里我们吧厨师的对虾的烹饪方式,作为策略的抽象。不同的做法为抽象策略的具体实现(不同算法)。
- 抽象策略
//抽象策略
public interface AbstractShrimpCook {
// 大虾烹饪方式
void cook();
}
- 具体实现A
public class BraiseShrimp implements AbstractShrimpCook{
@Override
public void cook() {
System.out.println("红烧大虾...");
}
}
- 具体实现B
public class PoachShrimp implements AbstractShrimpCook {
@Override
public void cook() {
System.out.println("水煮大虾...");
}
}
- 具体实现C
//清蒸
public class SteamedShrimp implements AbstractShrimpCook {
public void cook() {
System.out.println("清蒸大虾...");
}
}
- 环境信息
public class Context {
AbstractShrimpCook abstractShrimpCook;
public AbstractShrimpCook getAbstractShrimpCook() {
return abstractShrimpCook;
}
public void setAbstractShrimpCook(AbstractShrimpCook abstractShrimpCook) {
this.abstractShrimpCook = abstractShrimpCook;
}
// 调用
public void cook() {
abstractShrimpCook.cook();
}
}
- 客户端
public class Client {
public static void main(String[] args) throws Exception {
// 水煮
String cookType = "POACH";
AbstractShrimpCook abstractShrimpCook;
if ("POACH".equals(cookType)) {
abstractShrimpCook = new PoachShrimp();
}
else if ("BRAISE".equals(cookType)) {
abstractShrimpCook = new BraiseShrimp();
}
else if ("STEAMED".equals(cookType)) {
abstractShrimpCook = new SteamedShrimp();
} else {
throw new Exception("未找到指定类型策略");
}
Context context = new Context();
context.setAbstractShrimpCook(abstractShrimpCook);
context.cook();
}
}
这里我们模拟程序的调用方,根据传递的指定类型,来获取具体的策略即抽象策略的具体实现算法。
这里有个严重的问题,作为使用方来说,我们可以看出并不是无需关注策略的实现方式。具体使用的策略是根据执行的策略类型决定的。如果增加新的实现,使用方也需要增加新的ifelse判断逻辑。这也违背了策略模式的精髓。无需重点关注策略的实现,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性。
下面将案例2改造一下;将context改造为一下方式,并增加一个烹饪类型枚举对象
- 枚举对象
public enum CookType {
POACH,
BRAISE,
STEAMED;
}
- context
public class Kitchen {
AbstractShrimpCook abstractShrimpCook;
//策略缓存
static final Map<CookType,AbstractShrimpCook> cookMap = new HashMap<CookType, AbstractShrimpCook>();
static {
cookMap.put(CookType.POACH, new PoachShrimp());
cookMap.put(CookType.BRAISE, new BraiseShrimp());
cookMap.put(CookType.STEAMED, new SteamedShrimp());
}
public AbstractShrimpCook getAbstractShrimpCook() {
return abstractShrimpCook;
}
public void setAbstractShrimpCook(AbstractShrimpCook abstractShrimpCook) {
this.abstractShrimpCook = abstractShrimpCook;
}
//调用具体策略方法
public void cookMethod() {
abstractShrimpCook.cook();
}
// 通过类型匹配缓存中的策略
public void cookMethod2(CookType cookStrategy){
AbstractShrimpCook cShrimpCook = cookMap.get(cookStrategy);
if (Objects.nonNull(cShrimpCook)) {
cShrimpCook.cook();
}else {
throw new IllegalArgumentException("this "+ cookStrategy +"cookStrategy not found.");
}
}
}
- 客户端2
public class Client {
public static void main(String[] args) {
//初始化厨房(context)
Kitchen kitchen = new Kitchen();
//初始化烹饪策略(方式)
PoachShrimp poachShrimp = new PoachShrimp();
kitchen.setAbstractShrimpCook(poachShrimp);
//制作
poachShrimp.cook();
System.out.println("------方式二------");
kitchen.cookMethod2(CookType.BRAISE);
}
}
水煮大虾... ------方式二------ 红烧大虾...
案例结构图:
这种方法与方法一区别是,context(Kitchen)维护了所有算法的实现,客户端无需关注具体的实现,使用时传递指定的类型即可。及时后续有拓展的需求,只需要怎么抽象策略的实现,增加到context中,使用方也无需改动代码逻辑,且做到了算法的实现与使用做到了隔离,满足开闭原则。策略增加客户端无需增加新的判断逻辑。
策略模式的优缺点及应用场景
1.优点:
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同需求,动态切换实现。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
2.缺点:
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类,增加维护难度。
策略模式的应用场景
- 系统中对一种算法(方法)有多种实现,且需要动态的切换算法的场景;
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时;
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
策略模式在源码中的使用
- JDK源码的使用 在JDK中,Comparator接口提供比较方法compare()抽象,用于定义排序规则。实现类可以自定义的传递其比较算法,用于排序。
public static void main(String[] args) {
//定义一个整型数组
Integer[] array = {3, 5, 7, 2, 8, 1, 9};
// 定义升序匿名实现算法
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
if (o1 < o2) {
return -1;
} else {
return 0;
}
}
};
//使用 arrays 工具类排序
Arrays.sort(array,comparator);
System.out.println(Arrays.toString(array));
}
}
执行结果 [1, 2, 3, 5, 7, 8, 9] 看下JDK中,
Arrays.sort()
方法的源码
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
默认如果不传排序算法,可以使用默认的排序规则(升序),也可以自定义指定排序的规则。
这里是我们自定义实现的升序算法,在JDK的集合中,构造方法往往可以传递我们的排序算法。比如TreeMap
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
// 传递 Comparator 排序算法,map中存储的值,按照排序算法排序
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
}
我们知道treeMap是有序的,通过构造函数的排序算法可以自定义排序策略。还有例如TreeSet集合,源码如下
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
}