天问

  • Blog
  • 热修复与动态加载
  • python爬虫
  • 专栏
    • 热修复与动态加载
    • NDK开发
    • 设计模式与架构
    • React Native
  • 好文转载
    • 计算机技术
    • 投资理财
    • 摄影与拍照技巧
  • 投稿
  • 关于本站及隐私政策
  • Github
StevenQ
为今天工作,为明天投资,为未来孵化一些东西!
  1. 首页
  2. 计算机技术
  3. 设计模式
  4. 正文

【设计模式之】策略模式

2018年12月2日 396点热度 1人点赞 0条评论

策略模式也是使用非常广泛的一种设计模式,因为程序往往是用来解决实际问题的,而在实际生活中很多问题往往不止一种解决方案。也就是说解决该问题可以使用不同的算法或者在不同情形下需要使用不同算法。这种场景我们就可以将不同算法抽象为不同的策略,在程序运行时根据实际情况选择不同的策略。

应用场景

  • 同一个场景/问题有多种不同解决方案。传统的做法就是使用if-else或者switch-case根据不同参数选择不同解决方案。但这种做法代码耦合度高,不易于日后的扩展。一旦随着项目推移,if-else或者switch-case部分很可能越来越臃肿。
  • 某些功能的具体实现可能需要随着项目推移或实际场景的不同而被另外一种实现替代

这2种场景就适合使用策略模式:定义一个抽象的策略类,然后将这些方案的具体不同实现分离到每一个不同的策略类中。而调用端只需要持有这个抽象策略类。这样能够做到这些策略类随时相互替换。

对于第一种场景:举个生活中的实际例子大家就很容易明白了,假设你的女朋友住在另外一个小区,你去她那玩可以选择坐公交或者坐地铁。而公交和地铁的计费规则是不一样的。因此你写了一段程序来计算不同出行工具的成本。(注:该示例引用自安卓源码设计模式解析与实战这本书,个人觉得这个例子很贴切)

public class PriceCalculator {
    //公交车类型
    private static final int BUS = 1;
    //地铁类型
    private static final int SUBWAY = 2;

    public static void main(String[] args) {
        PriceCalculator calculator = new PriceCalculator();
        System.out.println("坐 16 公里的公交车票价为:" + calculator.calculatePrice(16, BUS));
        System.out.println("坐 16 公里的地铁票价为:" + calculator.calculatePrice(16, SVBHAY));
    }

    private int busPrice(int km) {
        //超过十公里的总距离
        int extraTotal = km - 10;
        //超过的距离是5公里的倍数”
        int extraFactor = extraTotal / 5;
        //超过的距离对5公里取余
        int fraction = extraTotal % 5;
        //价格计算
        int price = 1 + extraFactor;
        return fraction > 0 ? ++price : price;
    }

    private int subwayPrice(int km) {
        if (km <= 6) {
            return 3;
        } else if (km > 6 && km < 12) {
            return 4;
        } else if (km > 12 && km < 22) {
            return 5;
        } else if (km > 22 && km < 32) {
            return 6;
        }
        return 7;
    }

    int calculatePrice(int km, int type) {
        if (type == BUS) {
            return busPrice(km);
        } else if (type == SUBWAY) {
            return subwayPrice(km);
        }
        return 0;
    }

}

很明显这就是一个问题有多种解决方案的场景。上面的示例就是传统的做法,也是不使用设计模式的做法,就是通过if-else的形式来判断使用哪种计算方式。然后调用相应的函数(也即算法)。但是互联网软件开发一个很重要的特点就是需求变更快,如果随着项目推移,需要新增一种计价策略。比如后来你考虑坐出租车去你女朋友那。那么需要在PriceCaloilator中增加一个方法来计算出租车出行的价格,并且在calculatePrice(int km, int type)函数中增加一个判断。像下面这样:

public class PriceCalculator {
    //公交车类型
    private static final int BUS = 1;
    //地铁类型
    private static final int SUBWAY = 2;
    //出租车类型
    private static final int TAXI = 3;

    int calculatePrice(int km, int type) {
        if (type == BUS) {
            return busPrice(km);
        } else if (type == SUBWAY) {
            return subwayPrice(km);
        } else if (type == TAXI) {
            return taxiPrice(km);
        }
        return 0;
    }
   //省略具体的计价函数的实现...
}

从上面的更改可以看到,每增加一种计价方式,不仅需要新增计价函数本身,还需要修改calculatePrice的内部实现,新增if-else分支。不符合开闭原则,另外随着计价方式越来越多,PriceCalculator这个类代码必然越来越臃肿。使用策略模式就能够很轻易的解决上面的缺点。我们看下如何使用策略模式来重构上面的代码。

首先定义一个抽象的策略接口用来计算价格

public interface CalculateStrategy {
/**
*   按距离来计算价格
*	@param km 公里
*	@return返回价格 */
int calculatePrice(int km);
}

然后对于每一种具体的计价方式实现上面的策略接口。

public class BusStrategy implements CalculateStrategy {
    public int calculatePrice(int km) {
        //省略
    }
}
public class SubwayStrategy implements CalculateStrategy {
    public int calculatePrice(int km) {
        //省略
    }
}

看下调用端的代码:

public class TranficCalculator {
    CalculateStrategy mStrategy;
    public void setStrategy(CalculateStrategy mStrategy) {
        this.mStrategy = mStrategy;
    }
    public int calculatePrice(int km) {
        return mStrategy.calculatePrice(km);
    }
    
    
    public static void main(String[] args)(
        TranficCalculator calculator = new TranficCalculator();
        //设置计算策略
        calculator.setStrategy(new BusStrategy());
        //计算价格
        System.out.println("公交车乘 16 公里的价格:+ calculator.calculatePrice (16));
    }
}

可以看到对于调用端PriceCalculator而言,只需要持有一个抽象策略接口CalculateStrategy,同时提供一个策略注入函数setStrategy即可实现根据不同的策略达到计价算法calculatePrice的替换。

如果我们要新增出租车的计价方式,那么只需要定义一个出租车计价策略实现抽象策略接口。然后将出租车计价策略的实例通过策略注入接口注入给TranficCalculator即可。如下所示:

       // 1 定义出租车计价策略
        public class TaxiStrategy implements CalculateStrategy {
            public int calculatePrice(int km) {
                //省略具体实现
            }
        }
       // 2 将出租车计价策略注入到TranficCalculator中
        TranficCalculator calculator = new TranficCalculator();
        //注入计算策略
        calculator.setStrategy(new BusStrategy());
        //计算价格
        System.out.println("出租车乘 16 公里的价格:+ calculator.calculatePrice (16));

这样我们完全不需要修改TranficCalculator类的代码即可实现策略的替换。也就是可扩展性强,易于升级维护。从上面示例其实可以看到策略模式本质上也是面向接口编程。通过建立策略接口,将不同的算法实现构建成一个具体的策略类,操作策略类的上下文环境Contex类(在上例中是TranficCalculator类)只需要持有策略接口和提供策略注入函数,即可通过设置不同的策略实现算法的替换。

小结下使用策略模式的关键:

  1. 建立抽象接口,在接口中定义算法接口
  2. 将不同的算法实现构建成不同的策略类
  3. 操作策略类的上下文环境Contex类需要持有策略接口,同时提供策略注入函数
  4. 客户端调用时通过策略设置接口设置不同策略达到算法替换目的。也就是取代传统做法的if-else.

策略模式知名项目分析

谷歌的Volley网络框架的超时重试机制中就使用到了策略模式。谷歌默认网络请求超时是2.5s,重试次数为1。如果我们要修改默认请求超时为10s。重试次数为3次。那么我们的代码会是如下这样:

StringRequest stringRequest = new StringRequest(Request.Method.GET, "https://www.baidu.com", new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Log.d(TAG, response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                Log.e(TAG, volleyError.getMessage());
            }
        });
        //修改默认超时为10s,重试次数为3次
        stringRequest.setRetryPolicy(new DefaultRetryPolicy(
                1000 * 10, 3, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
        RequestQueue requestQueue = Volley.newRequestQueue(context);
        requestQueue.add(stringRequest);

这里的setRetryPolicy方法就是设置重试策略,我们使用了Volley内置的默认的重试策略DefaultRetryPolicy,这里的DefaultRetryPolicy就使用了策略模式。看下其代码实现:

public interface RetryPolicy {

    /** Returns the current timeout (used for logging). */
    int getCurrentTimeout();

    /** Returns the current retry count (used for logging). */
    int getCurrentRetryCount();

    /**
     * Prepares for the next retry by applying a backoff to the timeout.
     *
     * @param error The error code of the last attempt.
     * @throws VolleyError In the event that the retry could not be performed (for example if we ran
     *     out of attempts), the passed in error is thrown.
     */
    void retry(VolleyError error) throws VolleyError;
}
** Default retry policy for requests. */
public class DefaultRetryPolicy implements RetryPolicy {
    /** The current timeout in milliseconds. */
    private int mCurrentTimeoutMs;

    /** The current retry count. */
    private int mCurrentRetryCount;

    /** The maximum number of attempts. */
    private final int mMaxNumRetries;

    /** The backoff multiplier for the policy. */
    private final float mBackoffMultiplier;

    /** The default socket timeout in milliseconds */
    public static final int DEFAULT_TIMEOUT_MS = 2500;

    /** The default number of retries */
    public static final int DEFAULT_MAX_RETRIES = 1;

    /** The default backoff multiplier */
    public static final float DEFAULT_BACKOFF_MULT = 1f;

    /** Constructs a new retry policy using the default timeouts. */
    public DefaultRetryPolicy() {
        this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
    }

    /**
     * Constructs a new retry policy.
     *
     * @param initialTimeoutMs The initial timeout for the policy.
     * @param maxNumRetries The maximum number of retries.
     * @param backoffMultiplier Backoff multiplier for the policy.
     */
    public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
        mCurrentTimeoutMs = initialTimeoutMs;
        mMaxNumRetries = maxNumRetries;
        mBackoffMultiplier = backoffMultiplier;
    }

    /** Returns the current timeout. */
    @Override
    public int getCurrentTimeout() {
        return mCurrentTimeoutMs;
    }

    /** Returns the current retry count. */
    @Override
    public int getCurrentRetryCount() {
        return mCurrentRetryCount;
    }

    /** Returns the backoff multiplier for the policy. */
    public float getBackoffMultiplier() {
        return mBackoffMultiplier;
    }

    /**
     * Prepares for the next retry by applying a backoff to the timeout.
     *
     * @param error The error code of the last attempt.
     */
    @Override
    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        mCurrentTimeoutMs += (int) (mCurrentTimeoutMs * mBackoffMultiplier);
        if (!hasAttemptRemaining()) {
            throw error;
        }
    }

    /** Returns true if this policy has attempts remaining, false otherwise. */
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }
}

DefaultRetryPolicy类继承自策略接口RetryPolicy,然后实现了该接口中的几个方法,相当于是某一个具体策略的实现类。然后看下策略类的持有类Request类:

public abstract class Request<T> implements Comparable<Request<T>> {
    // ...省略其他不相关代码...
    /**
     * The retry policy for this request.
     */
    private RetryPolicy mRetryPolicy;

    /**
     * Sets the retry policy for this request.
     *
     * @return This Request object to allow for chaining.
     */
    public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
        mRetryPolicy = retryPolicy;
        return this;
    }
    // ...省略其他不相关代码...
}

可以看到Request关联了策略接口RetryPolicy,同时提供了一个策略注入函数setRetryPolicy。符合我们前面分析的策略模式。那么这里谷歌工程师为何要用策略模式呢?

很显然超时重置和网络相关,而不同场景对超时的要求是不一样的,某些场景对超时很敏感,那么超时时间就需要设置小一些,反之可以设置大一些,另外超时重置的具体实现也可能不一样。也就是说这个超时重置功能属于需要高度定制的模块。需要让算法的具体实现独立于客户端的使用,从而轻易做到相互替换。这实际上就是前面应用场景中提到的第2个场景。这种场景和第一种场景的区别在于第一种场景是某一问题已经存在多种不同的算法实现,那么相对而言我们比较容易的知道使用策略模式能够使得代码的扩展性更强,但第2种场景比较隐蔽,使用策略模式是为了让算法的实现和客户端的调用相分离,从而使得算法的具体实现能够轻易的被另外一种实现替换。 但这2中场景的本质都是一样的:面向接口编程,使用类Context关联抽象而不是具体实现类,同时提供依赖注入接口,通过依赖注入接口设置不同的具体实现类来达到新增或替换原算法实现的目的。

使用策略模式的好处

从前面讲解的示例可以很容易的知道策略模式能够避免传统的if-else模式选择不同算法可能造成的代码臃肿,另外最大的好处就是易于扩展和维护,能够在不修改操作策略类的上下文环境Contex类的前提下做到新增算法或行为(也就是策略)。亦或是替换原算法为另外一种算法。

 

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 设计模式
最后更新:2018年12月2日

天问

追梦追自由,穷则独善其身,达则兼济天下

打赏 点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2022 天问 ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang