設計模式學習之策略模式

寫代碼時總會出很多的if…else,或者case。如果在一個條件語句中又包含了多個條件語句就會使得代碼變得臃腫,維護的成本也會加大,而策略模式就能較好的解決這個問題,本篇博客就帶你詳細瞭解策略模式。

策略模式的定義和使用場景

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

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

策略模式的使用場景:

1.針對同一類型問題的多種處理方式,僅僅是具體行爲有差別時; 
2.需要安全地封裝多種同一類型的操作時; 
3.出現同一抽象類有多個子類,而又需要使用 if-else 或者 switch-case 來選擇具體子類時。

UML類圖

這裏寫圖片描述

這個模式涉及到三個角色:

環境(Context)角色:持有一個Strategy的引用。

抽象策略(Strategy)角色:這是一個抽象角色,通常由一個接口或抽象類實現。此角色給出所有的具體策略類所需的接口。

具體策略(ConcreteStrategy)角色:包裝了相關的算法或行爲。

策略模式的典型代碼如下:

抽象策略類

public interface Strategy {
    /**
     * 策略方法
     */
    public void strategyInterface();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

具體策略類

public class ConcreteStrategyA implements Strategy {

    @Override
    public void strategyInterface() {
        //相關的業務
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
public class ConcreteStrategyB implements Strategy {

    @Override
    public void strategyInterface() {
        //相關的業務
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

環境角色類

public class Context {
    //持有一個具體策略的對象
    private Strategy strategy;
    /**
     * 構造函數,傳入一個具體策略對象
     * @param strategy    具體策略對象
     */
    public Context(Strategy strategy){
        this.strategy = strategy;
    }
    /**
     * 策略方法
     */
    public void contextInterface(){

        strategy.strategyInterface();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

策略模式例子

假設鵝廠推出了3種會員,分別爲會員,超級會員以及金牌會員,還有就是普通玩家,針對不同類別的玩家,購買《王者農藥》皮膚有不同的打折方式,並且一個顧客每消費10000就增加一個級別,那麼我們就可以使用策略模式,因爲策略模式描述的就是算法的不同,這裏我們舉例就採用最簡單的,以上四種玩家分別採用原價(普通玩家),九折,八折和七價的收錢方式。

那麼我們首先要有一個計算價格的策略接口

public interface CalPrice {
    //根據原價返回一個最終的價格
    Double calPrice(Double orgnicPrice);
}
  • 1
  • 2
  • 3
  • 4

下面是4種玩家的計算方式的實現

public class Orgnic implements CalPrice {

    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
public class Vip implements CalPrice {
    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice * 0.9;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
public class SuperVip implements CalPrice {
    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice * 0.8;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
public class GoldVip implements CalPrice {
    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice * 0.7;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我們看客戶類,我們需要客戶類幫我們完成玩家升級的功能。

public class Player {
    private Double totalAmount = 0D;//客戶在鵝廠消費的總額
    private Double amount = 0D;//客戶單次消費金額
    private CalPrice calPrice = new Orgnic();//每個客戶都有一個計算價格的策略,初始都是普通計算,即原價

    //客戶購買皮膚,就會增加它的總額
    public void buy(Double amount) {
        this.amount = amount;
        totalAmount += amount;
        if (totalAmount > 30000) {//30000則改爲金牌會員計算方式
            calPrice = new GoldVip();
        } else if (totalAmount > 20000) {//類似
            calPrice = new SuperVip();
        } else if (totalAmount > 10000) {//類似
            calPrice = new Vip();
        }
    }

    //計算客戶最終要付的錢
    public Double calLastAmount() {
        return calPrice.calPrice(amount);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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


public class Client {
    public static void main(String[] args) {
        Player player = new Player();
        player.buy(5000D);
        System.out.println("玩家需要付錢:" + player.calLastAmount());
        player.buy(12000D);
        System.out.println("玩家需要付錢:" + player.calLastAmount());
        player.buy(12000D);
        System.out.println("玩家需要付錢:" + player.calLastAmount());
        player.buy(12000D);
        System.out.println("玩家需要付錢:" + player.calLastAmount());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

運行以後會發現,第一次是原價,第二次是九折,第三次是八折,最後一次則是七價。這樣設計的好處是,客戶不再依賴於具體的收費策略,依賴於抽象永遠是正確的。

在上面的基礎上,我們可以使用簡單工廠來稍微進行優化

public class CalPriceFactory {
    private CalPriceFactory(){}
    //根據客戶的總金額產生相應的策略
    public static CalPrice createCalPrice(Player customer){
        if (customer.getTotalAmount() > 30000) {//3000則改爲金牌會員計算方式
            return new GoldVip();
        }else if (customer.getTotalAmount() > 20000) {//類似
            return new SuperVip();
        }else if (customer.getTotalAmount() > 10000) {//類似
            return new Vip();
        }else {
            return new Orgnic();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

public class Player {
    private Double totalAmount = 0D;//客戶在鵝廠消費的總額
    private Double amount = 0D;//客戶單次消費金額
    private CalPrice calPrice = new Orgnic();//每個客戶都有一個計算價格的策略,初始都是普通計算,即原價

    //客戶購買皮膚,就會增加它的總額
    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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

雖然結合簡單工廠模式,我們的策略模式靈活了一些,但不免發現在工廠中多了if-else判斷,也就是如果增加一個會員類別,我又得增加一個else-if語句,這是簡單工廠的缺點,對修改開放。

那有什麼方法,可以較好的解決這個問題呢?那就是使用註解, 所以我們需要給註解加入屬性上限和下限,用來表示策略生效的區間,用來解決總金額判斷的問題。

1.首先我們做一個註解,這個註解是用來給策略添加的,當中可以設置它的上下限

//這是有效價格區間註解,可以給策略添加有效區間的設置
@Target(ElementType.TYPE)//表示只能給類添加該註解
@Retention(RetentionPolicy.RUNTIME)//這個必須要將註解保留在運行時
public @interface PriceRegion {
    int max() default Integer.MAX_VALUE;
    int min() default Integer.MIN_VALUE;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

可以看到,我們只是使用這個註解來聲明每一個策略的生效區間,於是對策略進行修改

@PriceRegion(max = 10000)
public class Orgnic implements CalPrice {

    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
@PriceRegion(max=20000)
public class Vip implements CalPrice {
    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice * 0.9;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
@PriceRegion(min=20000,max=30000)
public class SuperVip implements CalPrice {
    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice * 0.8;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
@PriceRegion(min=3000)
public class GoldVip implements CalPrice {
    @Override
    public Double calPrice(Double orgnicPrice) {
        return orgnicPrice * 0.7;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

接下來就是在策略工廠中去處理註解

public class CalPriceFactory {
    private static final String CAL_PRICE_PACKAGE = "com.example.stragedemo";//這裏是一個常量,表示我們掃描策略的包

    private ClassLoader classLoader = getClass().getClassLoader();

    private List<Class<? extends CalPrice>> calPriceList;//策略列表

    //根據玩家的總金額產生相應的策略
    public CalPrice createCalPrice(Player player) {
        //在策略列表查找策略
        for (Class<? extends CalPrice> clazz : calPriceList) {
            PriceRegion validRegion = handleAnnotation(clazz);//獲取該策略的註解
            //判斷金額是否在註解的區間
            if (player.getTotalAmount() > validRegion.min() && player.getTotalAmount() < validRegion.max()) {
                try {
                    //是的話我們返回一個當前策略的實例
                    return clazz.newInstance();
                } catch (Exception e) {
                    throw new RuntimeException("策略獲得失敗");
                }
            }
        }
        throw new RuntimeException("策略獲得失敗");
    }

    //處理註解,我們傳入一個策略類,返回它的註解
    private PriceRegion 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 PriceRegion) {
                return (PriceRegion) 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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95

雖然工廠裏的邏輯增加了,但是解耦的效果達到了,現在我們隨便加入一個策略,並設置好它的生效區間,策略工廠就可以幫我們自動找到適應的策略。

Android源碼中的策略模式

Android的源碼中,策略模式最典型的就是屬性動畫中的應用. 
我們使用屬性動畫的時候,可以通過set方法對插值器進行設置.可以看到內部維持了一個時間插值器的引用,並設置了getter和setter方法,默認情況下是先加速後減速的插值器,set方法如果傳入的是null,則是線性插值器。而時間插值器TimeInterpolator是個接口,有一個接口繼承了該接口,就是Interpolator這個接口,其作用是爲了保持兼容

public interface TimeInterpolator {

    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

平時我們使用的時候,通過設置不同的插值器,實現不同的動畫速率變換效果,比如線性變換,回彈,自由落體等等。這些都是插值器接口的具體實現,也就是具體的插值器策略。

@HasNativeInterpolator
public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolatorFactory {
    public AccelerateDecelerateInterpolator() {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

內部使用的時候直接調用getInterpolation方法就可以返回對應的值了,也就是屬性值改變的百分比。

屬性動畫中另外一個應用策略模式的地方就是估值器,它的作用是根據當前屬性改變的百分比來計算改變後的屬性值。該屬性和插值器是類似的,有幾個默認的實現。其中TypeEvaluator是一個接口。

public interface TypeEvaluator<T> {

    /**
     * This function returns the result of linearly interpolating the start and end values, with
     * <code>fraction</code> representing the proportion between the start and end values. The
     * calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
     * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
     * and <code>t</code> is <code>fraction</code>.
     *
     * @param fraction   The fraction from the starting to the ending values
     * @param startValue The start value.
     * @param endValue   The end value.
     * @return A linear interpolation between the start and end values, given the
     *         <code>fraction</code> parameter.
     */
    public T evaluate(float fraction, T startValue, T endValue);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

TypeEvaluator的實現

public class ArgbEvaluator implements TypeEvaluator {
    private static final ArgbEvaluator sInstance = new ArgbEvaluator();

    /**
     * Returns an instance of <code>ArgbEvaluator</code> that may be used in
     * {@link ValueAnimator#setEvaluator(TypeEvaluator)}. The same instance may
     * be used in multiple <code>Animator</code>s because it holds no state.
     * @return An instance of <code>ArgbEvalutor</code>.
     *
     * @hide
     */
    public static ArgbEvaluator getInstance() {
        return sInstance;
    }

    /**
     * This function returns the calculated in-between value for a color
     * given integers that represent the start and end values in the four
     * bytes of the 32-bit int. Each channel is separately linearly interpolated
     * and the resulting calculated values are recombined into the return value.
     *
     * @param fraction The fraction from the starting to the ending values
     * @param startValue A 32-bit int value representing colors in the
     * separate bytes of the parameter
     * @param endValue A 32-bit int value representing colors in the
     * separate bytes of the parameter
     * @return A value that is calculated to be the linearly interpolated
     * result, derived by separating the start and end values into separate
     * color channels and interpolating each one separately, recombining the
     * resulting values in the same way.
     */
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        int startInt = (Integer) startValue;
        int startA = (startInt >> 24) & 0xff;
        int startR = (startInt >> 16) & 0xff;
        int startG = (startInt >> 8) & 0xff;
        int startB = startInt & 0xff;

        int endInt = (Integer) endValue;
        int endA = (endInt >> 24) & 0xff;
        int endR = (endInt >> 16) & 0xff;
        int endG = (endInt >> 8) & 0xff;
        int endB = endInt & 0xff;

        return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
                (int)((startR + (int)(fraction * (endR - startR))) << 16) |
                (int)((startG + (int)(fraction * (endG - startG))) << 8) |
                (int)((startB + (int)(fraction * (endB - startB))));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

在Volley框架中,同樣使用到了策略模式,其中定義了一個緩存接口

/**
 * An interface for a cache keyed by a String with a byte array as data.
 */
public interface Cache {
    /**
     * Retrieves an entry from the cache.
     * @param key Cache key
     * @return An {@link Entry} or null in the event of a cache miss
     */
    public Entry get(String key);

    /**
     * Adds or replaces an entry to the cache.
     * @param key Cache key
     * @param entry Data to store and metadata for cache coherency, TTL, etc.
     */
    public void put(String key, Entry entry);

    /**
     * Performs any potentially long-running actions needed to initialize the cache;
     * will be called from a worker thread.
     */
    public void initialize();

    /**
     * Invalidates an entry in the cache.
     * @param key Cache key
     * @param fullExpire True to fully expire the entry, false to soft expire
     */
    public void invalidate(String key, boolean fullExpire);

    /**
     * Removes an entry from the cache.
     * @param key Cache key
     */
    public void remove(String key);

    /**
     * Empties the cache.
     */
    public void clear();

    /**
     * Data and metadata for an entry returned by the cache.
     */
    public static class Entry {
        /** The data returned from cache. */
        public byte[] data;

        /** ETag for cache coherency. */
        public String etag;

        /** Date of this response as reported by the server. */
        public long serverDate;

        /** The last modified date for the requested object. */
        public long lastModified;

        /** TTL for this record. */
        public long ttl;

        /** Soft TTL for this record. */
        public long softTtl;

        /** Immutable response headers as received from server; must be non-null. */
        public Map<String, String> responseHeaders = Collections.emptyMap();

        /** True if the entry is expired. */
        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

        /** True if a refresh is needed from the original data source. */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

它有兩個實現類NoCache和DiskBasedCache,使用的時候設置對應的緩存策略。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章