浅入深出设计模式篇之策略模式

引言

在我们日常开发中,我们或多或少会有这样的抱怨:

  1. 我擦, 为什么要换框架? 换这么多地方, 肯定会有大量的问题吧
  2. 唉,领导怎么想的,为什么又要改需求,万一其他地方出一堆问题怎么办?
  3. 哎呀我去,我改的不是这儿啊,为啥这个地方还报错了呢?

如果你有上面的抱怨,说明你该学习设计模式了少年。

上面很多问题,其实总结原因如下:

  1. 代码冗余,混乱,理不清头绪
  2. 耦合重, 出处相关
  3. 扩展性差,扩展起来很麻烦
  4. 可重用性低,到处存在相似的代码

设计模式就是为了解决这些问题。如果你也遇到了上述问题其一或者若干,你还觉得设计模式和日常开发无关吗?

今天我们讲的设计模式,是策略模式。掌握了策略模式,想要切换框架? So Easy !!!

那么通过引入场景,我们一步步的来看,策略模式到底好在哪?为什么要使用设计模式?

开始啦!

近几年听说流行一种很好用的框架, 简单易用,高效高能,听说是叫Glide。 这是一种图片加载框架,好处多多; 我相信这种听说在多年前就有相似的场景,那就是UniverseImageLoader。

如果我们还停留在初级阶段,工作经验不足,那么我们是这么写的

 // 在AdapterA中,使用Glide加载图片
    Glide.with(this).load(url).into(iv);

    // 在ActivityB中,使用Glide加载图片
    Glide.with(this).load(url).into(iv);

    // 在Fragment中,使用Glide加载图片
    Glide.with(this).load(url).into(iv);
    
    // Glide好用极了, 加载图片真快,而且内存管理好!!!

有一天,老大来说,测试说了,图片地址不能用的时候我们不能显示空白啊,这样太丑了,加个默认图片吧。我们想:这还不容易, Ctrl + H 搜一下Glide, 每个地方改一下呗! 然而改着改着,我们忽然发现,总共有1000处使用了Glide, 我们真是欲哭无泪。这时有一定开发经验的小王来了,用鄙视的眼光看着我们。“你这个傻鸟”,你写个GlideUtils, 直接调用GlideUtils不就行了。无论老大让怎么改,我只改GlideUtils就行了。

我们如获至宝,重新改造了代码

public class GlideUtils {
    
    public static void display(Activity context, String url, ImageView target) {
        Glide.with(context).load(url).into(target);
    }
}

我们偷偷开心, 嘿嘿,这下再改的话,就不用加班了吧!后来我们需要添加一些个性化设置,我们在方法中需要添加一些变量处理,但是我们不想把所有的变量都设为静态的,于是就有了

public class GlideUtils {
    private static GlideUtils utils;

    private GlideUtils() {
        // 初始化
    }

    public static getUtils() {
        if(utils == null) {
            utils = new GlideUtils();
        }
        return utils;
    }


    public void display(Activity context, String url, ImageView target) {
        Glide.with(context).load(url).into(target);
    }
}

这样我们在依赖外部资源的前提下,性能效率都提高了,还能做到按需初始化。我们觉得自己好棒, 成长真的很快。普及一下, 全静态方法实现的工具类, 和单例模式的工具类,区别在哪?

1.静态方法工具类是包含以下特征的静态方法的集合,它的静态方法是独立的,无任何外部依赖的。
1.单例模式工具类是系统的, 依赖外部资源或初始化的。

我们需要初始化Glide配置,也需要依赖一定的外部资源,因此改为单例模式。 但是问题又来了,好多小伙伴都在改这个类,加入了好多不同的甚至重复的方法, 我们甚是苦恼, 最终我们决定通过接口来约束功能的实现。

public class GlideUtils implements IGlider{
    private static GlideUtils utils;

    private GlideUtils() {
        // 初始化
    }

    public static getUtils() {
        if(utils == null) {
            utils = new GlideUtils();
        }
        return utils;
    }

    @Override
    public void display(Activity context, String url, ImageView target) {
        Glide.with(context).load(url).into(target);
    }
}

public interface IGlider {

    void display(Activity context, String url, ImageView target);
}

我们觉得很完美, 就像欣赏艺术品一样看着这段代码,内心暗喜。这时领导来了,说小明啊, 这个最近出了新的图片加载框架,那是相当牛逼啊,要不你改下吧。 这时你有点小得意了,还好设计的好,改一个类就好了。打开编译器,三下五除二改好了,你觉得So Easy, 老子真乃神人也。但是一运行,不对啊,怎么好多地方都有问题。原来的代码都删除了,现在的也不行,如果是紧急状态,怎么办?

接下来就是正题了,策略模式。

策略模式是什么?

策略模式是指有一定行动内容的相对稳定的策略名称。
优点:
1、 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码转移到父类里面,从而避免重复的代码。
2、 策略模式提供了可以替换继承关系的办法。继承可以处理多种算法或行为。如果不是用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。但是,这样一来算法或行为的使用者就和算法或行为本身混在一起。决定使用哪一种算法或采取哪一种行为的逻辑就和算法或行为的逻辑混合在一起,从而不可能再独立演化。继承使得动态改变算法或行为变得不可能。
3、 使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。
缺点:
1、客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
2、 策略模式造成很多的策略类,每个具体策略类都会产生一个新类。有时候可以通过把依赖于环境的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。

以上是百度百科的定义。 那么什么是策略模式? 相信大家都看过历史,有一个场景大家也都熟悉。谋臣XX向主公献计,往往都会提供多个计谋, 让主公根据自己的决定选择一个,并言明厉害,策略A乃上上之策, B乃上策, C乃下策。这就是策略。什么是策略模式呢? 我们从三要素来分析:

  1. 策略的目的是什么? 联合孙权,抵抗曹操。在程序中体现为接口。策略再多,但是实现的目的是一样的

  2. 继承目的的不同的策略,接口规范了策略的目的, 策略丰富了目的的达成方式。如投降曹操, 火烧赤壁, 蛮力抵抗

  3. 呈现策略并使用, 主公想要什么计策, 那么谋士要献上什么策略,主公来使用

我们已找对象为例, 进行说明。

  1. 我们的目的是什么? 找到一个合适的对象。定义一个接口,用来限定策略的执行目标。
/**
 * 不同策略所需要达到的最终目标
 * @author qichunjie 2018/5/28
 */

public interface IStrategy {
    /**
     * 找媳妇
     */
    public void findAWife();
}
  1. 目标已确定,那么我们有什么策略呢? 1) 自由恋爱 2) 相亲 3) 说媒 。 那么我们有了三个IStrategy的实现类
/**
 * @author qichunjie 2018/5/28
 */

public class LianAiStrategy implements IStrategy {

    @Override
    public void findAWife() {
        System.out.println("通过自由恋爱的方式,找到了媳妇儿");
    }
}

/**
 * @author qichunjie 2018/5/28
 */

public class ShuoMeiStrategy implements IStrategy {

    @Override
    public void findAWife() {
        System.out.println("通过说媒的方式,找到了媳妇儿");
    }
}

/**
 * 相亲策略
 *
 * @author qichunjie 2018/5/28
 */

public class XiangQinStrategy implements IStrategy {

    @Override
    public void findAWife() {
        System.out.println("通过相亲的方式,找到一个媳妇儿");
    }
}

有人说你可以自由恋爱啊(我何尝不想),有人说相亲才是靠谱的,有人又说了当然是说媒啊,成功率高啊。但是作为当事人,我要怎么选呢?当然是采纳一个,然后去执行啦

/**
 * @author qichunjie 2018/5/28
 */

public class Strategy implements IStrategy {

    private IStrategy iStrategy;

    public Strategy(IStrategy iStrategy) {
        this.iStrategy = iStrategy;
    }

    @Override
    public void findAWife() {
        if (iStrategy != null) {
            iStrategy.findAWife();
        }
    }

}

我们采纳一个策略(传入一个IStrategy), 然后执行(使用传入的策略),剩下的就看结果了。当然,在开发中,这些策略必须是正确执行的。 最后,我们要采用XX提供的策略去找对象啦,结果如何,请看下回分解!

/**
 * @author qichunjie 2018/5/28
 */

public class Main {

    /**
     * 策略模式的测试入口
     */

    public static void main(String[] args) {

        Strategy strategy = new Strategy(new XiangQinStrategy());
        strategy.findAWife();

    }
}

这么分解开来,其实很简单。有人就说了,你这个这么麻烦,为什么使用它?Ok,我们来分析一下它的好处以及解决的问题。

  1. 使用接口来定义一个统一规范,实现指定的目的。(代码再也不会那么混乱了)

2.不同的策略类实现单一的策略,单一职责原则。好处是只能单一,可复用性好(越单一的代码,复用率越高)

3.如果添加新策略,我们直接添加一个扩展类继承IStrategy, 然后传入即可。从来不需要动其他策略的任何东西,扩展性好

  1. 删除一个策略,修改一个策略,不会影响其他策略。如上GlideUtils你很可能会动到其他代码却不知道。符合开闭原则。

这里提到了单一职责原则以及开闭原则,简单的说一下。 当你的代码糅合了多种混合而成,那么它几乎是不可能复用的。如果把它拆分为单独的功能块,那么它的复用率将大大提高; 而且拆分越细, 复用率越高。举个例子,设置View宽高为屏幕比例。我们要做的是

  1. 先获取DisplayMetrics
  2. 获取屏幕宽高
  3. 设置View的尺寸

如果写在一个方法中,我们可能只能设置View的尺寸,并且局限于View的布局,也就是LinearLayout.LayoutParams或者RelativeLayout.LayoutParams。 如果拆分为获取屏幕的尺寸和设置View尺寸两个方法。 那么屏幕尺寸的获取,也是可以复用的。如果按上面1,2,3拆分为3个方法, 那么获取屏幕密度我们也可以使用1方法。所以,拆分的粒度越小,可复用性越好。

单一职责的好处二是,拆分后的功能是通过方法组合的方式来实现,如果足够规范,我们一眼即可以看出这个方法实现的是什么功能。拆分之前, 我们必须阅读完整个代码后,才会知道它是做什么的。

再说开闭原则, 对扩展开发,对修改关闭。为什么呢? 因为代码的修改是具有一定危险性的,如果不同的策略在一个类中,在修改一项策略时,很可能会波及其他策略,而我们自己还不知道。因为对修改保持最小的域来确保我们代码的维护性, 在对修改关闭时, 同时还要满足我们的修改需求,因此改为通过扩展的形式来实现代码的修改。保持修改代码的域最小化。这样的代码维护性,安全性会是最高的。

如上示例的策略,我们要修改相亲策略,那么只修改XiangQinStragy即可,毫不影响其他策略。我们要新增策略,扩展一个策略类,也不需要修改Strategy类。这使得安全性提高了很多。

本文中,我们可以得到一些关于架构设计的点:

  1. 为什么使用接口以及如何使用接口?

  2. 静态工具类和单例工具类的区别是什么?你会用了吗?

  3. 单一职责提高复用率, 减少代码修改域

  4. 开闭原则使你尽可能小的减少你的失误,从而影响其他代码

如果你还不理解上面的4个问题,建议带着问题回去阅读一遍本篇文章。希望本篇文章可以使你提供开发效率, 减少你的加班时间。

根据策略模式,我们来说明一下如何一行代码更新你的框架。

以图片加载框架为例, 框架的更新很快,而且功能也越来越强大,性能越来越好。 因此框架的切换是必要的,那么如何做到切换框架时,我们付出的代价最小呢? 策略模式给了你答案。

  1. 无论切换什么框架,我们要使用的功能是确定的。首先,我们来定义一个规范接口。比如我们需要加载图片,从缓存加载图片, 加载动图
/**
 * @author qichunjie 2018/6/4
 */

public interface ImageLoader {

    void display(Context context, String url, ImageView targetView);

    void display(Context context, String url, ImageView targetView, boolean cache);

    void loadGif(Context context, String url, ImageView targetView);
}
  1. 实现一个框架策略, 以Glide为例(只剖析思想,具体实现请百度Glide官网)
/**
 * @author qichunjie 2018/6/4
 */

public class GlideImageLoader implements ImageLoader {
    
    @Override
    public void display(Context context, String url, ImageView targetView) {
        // 使用Glide实现display图片加载
    }

    @Override
    public void display(Context context, String url, ImageView targetView, boolean cache) {
        // 使用Glide实现从缓存display图片加载
    }

    @Override
    public void loadGif(Context context, String url, ImageView targetView) {
        // 使用Glide实现加载动图
    }
}
  1. 我们需要一个策略加载类(开闭原则,通过扩展来替代该类代码的修改)
/**
 * @author qichunjie 2018/6/4
 */

public class ImageLoadStratergy implements ImageLoader {

    public ImageLoader imageLoader;

    public ImageLoadStratergy(ImageLoader imageLoader) {
        this.imageLoader = imageLoader;
    }


    @Override
    public void display(Context context, String url, ImageView targetView) {
        if(imageLoader != null) {
            imageLoader.display(context, url, targetView);
        }
    }

    @Override
    public void display(Context context, String url, ImageView targetView, boolean cache) {
        if(imageLoader != null) {
            imageLoader.display(context, url, targetView, cache);
        }
    }

    @Override
    public void loadGif(Context context, String url, ImageView targetView) {
        if(imageLoader != null) {
            imageLoader.loadGif(context, url, targetView);
        }
    }
}

4.为了便于全局使用,我们替换为单例模式

/**
 * @author qichunjie 2018/6/4
 */

public class ImageLoadStratergy implements ImageLoader {

    private static ImageLoadStratergy stratergy;
    private ImageLoader imageLoader;

    private ImageLoadStratergy() {
    }

    public void setImageLoader(ImageLoader imageLoader) {
        this.imageLoader = imageLoader;
    }

    public static ImageLoadStratergy getLoader() {
        if (stratergy == null) {
            synchronized (ImageLoadStratergy.class) {
                if (stratergy == null) {
                    stratergy = new ImageLoadStratergy();
                }
            }
        }
        return stratergy;
    }

    @Override
    public void display(Context context, String url, ImageView targetView) {
        if (imageLoader != null) {
            imageLoader.display(context, url, targetView);
        } else {
            throw new NullPointerException("imageLoader is null, setImageLoader first");
        }
    }

    @Override
    public void display(Context context, String url, ImageView targetView, boolean cache) {
        if (imageLoader != null) {
            imageLoader.display(context, url, targetView, cache);
        } else {
            throw new NullPointerException("imageLoader is null, setImageLoader first");
        }
    }

    @Override
    public void loadGif(Context context, String url, ImageView targetView) {
        if (imageLoader != null) {
            imageLoader.loadGif(context, url, targetView);
        } else {
            throw new NullPointerException("imageLoader is null, setImageLoader first");
        }
    }
}
  1. 我们来使用吧
ImageLoadStratergy.getLoader().loadGif(this, gifUrl, image_ad);
  1. 如果有新框架呢? 我们只需要扩展一个类,实现ImageLoader
/**
 * @author qichunjie 2018/6/4
 */

public class FrescoImageLoader implements ImageLoader {
    
    @Override
    public void display(Context context, String url, ImageView targetView) {
        // 使用Fresco实现图片加载
    }

    @Override
    public void display(Context context, String url, ImageView targetView, boolean cache) {
        // 使用Fresco实现图片缓存加载
    }

    @Override
    public void loadGif(Context context, String url, ImageView targetView) {
        // 使用Fresco实现动图加载
    }
}
  1. 然后初始化一下,最好是在Application中
 ImageLoadStratergy.getLoader().setImageLoader(new FrescoImageLoader());

重新加载一下FrescoImageLoader,就可以正常使用了。是不是很简单呢?

如果本篇文章中你有所收获,请不要吝惜你的赞,谢谢!!!

    原文作者:DevCW
    原文地址: https://www.jianshu.com/p/cd80ea0cf765
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞