Android設計模式之策略模式在項目中的實際使用總結

前言

策略模式在開發中也常常用到,當實現某一個功能時如支付功能時,支付功能可以有多種實現方式,比如微信支付、支付寶支付、一網通支付。再比如實現分享時也可以有多種策略,可以分享到QQ、微信、微博等社交平臺。

在衆多的實現方式中,可以將功能中涉及到的通用方法或策略提取出來,提供一個統一的接口,不同的算法或者策略有不同的實現類,這次在程序客戶端就可以通過注入不同的實現對象來實現算法或者策略的動態策略,這種模式的可維護性、或擴展性更好。這就是本文要介紹的策略模式。

策略模式定義

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

策略模式使用場景

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

策略模式模式UML類圖

在這裏插入圖片描述

上圖的角色介紹:

  • Context :用來操作策略的上下文環境;
  • Stragety:策略的抽象;
  • StagetyA、StagetyB:具體的策略的實現

策略模式簡單實現

以《Android源碼設計模式解析與實戰》書的公交車爲例,本文用kotlin實現。

1.首先定義策略的接口

/**
 * 定義策略的接口
 * 計算價格的接口
 */
interface ICalculateStrategy {
    fun calculatePrice(km:Int):Int
}

2.然後定義一個用來操作策略的上下文環境。

注: TranficCalculator 中引用的是接口,當更換具體實現類時,此內不用修改代碼,這就是針對接口編程的好處。

//定義一個用來操作策略的上下文環境
class TranficCalculator {
    lateinit var mStategy:ICalculateStrategy

    fun setStrategy(stategy:ICalculateStrategy):TranficCalculator{
        mStategy = stategy
        return this
    }
    fun calculatePrice(km:Int):Int{
        return mStategy.calculatePrice(km)
    }
}

3.接着定義不同的策略出租車、公交車和地鐵票價

/**
 * 出租車策略
 * 價格簡化爲公里數的2倍
 */
class TaxiStrategy : ICalculateStrategy {
    override fun calculatePrice(km: Int): Int {
        return 2 * km
    }
}

/**
 *  公交車價格計算的策略
 */
class BusStrategy : ICalculateStrategy {
    override fun calculatePrice(km: Int): Int {
        //超過10公里的總距離
        val extraTotal = km - 10
        //超過的距離是5公里的倍數
        val extraFactor = extraTotal / 5
        //超過的距離對5公里取餘
        val fraction = extraTotal % 5
        //價格計算
        var price = 1 + extraFactor * 1
        return if (fraction > 0) ++price; else price
    }
}

/**
 * 地鐵價格計算的策略
 * 6公里(含)內3元; 6-12公里(含)4元;12-22公里(含)5元;22-32公里(含)6元;其餘簡化爲7元
 */
class SubwayStrategy:ICalculateStrategy {
    override fun calculatePrice(km: Int): Int {
        return when {
            km <= 6 -> 3
            km in 7..11 -> 4
            km in 12..21 -> 5
            km in 22..31 -> 6
            else -> 7
        }
    }
}

4.最後寫一個測試

package designPatters

//注意Test類中,直接新建File類 不要class
fun main() {
    println("地鐵16公里的價格:" + TranficCalculator().setStrategy(SubwayStrategy()).calculatePrice(16))
    
    println("公交車16公里的價格:" + TranficCalculator().setStrategy(BusStrategy()).calculatePrice(16))
    
    println("出租車16公里的價格:"+TranficCalculator().setStrategy(TaxiStrategy()).calculatePrice(16))
}

運行結果如下:
在這裏插入圖片描述

UML類圖:
在這裏插入圖片描述

通過策略模式簡化了類的結構,方便了程序的擴展性、和解耦性,當我們在定義一個新的策略時候,只需要通過setStrategy 就可以輕鬆實現策略的替換,而不是用if-else 來做條件判斷。這樣保證了系統的簡化邏輯以及接口,方便系統的可讀性,對於業務的複雜邏輯也顯得更加直觀。

Android源碼中的策略模式實現分析

動畫中的時間插值器

日常的Android開發中經常會用到動畫,Android中最簡單的動畫就是Tween Animation了,當然幀動畫和屬性動畫也挺方便的,但是基本原理都類似,畢竟動畫的本質都是一幀一幀的展現給用戶的,只不要當fps小於60的時候,人眼基本看不出間隔,也就成了所謂的流暢動畫。(注:屬性動畫是3.0以後纔有的,低版本可採用NineOldAndroids來兼容。而動畫的動態效果往往也取決於插值器Interpolator不同,我們只需要對Animation對象設置不同的Interpolator就可以實現不同的效果,這是怎麼實現的呢?

首先要想知道動畫的執行流程,還是得從View入手,因爲Android中主要針對的操作對象還是View,所以我們首先到View中查找,我們找到了View.startAnimation(Animation animation)這個方法。

	public void startAnimation(Animation animation) {
		//初始化動畫開始時間
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
		//對View設置動畫
        setAnimation(animation); 
		//刷新父類緩存
        invalidateParentCaches();
		//刷新View本身及子View
        invalidate(true);
    }

考慮到View一般不會單獨存在,都是存在於某個ViewGroup中,所以google使用動畫繪製的地方選擇了在ViewGroup中的drawChild(Canvas canvas, View child, long drawingTime)方法中進行調用子View的繪製。

	protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

再看下View中的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法中是如何調用使用Animation的

	boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
		//...
		
		//查看是否需要清除動畫信息
		final int flags = parent.mGroupFlags;
        if ((flags & ViewGroup.FLAG_CLEAR_TRANSFORMATION) == ViewGroup.FLAG_CLEAR_TRANSFORMATION) {
            parent.getChildTransformation().clear();
            parent.mGroupFlags &= ~ViewGroup.FLAG_CLEAR_TRANSFORMATION;
        }
	
		//獲取設置的動畫信息
	   	final Animation a = getAnimation();
        if (a != null) {
			//繪製動畫
            more = drawAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } else {
			//...
		}
	}

可以看出在父類調用View的draw方法中,會先判斷是否設置了清除到需要做該表的標記,然後再獲取設置的動畫的信息,如果設置了動畫,就會調用View中的drawAnimation方法,具體如下:

	private boolean drawAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {

		Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
		//判斷動畫是否已經初始化過
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }
		
		//判斷View是否需要進行縮放
		final Transformation t = parent.getChildTransformation();
        boolean more = a.getTransformation(drawingTime, t, 1f);
        if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
            if (parent.mInvalidationTransformation == null) {
                parent.mInvalidationTransformation = new Transformation();
            }
            invalidationTransform = parent.mInvalidationTransformation;
            a.getTransformation(drawingTime, invalidationTransform, 1f);
        } else {
            invalidationTransform = t;
        }

		if (more) {
			//根據具體實現,判斷當前動畫類型是否需要進行調整位置大小,然後刷新不同的區域
            if (!a.willChangeBounds()) {
				//...
 				
			}else{
				//...
			}
		}
		return more;
	}

其中主要的操作是動畫始化、動畫操作、界面刷新。動畫的具體實現是調用了Animation中的getTransformation(long currentTime, Transformation outTransformation,float scale)方法。

	public boolean getTransformation(long currentTime, Transformation outTransformation,
            float scale) {
        mScaleFactor = scale;
        return getTransformation(currentTime, outTransformation);
    }

在上面的方法中主要是獲取縮放係數和調用Animation.getTransformation(long currentTime, Transformation outTransformation)來計算和應用動畫效果。

	Interpolator mInterpolator;  //成員變量
	public boolean getTransformation(long currentTime, Transformation outTransformation) {
			//計算處理當前動畫的時間點...
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
			//後續處理,以此來應用動畫效果...
            applyTransformation(interpolatedTime, outTransformation);
	    return mMore;
    }

很容易發現Android系統中在處理動畫的時候會調用插值器中的getInterpolation(float input)方法來獲取當前的時間點,依次來計算當前變化的情況。這就不得不說到Android中的插值器Interpolator,它的作用是根據時間流逝的百分比來計算出當前屬性值改變的百分比,系統預置的有LinearInterpolator(線性插值器:勻速動畫)、AccelerateDecelerateInterpolator(加速減速插值器:動畫兩頭慢中間快)和DecelerateInterpolator(減速插值器:動畫越來越慢)等,如圖:

url

由於初期比較舊的版本採用的插值器是TimeInterpolator抽象,google採用了多加一層接口繼承來實現兼容也不足爲怪了。很顯然策略模式在這裏作了很好的實現,Interpolator就是處理動畫時間的抽象,LinearInterpolator、CycleInterpolator等插值器就是具體的實現策略。插值器與Animation的關係圖如下:

url

這裏以LinearInterpolator和CycleInterpolator爲例:

  • LinearInterpolator

      public float getInterpolation(float input) {
          return input;
      }
    
  • CycleInterpolator

    	public float getInterpolation(float input) {
          return (float)(Math.sin(2 * mCycles * Math.PI * input));
      }    
    

可以看出LinearInterpolator中計算當前時間的方法是做線性運算,也就是返回input*1,所以動畫會成直線勻速播放出來,而CycleInterpolator是按照正弦運算,所以動畫會正反方向跑一次,其它插值器依次類推。不同的插值器的計算方法都有所差別,用戶設置插值器以實現動畫速率的算法替換。

ListAdapter

在Android中策略模式的其中一個典型應用就是Adapter,在我們平時使用的時候,一般情況下我們可能繼承BaseAdapter,然後實現不同的View返回,getView裏面實現不同的算法。外部使用的時候也可以根據不同的數據源,切換不同的Adapter。

//簡單佈局
mListView.setAdapter(new ArrayAdapter<>(),this,R.layout.item_text),Arrays.asList("one","two","threee"));

//複雜佈局
mListView.setAdapter(new BaseAdapter)(){
    @override
    public int getCount(){return mData.size();}
    
    @override
    public Object getItem(int position){
        return mData.get(position);
    }
    @override
    public long getItemId(int position){return position;}
    
    @override 
    public View getView(int position,View converView,ViewGroup parent){
        converView = LayoutInflater,from(SettingActivity.this).inflate(R.layout.item_sample,parent);
        return converView;
    }
}

ListAdapter源碼分析:

/**
 * Extended {@link Adapter} that is the bridge between a {@link ListView}
 * and the data that backs the list. Frequently that data comes from a Cursor,
 * but that is not
 * required. The ListView can display any data provided that it is wrapped in a
 * ListAdapter.
 */
public interface ListAdapter extends Adapter {
    public boolean areAllItemsEnabled();
    boolean isEnabled(int position);
}

BaseAdapter源碼分析:


/**
 * Common base class of common implementation for an {@link Adapter} that can be
 * used in both {@link ListView} (by implementing the specialized
 * {@link ListAdapter} interface) and {@link Spinner} (by implementing the
 * specialized {@link SpinnerAdapter} interface).
 */
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
    private final DataSetObservable mDataSetObservable = new DataSetObservable();
    private CharSequence[] mAutofillOptions;

    public boolean hasStableIds() {
        return false;
    }
    //……
}
public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
    private final Object mLock = new Object();

    private final LayoutInflater mInflater;

    private final Context mContext;
    //……
}

可以看到 ListAdapter 是一個接口,ArrayAdapter 和 BaseAdapter 是它的一個實現類。對比文章開始給出的 策略模式 UML 圖,可以發現 ListAdapter 就是 strategy 接口,ArrayAdpater 等就是具體的實現類,可以分別實現簡單的佈局和複雜的佈局。而在 ListView 中引用的是 接口 ListAdapter,可以證實這就是一個 策略模式 的使用。

ListView中還涉及到了一個重要的設計模式:適配器模式,後期有時間再深入研究總結。

volley的超時重發重發機制

1.首先定義重試策略接口

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

2.實現具體策略類。在Volley中,該接口有一個默認的實現DefaultRetryPolicy,Volley 默認的重試策略實現類。

主要通過在 retry(…) 函數中判斷重試次數是否達到上限確定是否繼續重試。

public class DefaultRetryPolicy implements RetryPolicy {
   ...
    public static final int DEFAULT_TIMEOUT_MS = 2500;
	private int mCurrentTimeoutMs;
    /** The default number of retries */
    public static final int DEFAULT_MAX_RETRIES = 0;

    /** The default backoff multiplier */
    public static final float DEFAULT_BACKOFF_MULT = 1f;
    
   //代表省略掉的代碼

    @Override
    public int getCurrentTimeout() {
        return mCurrentTimeoutMs;
    }

    @Override
    public int getCurrentRetryCount() {
        return mCurrentRetryCount;
    }

    @Override
    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        if (!hasAttemptRemaining()) {
            throw error;
        }
    }
}

3.設置使用某種策略類。

Request.java 請求類,持有一個抽象策略類RetryPolicy 的引用,最終給客戶端調用

public abstract class Request<T> implements Comparable<Request<T>> {
	 private RetryPolicy mRetryPolicy; //持有策略類的引用
	 
	// ...
	 
	public Request(int method, String url, Response.ErrorListener listener) {
        mMethod = method;
        mUrl = url;
        mIdentifier = createIdentifier(method, url);
        mErrorListener = listener;
        setRetryPolicy(new DefaultRetryPolicy());  //設置策略類

        mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
    }
	//...
	
	 public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
        mRetryPolicy = retryPolicy;
        return this;
    }

	//...
	
	public final int getTimeoutMs() {
        return mRetryPolicy.getCurrentTimeout();
    }
}

策略模式的優點與缺點

策略模式主要用來分離算法,根據相同的行爲抽象來做不同的具體策略實現。很好的實現了開閉原則,也就是定義抽象,注入具體實現,從而達到很好的可擴展性。

優點

  • 使用了組合,使架構更加靈活

  • 富有彈性,可以較好的應對變化(開閉原則)

  • 更好的代碼複用性(相對於繼承)

  • 消除大量的條件語句

缺點

  • 隨着策略的增加,子類也會變得繁多。

  • 選擇何種算法需要客戶端來創建對象,增加了耦合,這裏可以通過與工廠模式結合解決該問題;

策略模式設計原則

  • 找出應用中需要變化的部分,把它們獨立出來,不要和那些不需要變化的代碼混在一起
  • 面向接口編程,而不是面向實現編程
  • 多用組合,少用繼承

總結

策略模式在開發應用場景很多,平時有意識的運用好策略模式,可以很好的提高的程序的可維護性和可擴展性。

參考資料:

1.Android設計模式源碼解析之策略模式

2.Android 中的那些策略模式

3.實際項目運用之Strategy模式(策略模式)

4.何紅輝,關愛民. Android 源碼設計模式解析與實戰[M]. 北京:人民郵電出版社

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