策略模式解析以及在Android中的實際應用

stormWen發表於2019-03-04

概述

在實際專案或者生活中,總會遇到一類問題,比如一個需求,有多種解決方案,又或者比如從上海到北京,有多種方式可以選擇,高鐵,飛機,火車等等,就是實現某一個功能有多種演算法或者策略,我們可以根據環境或者條件的不同選擇不同的演算法或者策略來完成該功能,前面說了從上海到北京,如果是有錢的土豪,可以選擇飛機,如果是一般的家庭,可以選擇高鐵,如果錢不夠,可以選擇火車,可見選擇什麼策略需要根據具體環境決定,上面的例子中,有多少錢就代表了一種環境.策略模式是一系列的演算法定義,並將每一個演算法封裝起來,而且使它們還可以相互替換,策略模式讓演算法獨立於使用它的客戶而獨立變化,可以根據具體的情況增加或者減少策略,解耦程式碼。

適用性

一般情況下,當存在以下情況時使用Strategy模式

● 許多相關的類僅僅是行為有異。“策略”提供了一種用多個行為中的一個行為來配置一個類的方法。即一個系統需要動態地在幾種演算法中選擇一種。

● 需要使用一個演算法的不同變體。例如,你可能會定義一些反映不同的空間或者時間權衡的演算法。當這些變體實現為一個演算法的類層次時 ,可以使用策略模式。

● 演算法使用客戶不應該知道的資料。可使用策略模式以避免暴露覆雜的、與演算法相關的資料結構。

● 一個類定義了多種行為 , 並且這些行為在這個類的操作中以多個條件語句的形式出現。將相關的條件分支移入它們各自的Strategy類中以代替這些條件語句,比如可以替代以前的的if ,elseif語句。

結構

策略模式解析以及在Android中的實際應用

● 抽象策略類(Strategy):定義所有支援的演算法的公共介面,抽象也是可以的,但一般以介面為為主。

● 具體策略類(ConcreteStrategy):以Strategy介面實現某具體演算法,通常在複雜專案中有多個具體的實現。

● 環境類(Context):用一個ConcreteStrategy物件來配置,維護一個對Strategy物件的引用。可定義一個介面來讓Strategy訪問它的資料,一般在環境類可以決定了使用什麼樣的策略。

下面看一個例子:

假設從上海到北京旅遊,有一個旅行的介面:

public interface Travel {
    void onTravel();
}
複製程式碼

然後分別有飛機,高鐵,自駕車3中具體的方式(就是具體的策略)

public class Fly implements Travel {
    @Override
    public void onTravel() {
        System.out.println("飛機出發");
    }
}

public class ChrTravel implements Travel {
    @Override
    public void onTravel() {
        System.out.println("高鐵出發");
    }
}

public class Car implements Travel {
    @Override
    public void onTravel() {
        System.out.println("自駕車出發");
    }
}
複製程式碼

然後定義一個環境類,在這裡通過引數可以決定了使用哪種策略:

public class TravelContext {
    private Travel mTravel;

    //決定了使用哪種策略
    public TravelContext(Travel mTravel) {
        this.mTravel = mTravel;
    }

    public void onTravel() {
        mTravel.onTravel();
    }

}
複製程式碼

下面是執行的程式碼:

public class TravelClient {
    public static void main(String[] args){
        Travel trave;
        trave=new Fly();
        TravelContext travelClient=new TravelContext(trave);
        travelClient.onTravel();

        trave=new ChrTravel();
        TravelContext travelClient1=new TravelContext(trave);
        travelClient1.onTravel();

        trave=new Car();
        TravelContext travelClient2=new TravelContext(trave);
        travelClient2.onTravel();
    }
}
複製程式碼

策略模式解析以及在Android中的實際應用

好了,從上海到北京,有3中具體的方式(策略)來解決這個問題

策略模式優缺點

優點:

● 策略模式提供了管理相關的演算法族的辦法。策略類的等級結構定義了一個演算法或行為族。恰當使用繼承可以把公共的程式碼轉移到父類裡面,從而避免重複的程式碼。

● 策略模式提供了可以替換繼承關係的辦法。繼承可以處理多種演算法或行為。如果不是用策略模式,那麼使用演算法或行為的環境類就可能會有一些子類,每一個子類提供一個不同的演算法或行為。但是,這樣一來演算法或行為的使用者就和演算法或行為本身混在一起。決定使用哪一種演算法或採取哪一種行為的邏輯就和演算法或行為的邏輯混合在一起,從而不可能再獨立演化。繼承使得動態改變演算法或行為變得不可能。

● 使用策略模式可以避免使用多重條件轉移語句。多重轉移語句不易維護,它把採取哪一種演算法或採取哪一種行為的邏輯與演算法或行為的邏輯混合在一起,統統列在一個多重轉移語句裡面,比使用繼承的辦法還要原始和落後。

缺點:

● 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法類。換言之,策略模式只適用於客戶端知道所有的演算法或行為的情況。

● 策略模式造成很多的策略類,每個具體策略類都會產生一個新類。有時候可以通過把依賴於環境的狀態儲存到客戶端裡面,而將策略類設計成可共享的,這樣策略類例項可以被不同客戶端使用。換言之,可以使用享元模式來減少物件的數量,實際上所有的設計模式都有一個共同的特點,就是子類過多,特別是複雜專案的時候,尤其突出,可以考慮進一步的封裝實現,就是在策略模式內部再封裝。

策略模式在Android中原始碼的體現

策略模式在Android中可以說是廣泛應用的,拿我們熟悉的屬性動畫裡面的TimeInterpolator時間插值器來說吧,TimeInterpolator是一個介面,其程式碼如下:

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);
}
複製程式碼

一般我們在程式碼中可以這樣寫:

        ObjectAnimator animator=ObjectAnimator.ofFloat(button,"alpha",1f,0f);
        animator.setInterpolator(new LinearInterpolator());
        animator.setDuration(2000);
        animator.start();
        
        public void setInterpolator(TimeInterpolator value) {
        if (value != null) {
            mInterpolator = value;
        } else {
            mInterpolator = new LinearInterpolator();
        }
    }
        預設是線性變化的
複製程式碼

上面這段設定時間插值器的程式碼就是類似於例子中設定具體哪種交通方式去北京的意思, 目前的話系統中有好幾種已經實現的插值器了,比如LinearInterpolator,AccelerateInterpolator,AccelerateDecelerateInterpolator等等,我們也可以自定義一個新的TimeInterpolator,比如下面簡單的程式碼:

public class CustomInterpolator implements android.view.animation.Interpolator {
    @Override
    public float getInterpolation(float input) {
        return input*2;
    }
}
可以看到新的值是原來的2倍,大家執行的時候就可以看到效果了
複製程式碼

除了屬性動畫,策略模式在Android其他方面也是很多利用的,比如support包,其實也是利用了策略模式的,有空的同學可以去研究一下,這裡就不再詳述了。

策略模式在Android實際專案中的應用

除了Android原始碼廣泛利用了策略模式以外,實際的專案中也會經常用到這個策略模式的,下面舉一個實際的例子來說一下,以圖片載入例子來說,眾所周知,現在的框架很多,比如Picasso,Glide,Fresco,等等,特別是新的框架出來之後,很多人都願意在專案中嘗試使用一下,但如果為了使用新的框架而重新寫一套程式碼就很不划算了,這個時候就可以利用策略模式了,首先定義一個載入圖片的介面:

public interface ImageLoaderProvider {
    void loadImage(Context ctx, ImageConfig img);
}
ImageConfig是載入的一些配置,比如有些在wifi下才載入,或者載入大圖,中圖,縮圖等等,簡單的程式碼如下:
public class ImageConfig {
    private int type;
    private String url;
    private int placeHolder;
    private ImageView imageView;
    private int strategy;//載入策略,是否在wifi下才載入或者其他的策略,比如載入大圖,中圖,或者是小圖等等

    private ImageConfig(ImageConfig imageConfig) {
        this.type = imageConfig.type;
        this.url = imageConfig.url;
        this.placeHolder = imageConfig.placeHolder;
        this.imageView = imageConfig.imageView;
        this.strategy = imageConfig.strategy;
    }

    private ImageConfig(){

    }

    public int getType() {
        return type;
    }

    public String getUrl() {
        return url;
    }

    public int getPlaceHolder() {
        return placeHolder;
    }

    public ImageView getImageView() {
        return imageView;
    }

    public int getStrategy() {
        return strategy;
    }


    public static class Builder {
        private ImageConfig imageConfig;

        public Builder() {
            this.imageConfig = new ImageConfig();
        }
        public Builder type(int type) {
            imageConfig.type = type;
            return this;
        }

        public Builder url(String url) {
            imageConfig.url = url;
            return this;
        }

        public Builder placeHolder(int placeHolder) {
            imageConfig.placeHolder = placeHolder;
            return this;
        }

        public Builder imgView(ImageView imgView) {
            imageConfig.imageView = imgView;
            return this;
        }

        public Builder strategy(int strategy) {
            imageConfig.strategy = strategy;
            return this;
        }

        public ImageConfig build(){
            return new ImageConfig(imageConfig);
        }

    }
}
可以看到目前僅僅是判斷是否在wifi下才載入,當然實際情況可以適當擴充套件,但原理不變。
複製程式碼

假如第一個是 Picasso框架的,下面寫一個實現的類,程式碼如下:

public class PicassoImageLoaderProvider implements ImageLoaderProvider {
    private ImageView imageView;

    @Override
    public void loadImage(Context ctx, ImageConfig config) {

        // 一般實現,或者根據具體的一些另外的策略載入,比如是否在wifi下自動載入等,根據業務具體決定
        Picasso
                .with(ctx)
                .load(config.getUrl())
                .tag("Picasso") //引數為 Object
                .placeholder(config.getPlaceHolder())
                .into(target);
        imageView = config.getImageView();
    }

    private Target target = new Target() {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            //載入成功後會得到一個bitmap,可以自定義操作
            imageView.setImageBitmap(bitmap);
        }

        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
            // 載入失敗進行相應處理
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {

        }
    };
}
複製程式碼

可以看到實現了載入介面,這裡可以根據實際的業務擴充套件方法,第二個是Glide載入實現:

public class GlideImageLoaderProvider implements ImageLoaderProvider {
    @Override
    public void loadImage(Context ctx, ImageConfig config) {
        boolean flag = SettingUtils.getOnlyWifiLoadImg();
        //如果不是在wifi下載入圖片,直接載入
        if (!flag) {
            loadNormal(ctx, config);
            return;
        }
        int strategy = config.getStrategy();
        //這裡1表示的是wifi載入
        if (strategy == 1) {
            int netType = NetUtils.getNetWorkType(ctx);
            //如果是在wifi下才載入圖片,並且當前網路是wifi,直接載入
            if (netType == NetUtils.NETWORKTYPE_WIFI) {
                loadNormal(ctx, config);
            } else {
                //如果是在wifi下才載入圖片,並且當前網路不是wifi,載入快取
                loadCache(ctx, config);
            }
        } else {
            //如果不是在wifi下才載入圖片
            loadNormal(ctx, config);
        }

    }

    /**
     * 載入圖片,這裡需要注意的是預設載入的是全尺寸的,這樣的話很容易導致OOM的發生,需要進行適當的壓縮
     */
    private void loadNormal(Context ctx, final ImageConfig config) {
        Glide.with(ctx)
                .load(config.getUrl())
                .override(ProjectConfig.IMAGE_WIDTH, ProjectConfig.IMAGE_HEIGHT)
                .placeholder(config.getPlaceHolder())
                .into(new SimpleTarget<GlideDrawable>() {

                    @Override
                    public void onLoadFailed(Exception e, Drawable errorDrawable) {
                        config.getImageView().setImageResource(R.drawable.ic_icon_share_sp);
                    }

                    @Override
                    public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
                        config.getImageView().setImageDrawable(resource);
                    }
                });
    }


    /**
     * 載入快取圖片
     */
    private void loadCache(Context ctx, ImageConfig config) {
        Glide.with(ctx).using(new StreamModelLoader<String>() {
            @Override
            public DataFetcher<InputStream> getResourceFetcher(final String model, int i, int i1) {
                return new DataFetcher<InputStream>() {
                    @Override
                    public InputStream loadData(Priority priority) throws Exception {
                        throw new IOException();
                    }


                    @Override
                    public void cleanup() {

                    }

                    @Override
                    public String getId() {
                        return model;
                    }


                    @Override
                    public void cancel() {

                    }
                };
            }
        })
                .load(config.getUrl())
                .override(ProjectConfig.IMAGE_WIDTH, ProjectConfig.IMAGE_HEIGHT)
                .placeholder(config.getPlaceHolder())
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .into(config.getImageView());
    }
}
複製程式碼

這個是我專案中用到的,適當擴充套件了一些需要的方法,第三個是Fresco載入實現:

public class FrescoImageLoaderProvider implements ImageLoaderProvider {

    private static volatile FrescoImageLoaderProvider sInstance;

    public static FrescoImageLoaderProvider getInstance() {
        if (sInstance == null) {
            synchronized (FrescoImageLoaderProvider.class) {
                if (sInstance == null) {
                    sInstance = new FrescoImageLoaderProvider();
                }
            }
        }
        return sInstance;
    }

    @Override
    public void loadImage(Context ctx, ImageConfig config) {

        //這裡可以根據一些其他策略進行載入,比如wifi環境下載入或者不載入之類,具體業務決定

        DraweeController controller = Fresco.newDraweeControllerBuilder()
                .setUri(config.getUrl())
                .setAutoPlayAnimations(true)
                .setControllerListener(listener)
                .build();
        SimpleDraweeView simpleDraweeView = (SimpleDraweeView) config.getImageView();
        simpleDraweeView.setController(controller);
    }

    //圖片載入監聽
    ControllerListener listener = new BaseControllerListener() {

        /**
         * 當圖片載入成功時會執行的方法
         * @param id
         * @param imageInfo
         * @param animatable
         */
        @Override
        public void onFinalImageSet(String id, Object imageInfo, Animatable animatable) {
            super.onFinalImageSet(id, imageInfo, animatable);
        }


        /**
         * 圖片載入失敗時呼叫的方法
         * @param id
         * @param throwable
         */
        @Override
        public void onFailure(String id, Throwable throwable) {
            super.onFailure(id, throwable);
        }


        /**
         *  如果載入的圖片使用漸進式,這個方法將會被回撥
         */
        @Override
        public void onIntermediateImageFailed(String id, Throwable throwable) {
            super.onIntermediateImageFailed(id, throwable);
        }
    };
}
複製程式碼

可以看到都實現了載入的介面,那麼下面寫一個環境類,程式碼如下:

public class ImageLoaderContext {
    private ImageLoaderProvider mImageLoaderProvider;

    public void setImageLoaderProvider(ImageLoaderProvider mImageLoaderProvider) {
        this.mImageLoaderProvider = mImageLoaderProvider;
    }

    public void loadImage(Context ctx, ImageConfig img) {
        mImageLoaderProvider.loadImage(ctx, img);
    }

    //一些簡單的配置
    public void loadImg(Context context, ImageView imageView, String url) {
        ImageConfig config = new ImageConfig.Builder()
                .imgView(imageView)
                .url(url)
                .strategy(1)//比如說預設是大圖
                .placeHolder(R.drawable.ic_back_icon_sp)
                .build();
        loadImage(context, config);
    }
}
複製程式碼

這裡決定了最終需要使用哪種載入框架,如果某天需要換另一種,只需要實現了介面並且在這裡替換掉就好,而ImageConfig又可以進行單獨的配置和擴充套件,在一些更加複雜的場合,會對這個環境類和具體的實現類進行進一步的封裝,但都堅持一個類一個職責的原則,跟前面的ImageConfig是一樣的,可以配置更多更復雜的選項。

實際的執行結果就不演示了,實際上會發現,設計模式這種玩意,基本都是來自生活中的場景,個人認為對於設計模式,一定要理解,理解了寫程式碼才有思路,今天的文章就寫到這裡了,感覺大家閱讀。

相關文章