Android常見設計模式九:策略模式

對于開發人員來說,設計模式有時候就是一道坎,但是設計模式又非常有用,過了這道坎,它可以讓你水平提高一個檔次。而在android開發中,必要的了解一些設計模式又是必須的,因為設計模式在Android源碼中,可以說是無處不在。對于想系統的學習設計模式的同學,這里推薦一本書,《大話設計模式》。


Android常用設計模式系列:

面向對象的基礎特征
面向對象的設計原則
單例模式
模板模式
適配器模式
工廠模式
代理模式
原型模式
策略模式
Build模式
觀察者模式
裝飾者模式
中介模式
門面模式


策略模式

策略模式是非常常見的設計模式之一,寫個筆記,記錄一下我的學習過程和心得。

首先了解一些策略模式的定義。

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

乍一看,一如既往的一臉懵逼,還是舉個栗子吧。

假設我們要出去旅游,而去旅游出行的方式有很多,有步行,有坐火車,有坐飛機等等。而如果不使用任何模式,我們的代碼可能就是這樣子的。

    public class TravelStrategy {
        enum Strategy{
            WALK,PLANE,SUBWAY
        }
        private Strategy strategy;
        public TravelStrategy(Strategy strategy){
            this.strategy=strategy;
        }

        public void travel(){
            if(strategy==Strategy.WALK){
                print("walk");
            }else if(strategy==Strategy.PLANE){
                print("plane");
            }else if(strategy==Strategy.SUBWAY){
                print("subway");
            }
        }

        public static void main(String[] args) {
            TravelStrategy walk=new TravelStrategy(Strategy.WALK);
            walk.travel();
            TravelStrategy plane=new TravelStrategy(Strategy.PLANE);
            plane.travel();
            TravelStrategy subway=new TravelStrategy(Strategy.SUBWAY);
            subway.travel();
        }
    }

這樣做有一個致命的缺點,一旦出行的方式要增加,我們就不得不增加新的else if語句,而這違反了面向對象的原則之一,對修改封閉。而這時候,策略模式則可以完美的解決這一切。

首先,需要定義一個策略接口。

public interface Strategy {
  void travel();
}

然后根據不同的出行方式實行對應的接口

    public class WalkStrategy implements Strategy{
        @Override
        public void travel() {
            System.out.println("walk");
        }
    }
    public class PlaneStrategy implements Strategy{
        @Override
        public void travel() {
            System.out.println("plane");
        }
    }
    public class SubwayStrategy implements Strategy{
        @Override
        public void travel() {
            System.out.println("subway");
        }
    }

此外還需要一個包裝策略的類,并調用策略接口中的方法

    public class TravelContext {
        Strategy strategy;
        public Strategy getStrategy() {
            return strategy;
        }
        public void setStrategy(Strategy strategy) {
            this.strategy = strategy;
        }
        public void travel() {
            if (strategy != null) {
                strategy.travel();
            }
        }
    }

測試一下代碼

    public class Main {
        public static void main(String[] args) {
            TravelContext travelContext=new TravelContext();
            travelContext.setStrategy(new PlaneStrategy());
            travelContext.travel();
            travelContext.setStrategy(new WalkStrategy());
            travelContext.travel();
            travelContext.setStrategy(new SubwayStrategy());
            travelContext.travel();
        }
    }

輸出結果如下

plane
walk
subway

可以看到,應用了策略模式后,如果我們想增加新的出行方式,完全不必要修改現有的類,我們只需要實現策略接口即可,這就是面向對象中的對擴展開放準則。假設現在我們增加了一種自行車出行的方式。只需新增一個類即可。

    public class BikeStrategy implements Strategy{
        @Override
        public void travel() {
            System.out.println("bike");
        }
    }

之后設置策略即可

    public class Main {
        public static void main(String[] args) {
            TravelContext travelContext=new TravelContext();
            travelContext.setStrategy(new BikeStrategy());
            travelContext.travel();
        }
    }

廣泛應用

而在Android的系統源碼中,策略模式也是應用的相當廣泛的.最典型的就是屬性動畫中的應用.

想詳細了解屬性動畫的,可以看我另外一系列文章 android動畫

我們知道,在屬性動畫中,有一個東西叫做插值器,它的作用就是根據時間流逝的百分比來來計算出當前屬性值改變的百分比.

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

    private static final TimeInterpolator sDefaultInterpolator =
            new AccelerateDecelerateInterpolator();
    private TimeInterpolator mInterpolator = sDefaultInterpolator;
    @Override
    public void setInterpolator(TimeInterpolator value) {
        if (value != null) {
            mInterpolator = value;
        } else {
            mInterpolator = new LinearInterpolator();
        }
    }
    @Override
    public TimeInterpolator getInterpolator() {
        return mInterpolator;
    }
    public interface Interpolator extends TimeInterpolator {
        // A new interface, TimeInterpolator, was introduced for the new android.animation
        // package. This older Interpolator interface extends TimeInterpolator so that users of
        // the new Animator-based animations can use either the old Interpolator implementations or
        // new classes that implement TimeInterpolator directly.
    }

此外還有一個BaseInterpolator插值器實現了Interpolator接口,并且是一個抽象類

    abstract public class BaseInterpolator implements Interpolator {
        private int mChangingConfiguration;
        /**
         * @hide
         */
        public int getChangingConfiguration() {
            return mChangingConfiguration;
        }
        /**
         * @hide
         */
        void setChangingConfiguration(int changingConfiguration) {
            mChangingConfiguration = changingConfiguration;
        }
    }

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

    public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
        public LinearInterpolator() {
        }
        public LinearInterpolator(Context context, AttributeSet attrs) {
        }
        public float getInterpolation(float input) {
            return input;
        }
        /** @hide */
        @Override
        public long createNativeInterpolator() {
            return NativeInterpolatorFactoryHelper.createLinearInterpolator();
        }
    }
    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();
        }
    }

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

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

public interface TypeEvaluator<T> {
  public T evaluate(float fraction, T startValue, T endValue);
 } 
    public class IntEvaluator implements TypeEvaluator<Integer> {
        public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
            int startInt = startValue;
            return (int)(startInt + fraction * (endValue - startInt));
        }
    }
    public class FloatEvaluator implements TypeEvaluator<Number> {
        public Float evaluate(float fraction, Number startValue, Number endValue) {
            float startFloat = startValue.floatValue();
            return startFloat + fraction * (endValue.floatValue() - startFloat);
        }
    }
    public class PointFEvaluator implements TypeEvaluator<PointF> {
        private PointF mPoint;
        public PointFEvaluator() {
        }
        public PointFEvaluator(PointF reuse) {
            mPoint = reuse;
        }
        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            float x = startValue.x + (fraction * (endValue.x - startValue.x));
            float y = startValue.y + (fraction * (endValue.y - startValue.y));
            if (mPoint != null) {
                mPoint.set(x, y);
                return mPoint;
            } else {
                return new PointF(x, y);
            }
        }
    }
    public class ArgbEvaluator implements TypeEvaluator {
        private static final ArgbEvaluator sInstance = new ArgbEvaluator();
        public static ArgbEvaluator getInstance() {
            return sInstance;
        }
        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))));
        }
    }

上面的都是一些系統實現好的估值策略,在內部調用估值器的evaluate方法即可返回改變后的值了。我們也可以自定義估值策略。這里就不展開了。

當然,在開源框架中,策略模式也是無處不在的。

首先在Volley中,策略模式就能看到。

有一個重試策略接口

    public interface RetryPolicy {
        public int getCurrentTimeout();//獲取當前請求用時(用于 Log)
        public int getCurrentRetryCount();//獲取已經重試的次數(用于 Log)
        public void retry(VolleyError error) throws VolleyError;//確定是否重試,參數為這次異常的具體信息。在請求異常時此接口會被調用,可在此函數實現中拋出傳入的異常表示停止重試。
    }

在Volley中,該接口有一個默認的實現DefaultRetryPolicy,Volley 默認的重試策略實現類。主要通過在 retry(…) 函數中判斷重試次數是否達到上限確定是否繼續重試。

public class DefaultRetryPolicy implements RetryPolicy {
 ...
} 

而策略的設置是在Request類中

    public abstract class Request<T> implements Comparable<Request<T>> {
        private RetryPolicy mRetryPolicy;
        public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
            mRetryPolicy = retryPolicy;
            return this;
        }
        public RetryPolicy getRetryPolicy() {
            return mRetryPolicy;
        }
    }

此外,各大網絡請求框架,或多或少都會使用到緩存,緩存一般會定義一個Cache接口,然后實現不同的緩存策略,如內存緩存,磁盤緩存等等,這個緩存的實現,其實也可以使用策略模式。直接看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();
            }
        }
    }

它有兩個實現類NoCacheDiskBasedCache,使用的時候設置對應的緩存策略即可。

總結

在android開發中,ViewPager是一個使用非常常見的控件,它的使用往往需要伴隨一個Indicator指示器。如果讓你重新實現一個ViewPager,并且帶有Indicator,這時候,你會不會想到用策略模式呢?在你自己寫的ViewPager中(不是系統的,當然你也可以繼承系統的)持有一個策略接口Indicator的變量,通過set方法設置策略,然后ViewPager滑動的時候調用策略接口的對應方法改變指示器。默認提供幾個Indicator接口的實現類,比如圓形指示器CircleIndicator、線性指示器LineIndicator、Tab指示器TabIndicator、圖標指示器IconIndicator 等等等等。有興趣的話自己去實現一個吧。

優點

  1. 策略模式提供了管理相關的算法族的辦法。策略類的等級結構定義了一個算法或行為族。恰當使用繼承可以把公共的代碼移到父類里面,從而避免重復的代碼。
  2. 策略模式提供了可以替換繼承關系的辦法。繼承可以處理多種算法或行為。如果不是用策略模式,那么使用算法或行為的環境類就可能會有一些子類,每一個子類提供一個不同的算法或行為。但是,這樣一來算法或行為的使用者就和算法或行為本身混在一起。決定使用哪一種算法或采取哪一種行為的邏輯就和算法或行為的邏輯混合在一起,從而不可能再獨立演化。繼承使得動態改變算法或行為變得不可能。
  3. 使用策略模式可以避免使用多重條件轉移語句。多重轉移語句不易維護,它把采取哪一種算法或采取哪一種行為的邏輯與算法或行為的邏輯混合在一起,統統列在一個多重轉移語句里面,比使用繼承的辦法還要原始和落后。

缺點

  1. 客戶端必須知道所有的策略類,并自行決定使用哪一個策略類。這就意味著客戶端必須理解這些算法的區別,以便適時選擇恰當的算法類。換言之,策略模式只適用于客戶端知道所有的算法或行為的情況。
  2. 策略模式造成很多的策略類。有時候可以通過把依賴于環境的狀態保存到客戶端里面,而將策略類設計成可共享的,這樣策略類實例可以被不同客戶端使用。換言之,可以使用享元模式來減少對象的數量。

適用場景

  1. 如果在一個系統里面有許多類,它們之間的區別僅在于它們的行為,那么使用策略模式可以動態地讓一個對象在許多行為中選擇一種行為。
  2. 一個系統需要動態地在幾種算法中選擇一種。那么這些算法可以包裝到一個個的具體算法類里面,而這些具體算法類都是一個抽象算法類的子類。換言之,這些具體算法類均有統一的接口,由于多態性原則,客戶端可以選擇使用任何一個具體算法類,并只持有一個數據類型是抽象算法類的對象。
  3. 一個系統的算法使用的數據不可以讓客戶端知道。策略模式可以避免讓客戶端涉及到不必要接觸到的復雜的和只與算法有關的數據。
  4. 如果一個對象有很多的行為,如果不用恰當的模式,這些行為就只好使用多重的條件選擇語句來實現。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容