Java中避免if-else-if:策略模式

作者: JarvanMo | 来源:发表于2017-02-15 14:37 被阅读872次

    本文仅仅为入门,高手勿喷。

    实际工作中,我们总会遇到类似如下的需求:
    某支付系统接入以下几种商户进行充值:易宝网易,快线网银,19pay手机支付,支付宝支付,骏网一卡通,由于每家充值系统的结算比例不一样,而且 同一家商户的不同充值方式也有所不同,具体系统情况比较复杂,像支付宝既有支付宝账号支付和支付宝网银支付等这些暂时不考虑,为了讲述策略模式这里简单描 述,假如分为四种,手机支付,网银支付,商户账号支付和点卡支付。因为没个支付结算比例不同,所以对手续费低的做一些优惠活动,尽可能让用户使用手续费低 的支付方式来充值,这样降低渠道费用,增加收入,具体优惠政策如下:

    网银充值,8.5折
    商户充值,9折
    手机充值,没有优惠
    点卡充值,收取1%的渠道费

    作为一个新手的代码基本如下:

    public class Example {
        public Double calRecharge(Double charge ,RechargeTypeEnum type ){
          
           if(type.equals(RechargeTypeEnum.E_BANK)){
               return charge*0.85;
           }else if(type.equals(RechargeTypeEnum.BUSI_ACCOUNTS)){
               return charge*0.90;
           }else if(type.equals(RechargeTypeEnum.MOBILE)){
               return charge;
           }else if(type.equals(RechargeTypeEnum.CARD_RECHARGE)){
               return charge+charge*0.01;
           }else{
               return null;
           }
     
        }
       
    } 
    
    public enum RechargeTypeEnum {
     
        E_BANK(1, "网银"),
       
        BUSI_ACCOUNTS(2, "商户账号"),
       
        MOBILE(3,"手机卡充值"),
       
        CARD_RECHARGE(4,"充值卡")
        ;
       
       
        private int value;
       
       
        private String description;
       
       
       
        private RechargeTypeEnum(int value, String description) {
           this.value = value;
           this.description = description;
        }
          
        public int value() {
           return value;
        }
        public String description() {
           return description;
        }
       
     
        public static RechargeTypeEnum valueOf(int value) {
            for(RechargeTypeEnum type : RechargeTypeEnum.values()) {
                if(type.value() == value) {
                    return type;
                }
            }
            return  E_BANK;
        }
    }
    

    以上代码虽然实现了基本功能,但是四种计算方式都在一个方法内部,如果优惠模式又增加了几十种,代码会变成什么样?你是否愿意来维护和扩展这样的代码?此时有的同学或许会说,我用switch-case来实现:

    public class Example {
        public Double calRecharge(Double charge ,RechargeTypeEnum type ){
          
          switch(type){
            case RechargeTypeEnum.E_BANK:
                return charge*0.85;
            case RechargeTypeEnum.BUSI_ACCOUNTS:
                return charge*0.90;
            case RechargeTypeEnum.MOBILE:
                return charge;  
            case RechargeTypeEnum.CARD_RECHARGE:
                retrun charge+charge*0.01;
            default:
            return null;
          }
        
        }
       
    } 
    

    这段代码在简洁性确实要比if-else简洁一些,但实际上是换汤不换药,并没有从本质上解决问题。
    我们使用if-else事实上也是为了重用,但这只是面向过程的重用,程序员只看到代码重用,因为他看到if-else几种情况下大部分代码都是重复的,只有个别不同,因此使用if-else可以避免重复代码,并且认为这是模板Template模式。我们会从代码运行顺序来看待代码,这种思维类似水管或者串行电路,水沿着水管流动(代码运行次序),当遇到几个分管(子管),就分到这几个分管子在流动,这里就相当于碰到代码的if-else处了。这样的坏处就是,一旦业务发生了变化将给我们维护工作带来极大的困难。
    那么有没有更好的实现方式呢?当然有,我们可以通过工厂模式或者策略模式避免如上的面向过程的重用。而本文将要介绍的是 策略模式


    策略模式(Policy)

    定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。也称为政策模式(Policy)。(Definea family of algorithms,encapsulate each one, andmake them interchangeable. Strategy lets the algorithmvary independently from clients that use it.)
    

    UML图如下

    sss1.jpg

    由上图可看出策略模式由以下角色构成:

    • 抽象策略角色: 策略类,通常由一个接口或者抽象类实现。
    • 具体策略角色:包装了相关的算法和行为。
    • 环境角色:持有一个策略类的引用,最终给客户端调用。

    策略模式让算法独立于使用它的客户而独立变化。策略模式重点是封装不同的算法和行为,不同的场景下可以相互替换。策略模式是开闭原则的体现,开闭原则讲的是一个软件实体应该对扩展开放对修改关 闭。策略模式在新的策略增加时,不会影响其他类的修改,增加了扩展性,也就是对扩展是开放的;对于场景来说,只依赖于抽象,而不依赖于具体实现,所以对修改是关闭的。策略模式的认识可以借助《java与模式》一书中写到诸葛亮的锦囊妙计来学习,在不同的场景下赵云打开不同的锦囊,便化险为夷,锦囊便是抽象策略,具体的锦囊里面的计策便是具体的策略角色,场景就是赵云,变化的处境选择具体策略的条件。

    Strategy模式有下面的一些优点:

    1. 相关算法系列 Strategy类层次为Context定义了一系列的可供重用的算法或行为。 继承有助于析取出这些算法中的公共功能。
    2. 提供了可以替换继承关系的办法: 继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到 Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类 , 它们之间的唯一差别是它们所使用的算法或行为。 将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
    3. 消除了一些if else条件语句 :Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时 ,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句。含有许多条件语句的代码通常意味着需要使用Strategy模式。
    4. 实现的选择 Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间 /空间权衡取舍要求从不同策略中进行选择。

    Strategy模式缺点:

    1)客户端必须知道所有的策略类,并自行决定使用哪一个策略类: 本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时 , 才需要使用Strategy模式。
    2 ) Strategy和Context之间的通信开销 :无论各个ConcreteStrategy实现的算法是简单还是复杂, 它们都共享Strategy定义的接口。因此很可能某些 ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的 ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题 , 那么将需要在Strategy和Context之间更进行紧密的耦合。
    3 )策略模式将造成产生很多策略类:可以通过使用享元模式在一定程度上减少对象的数量。 增加了对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将 Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由 Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去。共享的 Strategy不应在各次调用之间维护状态。

    策略模式在实际工作中也很常用,在博客你还在用if-else吗有过很好的阐述,策略模式不仅是继承的代替方案,还能很好地解决if-else问题。下面结合本文之前的例子来说明一下如何使用策略模式。
    策略模式下的UML图如下:

    sss2.jpg

    创建抽象策略角色Strategy:

    public interface Strategy {
        public Double calRecharge(Double charge ,RechargeTypeEnum type );
    }
    

    根据需求,分别实现Strategy即具体策略角色:

    public class EBankStrategy implements Strategy{
    
        @Override
        public Double calRecharge(Double charge, RechargeTypeEnum type) {
           return charge*0.85;
        }
    
    
    public class BusiAcctStrategy implements Strategy{
    
        @Override
        public Double calRecharge(Double charge, RechargeTypeEnum type) {
           return charge*0.90;
        }
        
    } 
    
    public class MobileStrategy implements Strategy {
    
        @Override
        public Double calRecharge(Double charge, RechargeTypeEnum type) {
           return charge;
        }
        
    }
    
    public class CardStrategy implements Strategy{
     
        @Override
        public Double calRecharge(Double charge, RechargeTypeEnum type) {
           return charge+charge*0.01;
        }
     }
    

    创建环境角色Context:

    public class Context {
     
        private Strategy strategy;
       
        public Double calRecharge(Double charge, Integer type) {
           strategy = StrategyFactory.getInstance().creator(type);
           return strategy.calRecharge(charge, RechargeTypeEnum.valueOf(type));
        }
     
        public Strategy getStrategy() {
           return strategy;
        }
     
        public void setStrategy(Strategy strategy) {
           this.strategy = strategy;
        }
       
    }
    

    StrategyFactory工厂,负责Strategy实例的创建:

    public class StrategyFactory {
     
        private static StrategyFactory factory = new StrategyFactory();
        private StrategyFactory(){
        }
        private static Map strategyMap = new HashMap<>();
        static{
           strategyMap.put(RechargeTypeEnum.E_BANK.value(), new EBankStrategy());
           strategyMap.put(RechargeTypeEnum.BUSI_ACCOUNTS.value(), new BusiAcctStrategy());
           strategyMap.put(RechargeTypeEnum.MOBILE.value(), new MobileStrategy());
           strategyMap.put(RechargeTypeEnum.CARD_RECHARGE.value(), new CardStrategy());
        }
        public Strategy creator(Integer type){
           return strategyMap.get(type);
        }
        public static StrategyFactory getInstance(){
           return factory;
        }
    } 
    

    开始测试:

    public class Client {
     
        public static void main(String[] args) {
     
           Context context = new Context();
           // 网银充值100 需要付多少
           Double money = context.calRecharge(100D,
                  RechargeTypeEnum.E_BANK.value());
           System.out.println(money);
     
           // 商户账户充值100 需要付多少
           Double money2 = context.calRecharge(100D,
                  RechargeTypeEnum.BUSI_ACCOUNTS.value());
           System.out.println(money2);
     
           // 手机充值100 需要付多少
           Double money3 = context.calRecharge(100D,
                  RechargeTypeEnum.MOBILE.value());
           System.out.println(money3);
     
           // 充值卡充值100 需要付多少
           Double money4 = context.calRecharge(100D,
                  RechargeTypeEnum.CARD_RECHARGE.value());
           System.out.println(money4);
        }
     
    }
    

    运行结果:

    85.0
    90.0
    100.0
    101.0

    从上面类图和代码可以看出,策略模式把具体的算法封装到了具体策略角色内部,增强了可扩展性,隐蔽了实现细节;它替代继承来实现,避免了if- else这种不易维护的条件语句。当然我们也可以看到,策略模式由于独立策略实现,使得系统内增加了很多策略类;对客户端来说必须知道兜友哪些具体策略, 而且需要知道选择具体策略的条件。

    总结

    凡事具有两面性,策略模式也不例外,下面简单列举一下策略模式的优缺点。

    优点:

    1. 相关算法系列 Strategy类层次为Context定义了一系列的可供重用的算法或行为。 继承有助于析取出这些算法中的公共功能。
    2. 提供了可以替换继承关系的办法: 继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到 Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类 , 它们之间的唯一差别是它们所使用的算法或行为。 将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
    3. 消除了一些if else条件语句 :Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时 ,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句。含有许多条件语句的代码通常意味着需要使用Strategy模式。
    4. 实现的选择 Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间 /空间权衡取舍要求从不同策略中进行选择。

    缺点:

    1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类: 本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时 , 才需要使用Strategy模式。
      2 . Strategy和Context之间的通信开销 :无论各个ConcreteStrategy实现的算法是简单还是复杂, 它们都共享Strategy定义的接口。因此很可能某些 ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的 ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题 , 那么将需要在Strategy和Context之间更进行紧密的耦合。
      3 . 策略模式将造成产生很多策略类:可以通过使用享元模式在一定程度上减少对象的数量。 增加了对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将 Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由 Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去。共享的 Strategy不应在各次调用之间维护状态。

    最后,是否应该重构一下你的代码了?

    相关文章

      网友评论

      • 坑吭吭:dataSource那一套是不是就是策略模式
        JarvanMo:不一样的吧,这个是主要解决if-else太多的情况.
      • 1eff7f40b465:用哪个支付呢?不能都用吧
        JarvanMo:用哪个支付实际上由 context.calRecharge(100D,RechargeTypeEnum.MOBILE.value());中第二个参数决定的。事实上你可以这样使context.calRecharge(100D,1);
        最终实际是由RechargeTypeEnum的valueOf方法判断用哪种支付。
      • bb8d09432d79:你这个并没有消除if else,只是把不同策略的实现归类,具体选择策略的时候,你还是if else
        JarvanMo: @萌教授 枚举也好map也好,算是表驱动吧。
        bb8d09432d79:@JarvanMo 哦哦,又看了下,普遍还是用枚举或者map的方法代替if else,没什么更优解了
        JarvanMo:策略的选择实际上是由 RechargeTypeEnum.valueOf(type)来决择啊。
        Double money3 = context.calRecharge(100D,1);也可以这样使用。

      本文标题:Java中避免if-else-if:策略模式

      本文链接:https://www.haomeiwen.com/subject/iaqqwttx.html