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. 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容