阅读 91

设计模式-策略模式及应用

在平时的编码过程中,我们往往会遇到一个接口的实现会有多种,不同的实现(不同的策略)使用在不同的场景中。举个简单的场景,项目中使用到的站内消息,站内消息的存储介质我们可以是关系型数据库(又分不同的厂商),也可以是非关系型数据库。比如新增插入一条消息这个方法,他的实现方式可能就会很多,如我们现实为了满足不同的客户,关系型数据库使用的有mysql、oracel有些客户则使用的是mongonDB非关系型数据库,客户根据自己的需求,我们动态去调整切换,而无需重新开发。

不单单是代码层面,比如每年的双十一,我们买一件商品;这几年商家搞得五花八门的优惠方案供你选择。优惠券的组合方案,什么跨店满减、定金抵现金、成为本店会员享受商品几折的优惠等等,往往我们会综合考虑,考某种方式最省钱,我们会倾向于某种方式。今天介绍的策略模式,能很好解决我们上面生活中,或者软件设计的过程中所遇到这些问题。

定义及结构特点

策略模式,英文全称是Strategy Design Pattern。在GoF的《设计模式》一书中,它是这样定义的: 定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客戶端(这里的客戶端代指使用算法的代码)。对应我们前面所讲的例子就是我们的消息写入实现。 策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式的结构与实现

策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性,现在我们来分析其基本结构和实现方法。

  1. 模式的结构

  • 抽象策略(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));     }     }


作者:fw19940314
链接:https://juejin.cn/post/7026896701302505503


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐