策略模式(Strategy Pattern)。

定义

策略模式是一种比较简单的模式,也叫做政策模式(Policy Pattern)。其定义如下:

定义一组算法,将每个算法都封装起来,并且使他们之间可以互换。

这个定义是非常明确、清晰的,“定义一组算法”,看看我们的三个计谋是不是三个算法?“将每个算法都封装起来”,封装类Context不就是这个作用吗?“使他们可以互换”当然可以互换了,都实现是相同的接口,那当然可以相互转化了。
策略模式使用的就是面向对象的继承和多态机制,非常容易理解和掌握,我们再来看看策略模式的三个角色:

  • Context封装角色

他也叫作上下文角色,其承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。

  • Strategy抽象策略角色

策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。

  • ConcreteStrategy具体策略角色

实现抽象策略中的操作,该类含有具体的算法。

通用代码

我们再来看策略模式的通用源码,非常简单。先看抽象策略角色,他是一个非常普通的接口,在我们的项目中就是一个普通的不能再普通的接口了,定义一个或多个具体的算法,如下所示。

public interface Strategy {
	/**
	 * 策略模式的运算法则
	 */
	void doSomething();
}

具体策略也是非常普通的一个实现类,只要实现接口中的方法就可以,如下所示。

public class ConcreteStrategy1 implements Strategy {

	@Override
	public void doSomething() {
		System.out.println("具体策略1的运算法则");
	}

}
public class ConcreteStrategy2 implements Strategy {

	@Override
	public void doSomething() {
		System.out.println("具体策略2的运算法则");
	}

}

策略模式的重点就是封装角色,他是借用了代理模式的思路,大家可以想想,他和代理模式有什么差别,差别就是策略模式的封装角色和被封装的二策略类不用是同一个接口,如果是同一个接口那就成为了代理模式。我们来看封装角色,如下所示。

public class Context {
	// 抽象策略
	private Strategy strategy = null;
	/**
	 * 构造函数设置具体策略
	 * @param strategy
	 */
	public Context(Strategy strategy) {
		this.strategy = strategy;
	}
	/**
	 * 封装后的策略方法
	 */
	public void doAnything() {
		this.strategy.doSomething();
	}
}

高层模块的调用非常简单,知道要用哪个策略,产生出他的对象,然后放到封装角色中就完成任务了,如下所示。

public class Client {
	public static void main(String[] args) {
		// 声明一个具体的策略
		Strategy strategy = new ConcreteStrategy1();
		// 声明上下文对象
		Context context = new Context(strategy);
		// 执行封装后的方法
		context.doAnything();
	}
}

策略模式就是这么简单,他就是采用了面向对象的继承和多态机制,其他没什么玄机。想想看,你真实的业务环境有这么简单吗?一个类实现多个接口很正常,你要有火眼金睛看清楚哪个接口是抽象策略接口,哪些是和策略模式没有任何关系,这就是你作为系统分析师的价值所在。

优点

  • 算法可以自由切换

这是策略模式本身定义的,只要实现抽象策略,他就成为策略家族的一个成员,通过封装角色对其进行封装,保证对外提供“可自由切换”的策略。

  • 避免使用多重条件判断

如果没有策略模式,我们想想看会是什么样子?一个策略家族有5个策略算法,一会要使用A策略,一会要使用B策略,怎么设计呢?使用多重的条件语句?多重条件语句不易维护,而且出错的概率大大增强。使用策略模式后,可以由其他模块决定采用何种策略,策略家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。

  • 扩展性良好

这甚至都不用说是他的优点,因为他太明显了。在现有的系统中增加一个策略太容易了,只要实现接口就可以了,其他都不用修改,类似于一个可反复拆卸的插件,这大大的符合了OCP原则。

缺点

  • 策略类数量增多

每一个策略都是一个类,复用的可能性很小,类数量增多。

  • 所有的策略类都需要对外暴露

上层模块必须知道有哪些策略,然后才能决定使用哪一个策略,这与迪米特法则是相违背的,我只是想使用了一个策略,我凭什么就要了解这个策略呢?那要你的封装类还有什么意义?这时原装策略模式的一个缺点,幸运的是,我们可以使用其他模式来修正这个缺陷,如工厂方法模式、代理模式或享元模式。

使用场景

  • 多个类只有在算法或行为上稍有不同的场景。
  • 算法需要自由切换的场景。

例如,算法的选择而是由使用者决定的,或者算法始终在进化,特别是一些站在技术前沿的行业,连业务专家都无法给你保证这样的系统规则能够存在多长时间,在这种情况下策略模式是你最好的助手。

  • 需要屏蔽算法规则的场景。

现在的科技发展得很快,人脑的记忆是有限的(就目前来说是有限的),太多的算法你只要知道一个名字就可以了,传递相关的数字进来,反馈一个运算结果,万事大吉。

注意事项

如果系统中的一个策略家族的具体策略数量超过4个,则需要考虑使用混合模式,解决策略类膨胀和对外暴露的问题,否则日后的系统维护就会成为一个烫手山芋,谁都不想接。

扩展

先给出一道小学的题目:输入3个参数,进行加减法运算,参数中两个是int型的,剩下的一个参数是String型的,只有“+”、“-”两个符号可以选择,不要考虑什么复杂的校验,我们做的是白箱测试,输入的就是标准的int类型和合规的String类型,各位大侠,想想看,怎么做,简单的很!

有非常多的实现方式,我今天来说四种。先说第一种,写一个类,然后进行加减法运算,如下所示。

public class Calculator {
	// 加符号
	private final static String ADD_SYMBOL = "+";
	// 减符号
	private final static String SUB_SYMBOL = "-";

	public int exec(int a, int b, String symbol) {
		int result = 0;
		if (ADD_SYMBOL.equals(symbol)) {
			result = this.add(a, b);
		} else if (SUB_SYMBOL.equals(symbol)) {
			result = this.sub(a, b);
		}
		return result;
	}

	/**
	 * 加法运算
	 * 
	 * @param a
	 * @param b
	 * @return
	 */
	private int add(int a, int b) {
		return a + b;
	}

	/**
	 * 减法运算
	 * 
	 * @param a
	 * @param b
	 * @return
	 */
	private int sub(int a, int b) {
		return a - b;
	}
}

算法太简单了,每个程序员都会写。这个方案是非常简单的,能够解决问题,我详细这时大家最容易想到的方案,我们不评论这个方案的优劣,等把四个方案全部讲完了,你自己就会发现孰优孰劣。

我们再来看第二个方案,上面的算法太罗嗦了,简化算法如下所示。

public class Calculator2 {
	// 加符号
	private final static String ADD_SYMBOL = "+";

	public int exec(int a, int b, String symbol) {
		return symbol.equals(ADD_SYMBOL) ? a + b : a - b;
	}
}

这也非常简单,就是一个三目运算符,确实简化了很多。有缺陷先别管,我们主要讲设计,你在实际项目应用中要处理该程序中的缺陷。

我们再来思考第三个方案,本文介绍的策略模式,那把策略模式应用到该需求是不是很合适啊?是啊,非常合适!加减法就是一个具体的策略,非常简单,我们先来看抽象策略,定义每个策略必须实现的方法,如下所示。

public interface ICalculator {

	int exec(int a, int b);
}

抽象策略定义了一个唯一的方法来执行运算。至于具体执行的是加法还是减法,运算时由上下文角色决定。我们再来看两个具体的策略,如下所示。

public class Add implements ICalculator {

	@Override
	public int exec(int a, int b) {
		return a + b;
	}

}
public class Sub implements ICalculator {

	@Override
	public int exec(int a, int b) {
		return a - b;
	}

}

封装角色的责任是保证策略时可以相互替换,如下所示。

public class Context {

	private ICalculator cal = null;

	public Context(ICalculator cal) {
		this.cal = cal;
	}

	public int exec(int a, int b) {
		return this.cal.exec(a, b);
	}
}

代码非常简单,该部分就不再增加注释信息了。上下文类负责把策略封装起来,具体怎么自由的切换策略则是由高层模块负责声明的。

在该策略模式的一个具体应用中,我们使用Context准备了一组算法(加法和减法),并封装了起来,具体使用哪一个策略(加法还是减法)则由上层模块声明,这样扩展性非常好。

现在只剩最后一个方案,一般最后出场的都是重量级的任务,压场嘛!那就请出我们最后一个重量级角色,音乐响起,一个黑影站定舞台中央,所有灯光突然聚焦,主角缓缓抬起头,他就是——策略枚举!我们来看啊可能其真实实力,如下所示。

public enum CalculatorEnum {

	// 加法运算
	ADD("+") {
		@Override
		public int exec(int a, int b) {
			return a + b;
		}
	},
	// 减法运算
	SUB("-") {
		@Override
		public int exec(int a, int b) {
			return a - b;
		}
	};

	String value = "";

	/**
	 * 定义成员值类型
	 * 
	 * @param value
	 */
	private CalculatorEnum(String value) {
		this.value = value;
	}

	/**
	 * 获得枚举成员的值
	 * 
	 * @return
	 */
	public String getValue() {
		return value;
	}

	/**
	 * 声明一个抽象函数
	 * 
	 * @param a
	 * @param b
	 * @return
	 */
	public abstract int exec(int a, int b);
}

先想一想他的名字,为什么叫做策略枚举?枚举没有问题,他就是一个Enum类型,那为什么又叫做策略呢?找找看能不能找到策略的影子在里面?是的,我们定义了一个抽象的方法exec,然后在每个枚举成员中进行了实现,如果不实现会怎么样呢?你试试看看,不实现该方法就不能编译,现在是不是清楚了?把原有定义在抽象策略中的方法移植到枚举中,每个枚举成员就成为一个具体策略。简单吧,总结一下,策略枚举定义如下:

  • 他是一个枚举。
  • 他是一个浓缩了的策略模式的枚举。

当然,我使用内置类也可以实现相同的功能,写一个Context类,然后把抽象策略、具体策略都内置进去,不就可以解决问题了,是的,可以解决,但是扩展性如何?可读性入如何?代码是让人读的,然后才是让机器执行,别把顺序搞反了!
场景类如下所示。

public class Client {
	// 加符号
	private final static String ADD_SYMBOL = "+";
	// 减符号
	private final static String SUB_SYMBOL = "-";

	public static void main(String[] args) {
		// 输入的两个参数是数字
		int a = Integer.parseInt(args[0]);
		String symbol = args[1]; // 符号
		int b = Integer.parseInt(args[2]);
		System.out.println("输入的参数为:" + Arrays.toString(args));
		// 最直接的加减法
		calculatorTest1(a, symbol, b);
		// 简化算法
		calculator2Test(a, symbol, b);
		// 策略模式
		calculator3Test(a, symbol, b);
		// 策略枚举
		calculator4Test(a, symbol, b);
	}
	/**
	 * 最直接的加减法
	 * 
	 * @param a
	 * @param symbol
	 * @param b
	 */
	private static void calculatorTest1(int a, String symbol, int b) {
		// 生成一个运算器
		Calculator cal = new Calculator();
		System.out.println("运算结果为:" + a + symbol + b + "=" + cal.exec(a, b, symbol));
	}

	/**
	 * 简化算法
	 * 
	 * @param a
	 * @param symbol
	 * @param b
	 */
	private static void calculator2Test(int a, String symbol, int b) {
		Calculator2 calculator2 = new Calculator2();
		System.out.println("运算结果为:" + a + symbol + b + "=" + calculator2.exec(a, b, symbol));
	}

	/**
	 * 策略模式
	 * 
	 * @param a
	 * @param symbol
	 * @param b
	 */
	private static void calculator3Test(int a, String symbol, int b) {
		// 上下文
		Context context = null;
		// 判断初始化哪一个策略
		if (ADD_SYMBOL.equals(symbol)) {
			context = new Context(new Add());
		} else if (SUB_SYMBOL.equals(symbol)) {
			context = new Context(new Sub());
		}
		System.out.println("运算结果为:" + a + symbol + b + "=" + context.exec(a, b));
	}
	/**
	 * 策略枚举
	 * @param a
	 * @param symbol
	 * @param b
	 */
	private static void calculator4Test(int a, String symbol, int b) {
		System.out.println("运算结果为:" + a + symbol + b + "=" + CalculatorEnum.ADD.exec(a, b));
	}
}

第四种方案运行结果与方案一相同。看这个场景类,代码量非常少,而且还有一个显著的优点:真实地面向对象,看看这条语句:

CalculatorEnum.ADD.exec(a, b)

是不是类似于“拿出计算器(Calculator),对a和b进行加法运算(ADD),并立刻执行(exec)”,这与我们日常接触逻辑是不是非常相似,这也正是我们架构师要担当的职责!

注意:策略枚举是一个非常优秀和方便的模式,但是他受枚举类型的限制,每个枚举项都是public、final、static的,扩展性受到了一定的约束,因此在系统开发中,策略枚举一般担当不经常发生变化的角色。

最佳实践

策略模式是一个非常简单的模式。他在项目中使用的非常多,但他单独使用的地方就比较少了,因为他有致命缺陷:所有的策略都需要暴露出去,这样才方便客户端决定使用哪一个策略。在实际项目中,我们一般通过工厂方法模式来实现策略类的声明。

猜你喜欢

转载自blog.csdn.net/en_joker/article/details/82732425