(七)策略模式詳解


                 作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。

                 上章我們着重講解了觀察者模式和事件驅動,那么本章來討論一個個人認為在開發過程中出場率極高的設計模式,策略模式。

                 策略模式在LZ第一次接觸到的時候,LZ是這么理解的,就是如果我們想往一個方法當中插入隨便一段代碼的話,就是策略模式。即如下形式。

public class MyClass {

    public void myMethod(){
        System.out.println("方法里的代碼");
        //LZ想在這插入一段代碼,而且這個代碼是可以改變的,想怎么變就怎么變
        System.out.println("方法里的代碼");
    }
}

                在JAVA中,接口可以滿足LZ的這一過分要求,我們可以設計一個接口,並當做參數傳進去,就能達到這個效果了。我們來看,先定義一個接口。

public interface MyInterface {
    //我想插入的代碼
    void insertCode();
    
}

                將原來的類改成這樣,傳遞一個接口進去。

public class MyClass {

    public void myMethod(MyInterface myInterface){
        System.out.println("方法里的代碼");
        //你看我是不是插進來一段代碼?而且這段代碼是可以隨便改變的
        myInterface.insertCode();
        System.out.println("方法里的代碼");
    }
}

               我們只要實現了MyInterface這個接口,在insertCode方法中寫入我們想要插進去的代碼,再將這個類傳遞給myMethod方法,就可以將我們隨手寫的代碼插到這個方法當中。比如這樣。

class InsertCode1 implements MyInterface{

    public void insertCode() {
        System.out.println("我想插進去的代碼,第一種");
    }

}

class InsertCode2 implements MyInterface{

    public void insertCode() {
        System.out.println("我想插進去的代碼,第二種");
    }

}

              這樣我們在調用myMethod方法時就可以隨意往里面插入代碼了,比如。

//客戶端調用
public class Client {

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.myMethod(new InsertCode1());
        System.out.println("--------------------");
        myClass.myMethod(new InsertCode2());
    }
    
}

              那么運行出來的結果就是我們成功的將兩端代碼插入到了myMethod方法中,以上所講的算是JAVA中一種技術層面的實現,就是傳入一個接口,封裝代碼。那么既然談到設計模式,就要有設計模式的應用場景,有關策略模式,所產生的形式就和上述是一模一樣的,只是我們適當的給予模式的應用場景,就會讓它變的更有價值。

              下面LZ給出策略模式的標准定義,引自百度百科。

              定義:策略模式定義了一系列的算法,並將每一個算法封裝起來,而且使它們還可以相互替換。策略模式讓算法獨立於使用它的客戶而獨立變化。

              分析下定義,策略模式定義和封裝了一系列的算法,它們是可以相互替換的,也就是說它們具有共性,而它們的共性就體現在策略接口的行為上,另外為了達到最后一句話的目的,也就是說讓算法獨立於使用它的客戶而獨立變化,我們需要讓客戶端依賴於策略接口。

              下面給出策略模式的類圖,引自百度百科。


             這個類圖並不復雜,右邊是策略接口以及它的實現類,左邊會有一個上下文,這個上下文會擁有一個策略,而具體這個策略是哪一種,我們是可以隨意替換的。

             LZ下面使用JAVA代碼詮釋上面的類圖,方便各位理解各個類之間的關系。

             首先是策略接口以及它的實現類。

package net;

public interface Strategy {

    void algorithm();
    
}

class ConcreteStrategyA implements Strategy{

    public void algorithm() {
        System.out.println("采用策略A計算");
    }
    
}
class ConcreteStrategyB implements Strategy{

    public void algorithm() {
        System.out.println("采用策略B計算");
    }
    
}
class ConcreteStrategyC implements Strategy{

    public void algorithm() {
        System.out.println("采用策略C計算");
    }
    
}

             下面是我們的上下文,它會擁有一個策略接口。

package net;

public class Context {

    Strategy strategy;
    
    public void method(){
        strategy.algorithm();
    }

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
}

             method方法是上下文類的一個公開方法,實際當中一般會和業務相關,這里就暫且取名為method方法。下面我們使用客戶端調用一下。

package net;


public class Client {

    public static void main(String[] args) throws Exception {
        Context context = new Context();
        context.setStrategy(new ConcreteStrategyA());
        context.method();
        
        context.setStrategy(new ConcreteStrategyB());
        context.method();
        
        context.setStrategy(new ConcreteStrategyC());
        context.method();
    }
}

            上面我們替換了兩次策略,但是調用方式不變,下面我們看下運行結果。

 

            上面的例子代碼清晰但卻理解起來很生硬,下面LZ舉一個具有實際意義的例子。

            就比如我們要做一個商店的收銀系統,這個商店有普通顧客,會員,超級會員以及金牌會員的區別,針對各個顧客,有不同的打折方式,並且一個顧客每在商店消費1000就增加一個級別,那么我們就可以使用策略模式,因為策略模式描述的就是算法的不同,而且這個算法往往非常繁多,並且可能需要經常性的互相替換。

            這里我們舉例就采用最簡單的,以上四種顧客分別采用原價,八折,七折和半價的收錢方式。

            那么我們首先要有一個計算價格的策略接口,如下。

public interface CalPrice {
    //根據原價返回一個最終的價格
    Double calPrice(Double originalPrice);
    
}

             下面我們給出四個計算方式。

class Common implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice;
    }

}
class Vip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.8;
    }

}
class SuperVip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.7;
    }

}
class GoldVip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.5;
    }

}

                  以上四種計算方式非常清晰,分別是原價,八折,七折和半價。下面我們看客戶類,我們需要客戶類幫我們完成客戶升級的功能。

//客戶類
public class Customer {

    private Double totalAmount = 0D;//客戶在本商店消費的總額
    private Double amount = 0D;//客戶單次消費金額
    private CalPrice calPrice = new Common();//每個客戶都有一個計算價格的策略,初始都是普通計算,即原價
    
    //客戶購買商品,就會增加它的總額
    public void buy(Double amount){
        this.amount = amount;
        totalAmount += amount;
        if (totalAmount > 3000) {//3000則改為金牌會員計算方式
            calPrice = new GoldVip();
        }else if (totalAmount > 2000) {//類似
            calPrice = new SuperVip();
        }else if (totalAmount > 1000) {//類似
            calPrice = new Vip();
        }
    }
    //計算客戶最終要付的錢
    public Double calLastAmount(){
        return calPrice.calPrice(amount);
    }
}

                 下面我們看客戶端調用,系統會幫我們自動調整收費策略。

//客戶端調用
public class Client {

    public static void main(String[] args) {
        Customer customer = new Customer();
        customer.buy(500D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
        customer.buy(1200D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
        customer.buy(1200D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
        customer.buy(1200D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
    }
    
}
           運行以后會發現,第一次是原價,第二次是八折,第三次是七折,最后一次則是半價。我們這樣設計的好處是,客戶不再依賴於具體的收費策略,依賴於抽象永遠是正確的。不過上述的客戶類實在有點難看,尤其是buy方法,我們可以使用簡單工廠來稍微改進一下它。我們建立如下策略工廠。
//我們使用一個標准的簡單工廠來改進一下策略模式
public class CalPriceFactory {

    private CalPriceFactory(){}
    //根據客戶的總金額產生相應的策略
    public static CalPrice createCalPrice(Customer customer){
        if (customer.getTotalAmount() > 3000) {//3000則改為金牌會員計算方式
            return new GoldVip();
        }else if (customer.getTotalAmount() > 2000) {//類似
            return new SuperVip();
        }else if (customer.getTotalAmount() > 1000) {//類似
            return new Vip();
        }else {
            return new Common();
        }
    }
}

                 這樣我們就將制定策略的功能從客戶類分離了出來,我們的客戶類可以變成這樣。

//客戶類
public class Customer {

    private Double totalAmount = 0D;//客戶在本商店消費的總額
    private Double amount = 0D;//客戶單次消費金額
    private CalPrice calPrice = new Common();//每個客戶都有一個計算價格的策略,初始都是普通計算,即原價
    
    //客戶購買商品,就會增加它的總額
    public void buy(Double amount){
        this.amount = amount;
        totalAmount += amount;
        /* 變化點,我們將策略的制定轉移給了策略工廠,將這部分責任分離出去 */
        calPrice = CalPriceFactory.createCalPrice(this);
    }
    //計算客戶最終要付的錢
    public Double calLastAmount(){
        return calPrice.calPrice(amount);
    }
    
    public Double getTotalAmount() {
        return totalAmount;
    }
    
    public Double getAmount() {
        return amount;
    }
    
}

                   現在比之前來講,我們的策略模式更加靈活一點,但是相信看過LZ博文的都知道,LZ最不喜歡elseif,所以策略模式也是有缺點的,就是當策略改變時,我們需要使用elseif去判斷到底使用哪一個策略,哪怕使用簡單工廠,也避免不了這一點。比如我們又添加一類會員,那么你需要去添加elseif。再比如我們的會員現在打九折了,那么你需要添加一個九折的策略,這沒問題,我們對擴展開放,但是你需要修改elseif的分支,將會員的策略從八折替換為九折,這是簡單工廠的詬病,在之前已經提到過,對修改開放。

                   在簡單工廠模式一章中,LZ就遺留了一堆的elseif,並在工廠方法一章提供了解決方案,但是LZ沒有寫上來,這次LZ將整個解決方案搬上來,之前的簡單工廠可以使用相同的方法改進。

                   LZ在工廠方法一章中已經指明可以使用注解來處理這一問題,但是LZ只給出了實現的思路,沒有給出具體實現的方式,本次剛好將之前的實現方式補上,如果各位掌握了這里的實現方式,那么對於簡單工廠模式那一章的問題也自然而然可以輕松解決。

                   不過簡單工廠那一章的問題相對這里更簡單一點,因為那里我們只需要做一個servlet名稱與Class引用的映射關系,就可以消除掉elseif,使用現有的映射直接創造servlet實例,但是在這里就不行了,因為我們涉及到了客戶總金額的判斷,這不再是一個簡單的名稱與策略的對應關系。

                   所以我們需要給注解加入屬性上限和下限,用來表示策略生效的區間,用來解決總金額判斷的問題。

                   下面我們一步一步來,首先我們做一個注解,這個注解是用來給策略添加的,當中可以設置它的上下限,我們來看。

package com.calprice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//這是我們的有效區間注解,可以給策略添加有效區間的設置
@Target(ElementType.TYPE)//表示只能給類添加該注解
@Retention(RetentionPolicy.RUNTIME)//這個必須要將注解保留在運行時
public @interface TotalValidRegion {
     //為了簡單,我們讓區間只支持整數
    int max() default Integer.MAX_VALUE;
    int min() default Integer.MIN_VALUE;
}

                 這個注解很簡單,我們只是用它來記錄每一個策略的生效區間,下面我們就可以在我們的各個策略類里去設置我們的生效區間了,我們將策略類全部改成如下形式。

package com.calprice;

@TotalValidRegion(max=1000)//設置普通的在0-1000生效,以下類似,不再注釋
class Common implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice;
    }

}
@TotalValidRegion(min=1000,max=2000)
class Vip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.8;
    }

}
@TotalValidRegion(min=2000,max=3000)
class SuperVip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.7;
    }

}
@TotalValidRegion(min=3000)
class GoldVip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.5;
    }

}

                     好了,我們使用注解表示每一個策略生效的區間,下面我們要做最重要的工作,就是處理注解。我們在策略工廠處理注解。這個類會變的比較復雜一點,所以LZ直接加注釋。我們來看策略工廠,即改善后的簡單工廠。

package com.calprice1;

import java.io.File;
import java.io.FileFilter;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

//我們使用一個標准的簡單工廠來改進一下策略模式
public class CalPriceFactory {
    
    private static final String CAL_PRICE_PACKAGE = "com.calprice";//這里是一個常量,表示我們掃描策略的包,這是LZ的包名
    
    private ClassLoader classLoader = getClass().getClassLoader();//我們加載策略時的類加載器,我們任何類運行時信息必須來自該類加載器
    
    private List<Class<? extends CalPrice>> calPriceList;//策略列表
    
    //根據客戶的總金額產生相應的策略
    public CalPrice createCalPrice(Customer customer){
        //在策略列表查找策略
        for (Class<? extends CalPrice> clazz : calPriceList) {
            TotalValidRegion validRegion = handleAnnotation(clazz);//獲取該策略的注解
            //判斷金額是否在注解的區間
            if (customer.getTotalAmount() > validRegion.min() && customer.getTotalAmount() < validRegion.max()) {
                try {
                    //是的話我們返回一個當前策略的實例
                    return clazz.newInstance();
                } catch (Exception e) {
                    throw new RuntimeException("策略獲得失敗");
                } 
            }
        }
        throw new RuntimeException("策略獲得失敗");
    }
    
    //處理注解,我們傳入一個策略類,返回它的注解
    private TotalValidRegion handleAnnotation(Class<? extends CalPrice> clazz){
        Annotation[] annotations = clazz.getDeclaredAnnotations();
        if (annotations == null || annotations.length == 0) {
            return null;
        }
        for (int i = 0; i < annotations.length; i++) {
            if (annotations[i] instanceof TotalValidRegion) {
                return (TotalValidRegion) annotations[i];
            }
        }
        return null;
    }
    
    //單例
    private CalPriceFactory(){
        init();
    }
    
    //在工廠初始化時要初始化策略列表
    private void init(){
        calPriceList = new ArrayList<Class<? extends CalPrice>>();
        File[] resources = getResources();//獲取到包下所有的class文件
        Class<CalPrice> calPriceClazz = null;
        try {
            calPriceClazz = (Class<CalPrice>) classLoader.loadClass(CalPrice.class.getName());//使用相同的加載器加載策略接口
        } catch (ClassNotFoundException e1) {
            throw new RuntimeException("未找到策略接口");
        }
        for (int i = 0; i < resources.length; i++) {
            try {
                //載入包下的類
                Class<?> clazz = classLoader.loadClass(CAL_PRICE_PACKAGE + "."+resources[i].getName().replace(".class", ""));
                //判斷是否是CalPrice的實現類並且不是CalPrice它本身,滿足的話加入到策略列表
                if (CalPrice.class.isAssignableFrom(clazz) && clazz != calPriceClazz) {
                    calPriceList.add((Class<? extends CalPrice>) clazz);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    //獲取掃描的包下面所有的class文件
    private File[] getResources(){
        try {
            File file = new File(classLoader.getResource(CAL_PRICE_PACKAGE.replace(".", "/")).toURI());
            return file.listFiles(new FileFilter() {
                public boolean accept(File pathname) {
                    if (pathname.getName().endsWith(".class")) {//我們只掃描class文件
                        return true;
                    }
                    return false;
                }
            });
        } catch (URISyntaxException e) {
            throw new RuntimeException("未找到策略資源");
        }
    }
    
    public static CalPriceFactory getInstance(){
        return CalPriceFactoryInstance.instance;
    }
    
    private static class CalPriceFactoryInstance{
        
        private static CalPriceFactory instance = new CalPriceFactory();
    }
}

                     上述便是我們改善后的簡單工廠,雖然比剛開始的簡單工廠復雜了很多,但是我們的收益是很明顯的,現在我們隨便加入一個策略,並設置好它的生效區間,策略工廠就可以幫我們自動找到適應的策略。LZ在上面已經加了詳細的注釋,相信可以幫助各位看懂了。在這里再特別說明一點,我們的策略實現類最好放在一個包中,這樣我們可以掃描特定的包,可以加快初始化速度。
                      有了這個基於注解的簡單工廠,我們還需要稍微改變下客戶類,因為客戶類本來是調用的工廠的靜態方法,現在我們將工廠做成了單例,所以應該改成如下形式。

calPrice = CalPriceFactory.getInstance().createCalPrice(this);

                      好了,現在各位直接再調用客戶端代碼,會產生與之前一樣的結果,說明我們的策略正確選擇了。各位可以自己再試一下,比如到4000就變成四折,然后修改下客戶端,測試一下看是否能起效。
                      現在看來,我們已經使用簡單工廠,注解,反射等技術將策略模式優化的非常完美了,我們可以隨意新增策略,並且不需要修改原有的任何代碼。

                      但是,其實我們的設計還是有些不完美的,因為它無法支持策略的重疊,這是什么意思呢?

                      就是說我們同一時間只能采用一種策略,假設我們商店現在有這么一個需求,假設到端午節了,我們商店要采取滿1000返200,滿2000返400的方式,並且原有的打折還要繼續,這就相當於將返現金的活動與打折重疊計算了。

                     比如我是個金牌會員,假設我買了2000的東西,那么計算方式應該是先減去400為1600,再打五折,為800。最后這個會員只需要付800(靠,這減價有點狠,不過我們只是舉個例子,各位不要太在意數字)。

                     這就相當於將兩個策略重疊使用了,我們現在的設計無法支持這種方式。那怎么辦?你可能會想,可惡的老板就愛改需求。不過請永遠記得,優秀的程序猿面對各種刁難的需求,都可以輕松解決,而不是抱怨需求變的太頻繁或者太不合理,因為現實會告訴你,抱怨是沒用的,而且我們很多時候可以使用一些編程的技巧去容納這種變化。

                     剛才的需求,也是在提醒我們在設計一個系統時要考慮全面,我們雖然不應該考慮一些本不存在或者發生概率很小的需求,但像商店或者商場這種靈活的促銷方式,卻是我們剛開始就應該考慮到的。

                     現在我們的需求變了,即我們任意的策略都可以隨意組合,並且我們要求工廠幫我們自動判斷,並將策略疊加返回給我們。那么針對上面的設計我們還需要改善,如果要改善一個設計,我們就需要考慮現有的設計不能支持什么需求。我們考慮上述設計不能支持什么。LZ列出以下兩條。

                     1,我們只能根據客戶消費的總金額去處理,而不能根據客戶當次消費的金額去處理。

                     2,我們的設計只能支持單一策略,不能支持策略疊加。

                     為了滿足這兩個要求,我們需要添加一個類型的注解,去針對單次消費產生計費策略,另外,我們需要讓策略工廠能夠產生疊加的策略接口,那么沖着這個目標,我們首先定義如下三個注解,采用嵌套注解。

package com.calprice;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//我們定義一個嵌套注解
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidRegion {
    int max() default Integer.MAX_VALUE;
    int min() default Integer.MIN_VALUE;
    
    //既然可以任意組合,我們就需要給策略定義下順序,就比如剛才說的2000那個例子,按先返后打折的順序是800,反過來就是600了。
    //所以我們必須支持這一特性,默認0,為最優先
    int order() default 0;
}

                     我們定義上面這個嵌套注解是為了避免代碼的重復,因為這三個屬性我們在總額消費的策略注解和單次消費的策略注解中都要包括。下面給出另外兩個注解。

package com.calprice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//這是我們的總額有效區間注解,可以給策略添加有效區間的設置
@Target(ElementType.TYPE)//表示只能給類添加該注解
@Retention(RetentionPolicy.RUNTIME)//這個必須要將注解保留在運行時
public @interface TotalValidRegion {
    //我們引用有效區間注解
    ValidRegion value() default @ValidRegion;

}

                     上述這個總額注解與之前的注解基本一樣,直接換成了嵌套注解。下面還有一個一次性消費的注解。如下。

package com.calprice;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//這是我們針對單次消費的有效區間注解,可以給策略添加有效區間的設置
@Target(ElementType.TYPE)//表示只能給類添加該注解
@Retention(RetentionPolicy.RUNTIME)//這個必須要將注解保留在運行時
public @interface OnceValidRegion{
    //我們引用有效區間注解
    ValidRegion value() default @ValidRegion;
}

                      以上三個注解我們就可以支持剛才的第一個要求了,我們可以針對一次消費進行策略判斷,接下來我們需要修改策略工廠,去支持單次消費判斷,並且還要支持策略重疊。如下。

package com.calprice;

import java.io.File;
import java.io.FileFilter;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

//我們使用一個標准的簡單工廠來改進一下策略模式
public class CalPriceFactory {
    
    private static final String CAL_PRICE_PACKAGE = "com.calprice";//這里是一個常量,表示我們掃描策略的包,這是LZ的包名
    
    private ClassLoader classLoader = getClass().getClassLoader();//我們加載策略時的類加載器,我們任何類運行時信息必須來自該類加載器
    
    private List<Class<? extends CalPrice>> calPriceList;//策略列表
    
    //根據客戶的總金額產生相應的策略
    public CalPrice createCalPrice(Customer customer){
        //變化點:為了支持優先級排序,我們采用可排序的MAP支持,這個Map是為了儲存我們當前策略的運行時類信息
        SortedMap<Integer, Class<? extends CalPrice>> clazzMap = new TreeMap<Integer, Class<? extends CalPrice>>();
        //在策略列表查找策略
        for (Class<? extends CalPrice> clazz : calPriceList) {
            Annotation validRegion = handleAnnotation(clazz);//獲取該策略的注解
            //變化點:根據注解類型進行不同的判斷
            if (validRegion instanceof TotalValidRegion) {
                TotalValidRegion totalValidRegion = (TotalValidRegion) validRegion;
                //判斷總金額是否在注解的區間
                if (customer.getTotalAmount() > totalValidRegion.value().min() && customer.getTotalAmount() < totalValidRegion.value().max()) {
                    clazzMap.put(totalValidRegion.value().order(), clazz);//將采用的策略放入MAP
                }
            }
            else if (validRegion instanceof OnceValidRegion) {
                OnceValidRegion onceValidRegion = (OnceValidRegion) validRegion;
                //判斷單次金額是否在注解的區間,注意這次判斷的是客戶當次消費的金額
                if (customer.getAmount() > onceValidRegion.value().min() && customer.getAmount() < onceValidRegion.value().max()) {
                    clazzMap.put(onceValidRegion.value().order(), clazz);//將采用的策略放入MAP
                }
            }
        }
        try {
            //我們采用動態代理處理策略重疊的問題,相信看過LZ的代理模式的同學應該都對代理模式的原理很熟悉了,那么下面出現的代理類LZ將不再解釋,留給各位自己琢磨。
            return CalPriceProxy.getProxy(clazzMap);
        } catch (Exception e) {
            throw new RuntimeException("策略獲得失敗");
        }
    }
    
    //處理注解,我們傳入一個策略類,返回它的注解
    private Annotation handleAnnotation(Class<? extends CalPrice> clazz){
        Annotation[] annotations = clazz.getDeclaredAnnotations();
        if (annotations == null || annotations.length == 0) {
            return null;
        }
        for (int i = 0; i < annotations.length; i++) {
            //變化點:這里稍微改動了下,如果是TotalValidRegion,OnceValidRegion這兩種注解則返回
            if (annotations[i] instanceof TotalValidRegion || annotations[i] instanceof OnceValidRegion) {
                return annotations[i];
            }
        }
        return null;
    }
    
    /*  以下不需要改變  */
    
    //單例,並且我們需要在工廠初始化的時候
    private CalPriceFactory(){
        init();
    }
    
    //在工廠初始化時要初始化策略列表
    private void init(){
        calPriceList = new ArrayList<Class<? extends CalPrice>>();
        File[] resources = getResources();//獲取到包下所有的class文件
        Class<CalPrice> calPriceClazz = null;
        try {
            calPriceClazz = (Class<CalPrice>) classLoader.loadClass(CalPrice.class.getName());//使用相同的加載器加載策略接口
        } catch (ClassNotFoundException e1) {
            throw new RuntimeException("未找到策略接口");
        }
        for (int i = 0; i < resources.length; i++) {
            try {
                //載入包下的類
                Class<?> clazz = classLoader.loadClass(CAL_PRICE_PACKAGE + "."+resources[i].getName().replace(".class", ""));
                //判斷是否是CalPrice的實現類並且不是CalPrice它本身,滿足的話加入到策略列表
                if (CalPrice.class.isAssignableFrom(clazz) && clazz != calPriceClazz) {
                    calPriceList.add((Class<? extends CalPrice>) clazz);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    //獲取掃描的包下面所有的class文件
    private File[] getResources(){
        try {
            File file = new File(classLoader.getResource(CAL_PRICE_PACKAGE.replace(".", "/")).toURI());
            return file.listFiles(new FileFilter() {
                public boolean accept(File pathname) {
                    if (pathname.getName().endsWith(".class")) {//我們只掃描class文件
                        return true;
                    }
                    return false;
                }
            });
        } catch (URISyntaxException e) {
            throw new RuntimeException("未找到策略資源");
        }
    }
    
    public static CalPriceFactory getInstance(){
        return CalPriceFactoryInstance.instance;
    }
    
    private static class CalPriceFactoryInstance{
        
        private static CalPriceFactory instance = new CalPriceFactory();
    }
}

                     上面我們改動的地方並不多,主要是添加了一個單次消費的判斷,另外就是沒有直接返回策略實例,而是將滿足條件的策略類信息傳遞給代理,產生一個代理,從而滿足我們第二個要求,即策略可以重疊,下面LZ給出代理類,相信如果各位看過LZ的代理模式,並完全理解了,那么看懂這里面的道理是非常簡單的,為此,LZ不在多做解釋。代理類如下。

package com.calprice;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.SortedMap;

public class CalPriceProxy implements InvocationHandler{

    private SortedMap<Integer, Class<? extends CalPrice>> clazzMap;
    
    private CalPriceProxy(SortedMap<Integer, Class<? extends CalPrice>> clazzMap) {
        super();
        this.clazzMap = clazzMap;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Double result = 0D;
        if (method.getName().equals("calPrice")) {
            for (Class<? extends CalPrice> clazz : clazzMap.values()) {
                if (result == 0) {
                    result = (Double) method.invoke(clazz.newInstance(), args);
                }else {
                    result = (Double) method.invoke(clazz.newInstance(), result);
                }
            }
            return result;
        }
        return null;
    }
    
    public static CalPrice getProxy(SortedMap<Integer, Class<? extends CalPrice>> clazzMap){
        return (CalPrice) Proxy.newProxyInstance(CalPriceProxy.class.getClassLoader(), new Class<?>[]{CalPrice.class}, new CalPriceProxy(clazzMap));
    }

}

                      好了,這下我們可以支持策略重疊了,我給各位一個指定好的一系列策略,如下。

package com.calprice;
//我們使用嵌套注解,並且制定我們打折的各個策略順序是99,這算是很靠后的
//因為我們最后打折算出來錢是最多的,這個一算就很清楚,LZ不再解釋數學問題
@TotalValidRegion(@ValidRegion(max=1000,order=99))
class Common implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice;
    }

}
@TotalValidRegion(@ValidRegion(min=1000,max=2000,order=99))
class Vip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.8;
    }

}
@TotalValidRegion(@ValidRegion(min=2000,max=3000,order=99))
class SuperVip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.7;
    }

}
@TotalValidRegion(@ValidRegion(min=3000,order=99))
class GoldVip implements CalPrice{

    public Double calPrice(Double originalPrice) {
        return originalPrice * 0.5;
    }

}
@OnceValidRegion(@ValidRegion(min=1000,max=2000,order=40))
class OneTDTwoH implements CalPrice{
    
    public Double calPrice(Double originalPrice) {
        return originalPrice - 200;
    }
    
}

@OnceValidRegion(@ValidRegion(min=2000,order=40))
class TwotDFourH implements CalPrice{
    
    public Double calPrice(Double originalPrice) {
        return originalPrice - 400;
    }
    
}

                      這里面相比上面,又添了兩種策略,即滿1000返200和滿2000返400,並且優先級高於打折,也就是說會先計算現金返回,再打折。各位可以使用如下客戶端測試一下。

package com.calprice;

//客戶端調用
public class Client {

    public static void main(String[] args) {
        Customer customer = new Customer();
        customer.buy(500D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
        customer.buy(1200D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
        customer.buy(1200D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
        customer.buy(1200D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
        customer.buy(2600D);
        System.out.println("客戶需要付錢:" + customer.calLastAmount());
    }
    
}

                      當然你還可以多寫幾個buy方法,看下我們的策略是否在發揮作用。這下我們的策略模式就更加靈活了,不僅可以支持策略的隨意增加,而且還可以重疊。相信這下老板再要搞什么促銷活動,都可以輕松應付了吧。當然,沒有最好的設計只有更適合的設計,只要可以滿足大部分需求,容納大部分變化就算是很好的設計了。實在容納和滿足不了,我們還可以重構,而且重構往往會比預先設計更加湊效。

                      本次講解策略模式算是由淺及深的方式,剛開始給出的是LZ作為一個JAVA新人的時候對策略模式的理解,后面是一個代入了業務場景的例子,以及后面的逐漸使用簡單工廠,注解,反射,代理等方式改善我們的策略工廠的過程,算是LZ和各位讀者一起進行一個設計思想的鍛煉吧。

                     策略模式本身並不太復雜,實現也比較簡單,但是我們卻花費了大量的篇幅去完善它,這是因為完善策略模式往往比使用更加復雜。

                     最后總結一下策略模式的使用場景,就是有一系列的可相互替換的算法的時候,我們就可以使用策略模式將這些算法做成接口的實現,並讓我們依賴於算法的類依賴於抽象的算法接口,這樣可以徹底消除類與具體算法之間的耦合。

                     比如我們現在客戶類,它就知道有個CalPrice接口可以計算最終價格,其它的它什么都不知道了,這不正是我們之前總綱中提到的最小知道原則嗎?
                     當然策略模式也有缺點,就是我們不停的在各個算法間切換,造成很多邏輯判斷,不過我們本章已經給各位提供了思路,我們是可以使用一些其他的模式或者JAVA的技術去消除這種邏輯判斷的,對吧?

                     最最后,感謝各位的收看。
                     下期預告,適配器模式。




 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM