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

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

实际工作中,我们总会遇到类似如下的需求:
某支付系统接入以下几种商户进行充值:易宝网易,快线网银,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不应在各次调用之间维护状态。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容