设计模式之策略设计模式

195 阅读8分钟

前言

插卡游戏机是很多人童年的回忆,相信很多小伙伴都有偷偷在家打游戏的经历,在那个手机没有普及的年代,能有个小霸王游戏机是一件令人多么兴奋的事。 image.png

这款游戏机由两部分组成,游戏机+游戏卡。当时游戏主要靠硬件,不像现在一个手机上可以安装各种游戏软件。当年超级玛丽,魂斗罗都是非常火热的游戏。游戏硬件直接集成到主板中,如果想玩两种游戏你可能需要购买两个游戏机。这就显得很多余了,因为游戏主板一样,手柄也是一样,仅仅为了玩另外一种游戏就得多出一份钱,去买另一个游戏机。读到这里你应该也发现了,只有游戏硬件是变化的,其他的都是不变的。那能不能把游戏硬件封装起来,并把游戏机留一个游戏卡接口。这样就可以通过更换不同的游戏卡玩不同的游戏。这种方法,不仅仅减少买家购买成本,还能够普及游戏。游戏机只需要购买一个,想玩什么游戏直接购买游戏卡就行,玩的时候直接将游戏卡查到游戏机的接口中。这种游戏机与游戏硬件解耦分离,通过接口来关联两者的设计方式,与设计模式中的策略设计模式可以说是完全一致的,要不然怎么说代码来源与生活。

策略设计模式

策略设计模式也是工作开发过程中经常会遇到的一种场景,我们经常会遇到同一个业务逻辑,但是可能会需要不同的实现逻辑。输入数据是完全一致的,但是对数据处理的方式不一样,这就导致了输入参数一致,经过黑盒子处理后,输出不同的结果。

如下图,同一份数据,根据不同的条件使用不同的黑盒处理方法,输出不同的数据。不同的黑盒就是封装不同的数据业务处理逻辑。这里多啰嗦了一点,强调了封装的是业务逻辑。因此你可以发现策略设计模式的适用场景,当涉及同级的不同业务处理逻辑,这些业务逻辑完全可以相互替换。并且这些逻辑没法抽象成一个实体,比如说不同的HTTP Client 封装,Client 本身就是一个实体,而不是一段逻辑,所以不适用,但是如果是Client取得的数据,需要根据需求转换成不同的POJO类,不同的转换逻辑就非常适用策略设计模式。 image.png

说的这么多,希望读者能够体会到哪种场景适用策略设计模式,而避免模式乱用导致代码结构混乱。这个时候回头思考一下小霸王游戏机设计思想,是不是也是符合策略设计模式思想。不同的游戏卡就是不同的数据处理业务逻辑,通过切换不同的游戏卡来实现不同的游戏转换。下面笔者将会结合游戏机切换游戏来详细解说策略设计模式。

  1. 超级玛丽游戏机
class SuperMarioMachine{
  void load(){
    System.out.println("loading super marion game ...");
  }
}
  1. 魂斗罗游戏机
class ContraMachine{
  void load(){
     System.out.println("loading contra ...");
  }
}
  1. 控制台代码
class Console{
  SuperMarioMachine superMario=new SuperMarioMachine();
  superMario.load();
  ContraMachine contra=new ContraMachine();
  contra.load();
}

这是最糟糕的设计方法,为不同的游戏设计一个类,随着游戏越来越多,实现的类也越来越多,而且控制台与游戏机完全耦合。

image-20210406215256440.png

这就会导致想要玩不同的游戏,就得生成不同的游戏机。

进一步抽象,不难发现游戏机器基本组件是相同的,只是游戏逻辑逻辑不一样。因此封装变化,将游戏逻辑封装成游戏卡。

public class Machine{
  private Card card;
  public insertCard(Card card){
    this.card=card;
  }
  public void load(){
     this.card.load();
  }
  
}
public interface Card{
  void load();
}

public class SuperMarioCard implements Card{
  @Override
  public void load(){
     System.out.println("loading super marion game ...");
  }
}

public class ContraCard implements Card {
    @Override
    public void load(){
        System.out.println("loading contra ...");
    }
}

class Console{
  Machine machine=new Machine();
  machine.insertCard(new SuperMarioCard());
  machine.load();
  machine.insertCard(new ContraCard());
  machine.load();
  
}

封装后的UML类图如下,很明显游戏机与游戏卡进行解耦,不在相互耦合在一起。需要不同的游戏只需要注入相应的游戏卡策略即可。如果后期开发更多的游戏,可以发现代码很容易维护,几乎不用修改任何历史的代码,只需要创建一个新的游戏卡类即可。

image-20210406220840457.png

不过策略模式明显也有缺点,不同的游戏策略很难被其他代码复用,而且客户端的依赖变多了,客户端不仅要知道游戏机,还得知道不同的游戏卡类。这不难理解,因为策略封装的本来就是业务逻辑,业务逻辑基本上谈不上复用一说。随着策略类边多,而且策略类还需要暴露给客户端,客户端负担变重,这会产生后期难以维护的弊端。不过还好,平时开发过程中很难遇到那种能扩展到十几种以上策略类的业务。

常用策略设计模式业务场景

  1. 场景一

笔者最近就在开发过程中遇到了一个业务场景,使用策略设计模式来解决。我们在Redis中存了一个list,这个list比较大,一次性取出全部可能会占用redis太久,阻塞redis。于是我们就是先查询list的大小,然后进行分批查询。假设长度为100条,那每次只取10条,取10次,把数据全部取处理完,减少了redis一次原子操作的时间,达到了优化查询的目的。

批量查询redis是一个公共的方法,我们有多个不同长度的list,不同的list存放不同的数据。我们会把redis查询的list推送给前端,但是很明显如果批量查询得到结果后,在对数据进行包装,包装完成后一次性推送给前端,很明显这是个漫长的过程,客户一般是没有耐心的。所以,我们就考虑针对不同的list,封装不同的数据处理策略,并且封装好后直接推送给前端,实现数据预加载。因此当批量查询redis的时候,每查询出来一批立马推送给前端。将针对不同的list数据的数据处理逻辑封装成策略类,直接注入到 redis 工具类中,在批量查询过程中把每次查询的数据循环调用策略类方法。

笔者遇到的这个需求,使用策略设计模式也是个无奈之举,因为DAO层的历史代码逻辑不希望被更改,并且也没有必要新开发一个数据接口。

  1. 场景二

还有一种场景也适用策略设计模式,策略设计模式可以用来消除繁琐的if else。

if(a){
  ......
}else if(b){
  .......
}else if(c){
  ......
}

并不是所有的 if else 都需要被重构成策略,如果 if else 的条件不会被频繁的扩展,或者条件内的逻辑不涉及频繁修改,我认为都没有必要去重构,因为维护一个不会改变的 if else 虽然条件可能很多,但总比维护多个类要简单的多,所以为了避免滥用设计模式,在使用前多思考是不是非重构不可?又或者是单个条件体内的逻辑异常复杂庞大,那新建一个类封装未尝不可。 移动支付现在是非常的常见了,当我们使用app在线付款时候,会提示用户选择付款的方式(微信,支付宝,银行卡等多种支付方式),如果通过 if else 直接判断用户选择付款的平台,代码如下。

enum Payment {
    Wechat, Alipay, Bankcard
}
class PayContext{
  void pay(PayEvent event){
    if(event==PayEvent.Wechat){
      //complicated wechat pay logic
      System.out.println("wechat pay success")
    }else if(event==PayEvent.Alipay){
       //complicated Alipay pay logic
      System.out.println("alipay pay success")
    }else if(event==PayEvent.Bankcard){
       //complicated Bankcard pay logic
      System.out.println("bankcard pay success")
    }
  }
}

这种写法的缺点在于,扩展复杂,如果新增了付款方式,需要修改历史代码。另外复杂的付款逻辑都集中在一个方法里面,条件越来越多,可读性也差,3个4个条件还好,如果5个以上的条件读起来就非常的辛苦了。

下面我们将用策略设计模式来重构上面的代码,主要是将 if 中的逻辑封装为不同的算法策略类。

  1. 支付策略接口封装
interface PayStrategy{
  void pay();
}
  1. 三种支付策略接口实现
class WechatStrategy implements PayStrategy{
  
  @Override
  void pay(){
          //complicated wechat pay logic
      System.out.println("wechat pay success")
  }
}

class AlipayStrategy implements PayStrategy{
  
  @Override
  void pay(){
       //complicated Alipay pay logic
      System.out.println("alipay pay success")
  }
}
class BankcardStrategy implements PayStrategy{
  
  @Override
  void pay(){
       //complicated Bankcard pay logic
      System.out.println("bankcard pay success")
  } 
}
  1. 结合工厂模式,返回不同的策略实例
class PayFactory {
    public PayStrategy getPay(PayEvent event){
        switch (event) {
            case Wechat:
                return new WechatStrategy();
            case Alipay:
                return new AlipayStrategy();
            case Bankcard:
                return new BankcardStrategy();
        }
        return null;
    }
}
  1. 客户端代码
class PayContext{
  void pay(PayEvent event){
     PayStrategy pay=PayFactory.getPay(event);
    if(pay!=null){
        pay.pay();
    }
  }
}

通过结合工厂模式,消除了客户端中的 else if。你也可以利用反射来消除工厂中是switch 代码。以上就是对策略设计模式的介绍以及重构场景,希望本篇能够让你对面向对象设计有新的感悟,毕竟模式是死的,如何灵活运用才是学习设计模式的目标。笔者最近毕竟懒,如果喜欢本篇文章的话,给我点个赞吧,你的赞是我继续写下去的动力。