策略模式——算法的复用和独立变化


Demo 地址: https://github.com/ooblee/HelloDesignPattern

1. 定义

策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。

很好理解,把算法封装起来成类,让算法独立变化。然后具体使用什么样的算法,交给使用者去配置

现实中的模型:

比如出去某个城市旅游,可以坐大巴,坐动车,坐飞机,有多种交通方式都可以到达

比如肚子饿了,可以选择叫外卖,自己煮饭,去食堂吃,下馆子等方式

比如商家做促销,可以选择打折,发优惠券,买一送一等方式

具体采用什么样的方式,需要根据具体的计划安排、环境变化、主观客观因素等等来做决策

使用策略模式,把算法的行为和具体的使用环境分离开来,每一个类负责单独的算法,然后在具体的环境中根据条件来使用相应的策略

具体的环境,不依赖于算法的实现细节,而是针对算法策略类的抽象进行编程,策略模式把选择策略模式的过程和具体策略的实现分离开来独立维护

可以这样简单的理解,有两个点,A 和 B,在 A 和 B 之间有多条路径,策略模式就是封装每一条路径的走法,让使用者去决定走那一条路

多种路径

2. 设计

  • 环境类,持有抽象策略类的引用,使用抽象策略类编程。
  • 抽象策略类,定义处理方法。
  • 具体策略类,实现处理方法。

类图可以简单表示如下:

策略模式-类图

这里的抽象策略类就是 IStragegy,里面有一个 algorithm。而具体的算法细节交给子类具体策略类去实现,比如 StrategyA,StrategyB,StrategyC 等等

具体使用哪一种策略类,由环境类在运行时动态确定。

...
Context context = new Context();
IStrategy strategy = new StrategyA();
context.setStrategy(strategy);
context.algorithm();
...

3. 应用

3.1. 多重条件选择的改进

当满足不同的条件,有多种实现方案的时候,经常会有这样的代码,使用 if else 或者 swtich case 枚举条件,并选择相应的策略,比如

...
if (条件一) {
	// A 方案
} else if (条件二) {
  	// B 方案
} else if (条件三) {
  	// C 方案
}
...

这是把具体策略的使用和策略的实现硬编码在一个多重条件选择中,会造成后面维护困难,有这样的问题:

  • 使用条件的变化,会改动这个多重条件选择,而方案代码也在里头没有隔离开,对这里的任何改动都意味着风险

  • 其他地方也需要这些方案,无法复用

改进方案,就是把条件分支的每一个策略但单独封装成一个类,策略使用者动态配置。如果使用者只可能使用一种策略,这个多重条件选择就可以去掉

之后,使用方改变条件,就不会影响到实现方的代码了

3.2. 缓存策略

有这样一个场景,我们要做一个图片本地缓存,然后根据缓存的规模限制、缓存的失效管理,可以有多种策略。这时候就可以使用策略模式

现实中的缓存策略是很复杂的,这里做一个简化阉割版

先定义抽象策略类

public interface DiskCache {
 	boolean put(String key, Bitmap);
 	File get(String key);
}

使用者针对该抽象类的方法编程,然后在运行时动态决定用那种类型的缓存策略

比如我们可以定义这两种策略

  • 无限增长

    public class UnlimitedDiskCache implements DiskCache {
      	public boolean put(String key, Bitmap) {
          	// 如果本地磁盘空间够,直接保存起来
      	}
     	public File get(String key) {
          	// 从缓存文件夹中获取缓存文件
     	}
    }
    
  • 固定规模,采用 LRU 算法进行缓存失效管理

    public class LruDiskCache implements DiskCache {
    
        public boolean put(String key, Bitmap) {
          	// 如果没有达到固定的存储容量,直接保存
          	// 如果达到存储容量,采用 LRU 算法,判断最近被使用的时间,把最久未使用的文件淘汰,直到有空间保存新文件
      	}
     	public File get(String key) {
          	// 从缓存文件夹中获取缓存文件
     	}
    }
    
  • 固定规模,采用 LFU 算法进行缓存失效管理

    public class LfuDiskCache implements DiskCache {
    
        public boolean put(String key, Bitmap) {
          	// 如果没有达到固定的存储容量,直接保存
          	// 如果达到存储容量,采用 LFU 算法,判断在一段时间内使用次数最少的文件淘汰,直到有空间保存新文件
      	}
     	public File get(String key) {
          	// 从缓存文件夹中获取缓存文件
     	}
    }
    

使用者,直接动态选择需要的缓存策略即可

BitmapLoader loader = new BitmapLoader();
loader.setDiskCachePolicy(new LruDiskCache);

loader.put("key", bitmap);
File file = loader.get("key")

我们可以得到这样的类图

缓存策略-类图

3.3. JDK 的 Comparator

Java 集合框架中对 Comparator 的使用也是策略模式的一种体现

我们在使用 Java 集合框架的 Arrays 或者 Collections 进行排序的时候

Arrays.sort(T[],Comparator<? super T> c);
Collections.sort(List<T> list,Comparator<? super T> c);

需要传递一个 Comparator 对象用来决定每个元素的顺序

如果要升序排序,则 o1 小于 o2,返回 -1;相等,返回 0;o1 大于 o2,返回 1;降序排序反之

public interface Comparator<T> {
	int compare(T o1, T o2);
}

谁在前,谁在后,由我们来确定,于是 Java 集合框架抽象策略类 Comparator,Java 集合框架内部排序针对该抽象策略类进行编程,而我们只需要实现相应的策略,交给排序方法

这就做到了策略的使用和策略的实现分离,所以 Collections 工具类或者 Arrays 工具类才能做得如此通用,能够为各种异构类型的数据进行排序,内部专心实现排序算法,比较策略由使用者提供

比如我们有一个 People 类

public static class People {
	public final int age;
	public final String name;

	public People(int age, String name) {
		this.age = age;
		this.name = name;
	}
}

假设我们在一个数组内,要针对 age 字段进行升序排序,则可以

List<People> peoples = new ArrayList<People>();
peoples.add(new People(40, "小明"));
peoples.add(new People(20, "小红"));
peoples.add(new People(30, "小白"));
peoples.add(new People(90, "老黑"));
...

建立升序比较策略:

Comparator ascCmp = new Comparator<People>() {
	public int compare(People o1, People o2) {
		if (o1.age < o2.age ) return -1;
		if (o1.age == o2.age) return 0;
		if (o1.age > o2.age) return 1;
		return 0;
	}
}

建立降序比较策略:

Comparator descCmp = new Comparator<People>() {
	public int compare(People o1, People o2) {
		if (o1.age < o2.age ) return 1;
		if (o1.age == o2.age) return 0;
		if (o1.age > o2.age) return -1;
		return 0;
	}
}

最后把比较策略交给使用者:

// 升序排序
Collections.sort(peoples, ascCmp);

// 降序排序
Collections.sort(peoples, descCmp);

3.4. Spring 的 ResourceLoader

ResourceLoader 用来加载配置文件。

因为配置文件的路径很多样,所以可以对应多种策略。

抽象策略类定义如下:

public interface ResourceLoader {

	/** Pseudo URL prefix for loading from the class path: "classpath:" */
	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;


	/**
	 * Return a Resource handle for the specified resource location.
	 * <p>The handle should always be a reusable resource descriptor,
	 * allowing for multiple {@link Resource#getInputStream()} calls.
	 * <p><ul>
	 * <li>Must support fully qualified URLs, e.g. "file:C:/test.dat".
	 * <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
	 * <li>Should support relative file paths, e.g. "WEB-INF/test.dat".
	 * (This will be implementation-specific, typically provided by an
	 * ApplicationContext implementation.)
	 * </ul>
	 * <p>Note that a Resource handle does not imply an existing resource;
	 * you need to invoke {@link Resource#exists} to check for existence.
	 * @param location the resource location
	 * @return a corresponding Resource handle (never {@code null})
	 * @see #CLASSPATH_URL_PREFIX
	 * @see Resource#exists()
	 * @see Resource#getInputStream()
	 */
	Resource getResource(String location);

	/**
	 * Expose the ClassLoader used by this ResourceLoader.
	 * <p>Clients which need to access the ClassLoader directly can do so
	 * in a uniform manner with the ResourceLoader, rather than relying
	 * on the thread context ClassLoader.
	 * @return the ClassLoader (only {@code null} if even the system
	 * ClassLoader isn't accessible)
	 * @see org.springframework.util.ClassUtils#getDefaultClassLoader()
	 */
	ClassLoader getClassLoader();

}

具体的实现类有:

  • DefaultResourceLoader ,默认实现,处理 URL、classpath: 或者 \ 开头的资源路径。
  • ClassRelativeResourceLoader,当前类路径下。
  • FileSystemResourceLoader,在文件系统中。
  • ServletContextResourceLoader,ServletContext 的资源。

4. 特点

4.1. 优势

  • 算法和使用分离。两者独立变化,算法内容的调整不影响使用。

  • 易于扩展。新的算法只要实现新的类,不需要对原有的框架进行修改,符合开闭原则

  • 避免多重条件选择

  • 算法可复用

4.2. 缺点

  • 类膨胀

    但是如果算法变化多的话,容易造成系统需要实现多个具体的策略类。不过现实场景来说,这个出现还是比较少的,而且比起它的优势,这个缺点是可以忍受的

发布了61 篇原创文章 · 获赞 43 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/firefile/article/details/90314190