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

本文僅僅為入門玻淑,高手勿噴。

實(shí)際工作中呀伙,我們總會(huì)遇到類似如下的需求:
某支付系統(tǒng)接入以下幾種商戶進(jìn)行充值:易寶網(wǎng)易补履,快線網(wǎng)銀,19pay手機(jī)支付剿另,支付寶支付箫锤,駿網(wǎng)一卡通,由于每家充值系統(tǒng)的結(jié)算比例不一樣雨女,而且 同一家商戶的不同充值方式也有所不同谚攒,具體系統(tǒng)情況比較復(fù)雜查辩,像支付寶既有支付寶賬號(hào)支付和支付寶網(wǎng)銀支付等這些暫時(shí)不考慮晒旅,為了講述策略模式這里簡(jiǎn)單描 述,假如分為四種名扛,手機(jī)支付讼稚,網(wǎng)銀支付括儒,商戶賬號(hào)支付和點(diǎn)卡支付绕沈。因?yàn)闆]個(gè)支付結(jié)算比例不同,所以對(duì)手續(xù)費(fèi)低的做一些優(yōu)惠活動(dòng)帮寻,盡可能讓用戶使用手續(xù)費(fèi)低 的支付方式來充值乍狐,這樣降低渠道費(fèi)用,增加收入规婆,具體優(yōu)惠政策如下:

網(wǎng)銀充值澜躺,8.5折
商戶充值,9折
手機(jī)充值抒蚜,沒有優(yōu)惠
點(diǎn)卡充值掘鄙,收取1%的渠道費(fèi)

作為一個(gè)新手的代碼基本如下:

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, "網(wǎng)銀"),
   
    BUSI_ACCOUNTS(2, "商戶賬號(hào)"),
   
    MOBILE(3,"手機(jī)卡充值"),
   
    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;
    }
}

以上代碼雖然實(shí)現(xiàn)了基本功能,但是四種計(jì)算方式都在一個(gè)方法內(nèi)部嗡髓,如果優(yōu)惠模式又增加了幾十種操漠,代碼會(huì)變成什么樣?你是否愿意來維護(hù)和擴(kuò)展這樣的代碼饿这?此時(shí)有的同學(xué)或許會(huì)說浊伙,我用switch-case來實(shí)現(xiàn):

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;
      }
    
    }
   
} 

這段代碼在簡(jiǎn)潔性確實(shí)要比if-else簡(jiǎn)潔一些,但實(shí)際上是換湯不換藥长捧,并沒有從本質(zhì)上解決問題嚣鄙。
我們使用if-else事實(shí)上也是為了重用,但這只是面向過程的重用串结,程序員只看到代碼重用哑子,因?yàn)樗吹絠f-else幾種情況下大部分代碼都是重復(fù)的,只有個(gè)別不同肌割,因此使用if-else可以避免重復(fù)代碼卧蜓,并且認(rèn)為這是模板Template模式。我們會(huì)從代碼運(yùn)行順序來看待代碼把敞,這種思維類似水管或者串行電路弥奸,水沿著水管流動(dòng)(代碼運(yùn)行次序),當(dāng)遇到幾個(gè)分管(子管)奋早,就分到這幾個(gè)分管子在流動(dòng)盛霎,這里就相當(dāng)于碰到代碼的if-else處了。這樣的壞處就是耽装,一旦業(yè)務(wù)發(fā)生了變化將給我們維護(hù)工作帶來極大的困難愤炸。
那么有沒有更好的實(shí)現(xiàn)方式呢?當(dāng)然有剂邮,我們可以通過工廠模式或者策略模式避免如上的面向過程的重用摇幻。而本文將要介紹的是 策略模式


策略模式(Policy)

定義一系列的算法,把每一個(gè)算法封裝起來,并且使它們可相互替換。本模式使得算法可獨(dú)立于使用它的客戶而變化。也稱為政策模式(Policy)绰姻。(Definea family of algorithms,encapsulate each one, andmake them interchangeable. Strategy lets the algorithmvary independently from clients that use it.)

UML圖如下

sss1.jpg

由上圖可看出策略模式由以下角色構(gòu)成:

  • 抽象策略角色: 策略類枉侧,通常由一個(gè)接口或者抽象類實(shí)現(xiàn)。
  • 具體策略角色:包裝了相關(guān)的算法和行為狂芋。
  • 環(huán)境角色:持有一個(gè)策略類的引用榨馁,最終給客戶端調(diào)用。

策略模式讓算法獨(dú)立于使用它的客戶而獨(dú)立變化帜矾。策略模式重點(diǎn)是封裝不同的算法和行為翼虫,不同的場(chǎng)景下可以相互替換。策略模式是開閉原則的體現(xiàn)屡萤,開閉原則講的是一個(gè)軟件實(shí)體應(yīng)該對(duì)擴(kuò)展開放對(duì)修改關(guān) 閉珍剑。策略模式在新的策略增加時(shí),不會(huì)影響其他類的修改死陆,增加了擴(kuò)展性招拙,也就是對(duì)擴(kuò)展是開放的;對(duì)于場(chǎng)景來說措译,只依賴于抽象别凤,而不依賴于具體實(shí)現(xiàn),所以對(duì)修改是關(guān)閉的领虹。策略模式的認(rèn)識(shí)可以借助《java與模式》一書中寫到諸葛亮的錦囊妙計(jì)來學(xué)習(xí)规哪,在不同的場(chǎng)景下趙云打開不同的錦囊,便化險(xiǎn)為夷塌衰,錦囊便是抽象策略诉稍,具體的錦囊里面的計(jì)策便是具體的策略角色,場(chǎng)景就是趙云猾蒂,變化的處境選擇具體策略的條件均唉。

Strategy模式有下面的一些優(yōu)點(diǎn):

  1. 相關(guān)算法系列 Strategy類層次為Context定義了一系列的可供重用的算法或行為是晨。 繼承有助于析取出這些算法中的公共功能肚菠。
  2. 提供了可以替換繼承關(guān)系的辦法: 繼承提供了另一種支持多種算法或行為的方法。你可以直接生成一個(gè)Context類的子類罩缴,從而給它以不同的行為蚊逢。但這會(huì)將行為硬行編制到 Context中,而將算法的實(shí)現(xiàn)與Context的實(shí)現(xiàn)混合起來,從而使Context難以理解箫章、難以維護(hù)和難以擴(kuò)展烙荷,而且還不能動(dòng)態(tài)地改變算法。最后你得到一堆相關(guān)的類 , 它們之間的唯一差別是它們所使用的算法或行為檬寂。 將算法封裝在獨(dú)立的Strategy類中使得你可以獨(dú)立于其Context改變它终抽,使它易于切換、易于理解、易于擴(kuò)展昼伴。
  3. 消除了一些if else條件語(yǔ)句 :Strategy模式提供了用條件語(yǔ)句選擇所需的行為以外的另一種選擇匾旭。當(dāng)不同的行為堆砌在一個(gè)類中時(shí) ,很難避免使用條件語(yǔ)句來選擇合適的行為。將行為封裝在一個(gè)個(gè)獨(dú)立的Strategy類中消除了這些條件語(yǔ)句圃郊。含有許多條件語(yǔ)句的代碼通常意味著需要使用Strategy模式价涝。
  4. 實(shí)現(xiàn)的選擇 Strategy模式可以提供相同行為的不同實(shí)現(xiàn)〕钟撸客戶可以根據(jù)不同時(shí)間 /空間權(quán)衡取舍要求從不同策略中進(jìn)行選擇色瘩。

Strategy模式缺點(diǎn):

1)客戶端必須知道所有的策略類,并自行決定使用哪一個(gè)策略類: 本模式有一個(gè)潛在的缺點(diǎn)逸寓,就是一個(gè)客戶要選擇一個(gè)合適的Strategy就必須知道這些Strategy到底有何不同居兆。此時(shí)可能不得不向客戶暴露具體的實(shí)現(xiàn)問題。因此僅當(dāng)這些不同行為變體與客戶相關(guān)的行為時(shí) , 才需要使用Strategy模式竹伸。
2 ) Strategy和Context之間的通信開銷 :無論各個(gè)ConcreteStrategy實(shí)現(xiàn)的算法是簡(jiǎn)單還是復(fù)雜, 它們都共享Strategy定義的接口史辙。因此很可能某些 ConcreteStrategy不會(huì)都用到所有通過這個(gè)接口傳遞給它們的信息;簡(jiǎn)單的 ConcreteStrategy可能不使用其中的任何信息佩伤!這就意味著有時(shí)Context會(huì)創(chuàng)建和初始化一些永遠(yuǎn)不會(huì)用到的參數(shù)聊倔。如果存在這樣問題 , 那么將需要在Strategy和Context之間更進(jìn)行緊密的耦合。
3 )策略模式將造成產(chǎn)生很多策略類:可以通過使用享元模式在一定程度上減少對(duì)象的數(shù)量生巡。 增加了對(duì)象的數(shù)目 Strategy增加了一個(gè)應(yīng)用中的對(duì)象的數(shù)目耙蔑。有時(shí)你可以將 Strategy實(shí)現(xiàn)為可供各Context共享的無狀態(tài)的對(duì)象來減少這一開銷。任何其余的狀態(tài)都由 Context維護(hù)孤荣。Context在每一次對(duì)Strategy對(duì)象的請(qǐng)求中都將這個(gè)狀態(tài)傳遞過去甸陌。共享的 Strategy不應(yīng)在各次調(diào)用之間維護(hù)狀態(tài)。

策略模式在實(shí)際工作中也很常用盐股,在博客你還在用if-else嗎有過很好的闡述钱豁,策略模式不僅是繼承的代替方案,還能很好地解決if-else問題疯汁。下面結(jié)合本文之前的例子來說明一下如何使用策略模式牲尺。
策略模式下的UML圖如下:

sss2.jpg

創(chuàng)建抽象策略角色Strategy:

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

根據(jù)需求,分別實(shí)現(xiàn)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;
    }
 }

創(chuàng)建環(huán)境角色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工廠,負(fù)責(zé)Strategy實(shí)例的創(chuàng)建:

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;
    }
} 

開始測(cè)試:

public class Client {
 
    public static void main(String[] args) {
 
       Context context = new Context();
       // 網(wǎng)銀充值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);
 
       // 手機(jī)充值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);
    }
 
}

運(yùn)行結(jié)果:

85.0
90.0
100.0
101.0

從上面類圖和代碼可以看出幌蚊,策略模式把具體的算法封裝到了具體策略角色內(nèi)部谤碳,增強(qiáng)了可擴(kuò)展性,隱蔽了實(shí)現(xiàn)細(xì)節(jié)溢豆;它替代繼承來實(shí)現(xiàn)蜒简,避免了if- else這種不易維護(hù)的條件語(yǔ)句。當(dāng)然我們也可以看到漩仙,策略模式由于獨(dú)立策略實(shí)現(xiàn)搓茬,使得系統(tǒng)內(nèi)增加了很多策略類犹赖;對(duì)客戶端來說必須知道兜友哪些具體策略, 而且需要知道選擇具體策略的條件卷仑。

總結(jié)

凡事具有兩面性冷尉,策略模式也不例外,下面簡(jiǎn)單列舉一下策略模式的優(yōu)缺點(diǎn)系枪。

優(yōu)點(diǎn):

  1. 相關(guān)算法系列 Strategy類層次為Context定義了一系列的可供重用的算法或行為雀哨。 繼承有助于析取出這些算法中的公共功能。
  2. 提供了可以替換繼承關(guān)系的辦法: 繼承提供了另一種支持多種算法或行為的方法私爷。你可以直接生成一個(gè)Context類的子類雾棺,從而給它以不同的行為。但這會(huì)將行為硬行編制到 Context中衬浑,而將算法的實(shí)現(xiàn)與Context的實(shí)現(xiàn)混合起來,從而使Context難以理解捌浩、難以維護(hù)和難以擴(kuò)展,而且還不能動(dòng)態(tài)地改變算法工秩。最后你得到一堆相關(guān)的類 , 它們之間的唯一差別是它們所使用的算法或行為尸饺。 將算法封裝在獨(dú)立的Strategy類中使得你可以獨(dú)立于其Context改變它,使它易于切換助币、易于理解浪听、易于擴(kuò)展。
  3. 消除了一些if else條件語(yǔ)句 :Strategy模式提供了用條件語(yǔ)句選擇所需的行為以外的另一種選擇眉菱。當(dāng)不同的行為堆砌在一個(gè)類中時(shí) ,很難避免使用條件語(yǔ)句來選擇合適的行為迹栓。將行為封裝在一個(gè)個(gè)獨(dú)立的Strategy類中消除了這些條件語(yǔ)句。含有許多條件語(yǔ)句的代碼通常意味著需要使用Strategy模式俭缓。
  4. 實(shí)現(xiàn)的選擇 Strategy模式可以提供相同行為的不同實(shí)現(xiàn)克伊。客戶可以根據(jù)不同時(shí)間 /空間權(quán)衡取舍要求從不同策略中進(jìn)行選擇华坦。

缺點(diǎn):

  1. 客戶端必須知道所有的策略類愿吹,并自行決定使用哪一個(gè)策略類: 本模式有一個(gè)潛在的缺點(diǎn),就是一個(gè)客戶要選擇一個(gè)合適的Strategy就必須知道這些Strategy到底有何不同惜姐。此時(shí)可能不得不向客戶暴露具體的實(shí)現(xiàn)問題犁跪。因此僅當(dāng)這些不同行為變體與客戶相關(guān)的行為時(shí) , 才需要使用Strategy模式。
    2 . Strategy和Context之間的通信開銷 :無論各個(gè)ConcreteStrategy實(shí)現(xiàn)的算法是簡(jiǎn)單還是復(fù)雜, 它們都共享Strategy定義的接口载弄。因此很可能某些 ConcreteStrategy不會(huì)都用到所有通過這個(gè)接口傳遞給它們的信息耘拇;簡(jiǎn)單的 ConcreteStrategy可能不使用其中的任何信息撵颊!這就意味著有時(shí)Context會(huì)創(chuàng)建和初始化一些永遠(yuǎn)不會(huì)用到的參數(shù)宇攻。如果存在這樣問題 , 那么將需要在Strategy和Context之間更進(jìn)行緊密的耦合。
    3 . 策略模式將造成產(chǎn)生很多策略類:可以通過使用享元模式在一定程度上減少對(duì)象的數(shù)量倡勇。 增加了對(duì)象的數(shù)目 Strategy增加了一個(gè)應(yīng)用中的對(duì)象的數(shù)目逞刷。有時(shí)你可以將 Strategy實(shí)現(xiàn)為可供各Context共享的無狀態(tài)的對(duì)象來減少這一開銷嘉涌。任何其余的狀態(tài)都由 Context維護(hù)。Context在每一次對(duì)Strategy對(duì)象的請(qǐng)求中都將這個(gè)狀態(tài)傳遞過去夸浅。共享的 Strategy不應(yīng)在各次調(diào)用之間維護(hù)狀態(tài)仑最。

最后,是否應(yīng)該重構(gòu)一下你的代碼了帆喇?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末警医,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子坯钦,更是在濱河造成了極大的恐慌预皇,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件婉刀,死亡現(xiàn)場(chǎng)離奇詭異吟温,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)突颊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門鲁豪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人律秃,你說我怎么就攤上這事爬橡。” “怎么了棒动?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵堤尾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我迁客,道長(zhǎng)郭宝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任掷漱,我火速辦了婚禮粘室,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卜范。我一直安慰自己衔统,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布海雪。 她就那樣靜靜地躺著锦爵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奥裸。 梳的紋絲不亂的頭發(fā)上险掀,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音湾宙,去河邊找鬼樟氢。 笑死冈绊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的埠啃。 我是一名探鬼主播死宣,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼碴开!你這毒婦竟也來了毅该?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤潦牛,失蹤者是張志新(化名)和其女友劉穎鹃骂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罢绽,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡畏线,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了良价。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寝殴。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖明垢,靈堂內(nèi)的尸體忽然破棺而出蚣常,到底是詐尸還是另有隱情,我是刑警寧澤痊银,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布抵蚊,位于F島的核電站,受9級(jí)特大地震影響溯革,放射性物質(zhì)發(fā)生泄漏贞绳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一致稀、第九天 我趴在偏房一處隱蔽的房頂上張望冈闭。 院中可真熱鬧,春花似錦抖单、人聲如沸萎攒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)耍休。三九已至,卻和暖如春货矮,著一層夾襖步出監(jiān)牢的瞬間羊精,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工次屠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留园匹,地道東北人雳刺。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓劫灶,卻偏偏與公主長(zhǎng)得像裸违,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子本昏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容