设计模式(八)——策略模式

375 阅读13分钟

本文属于系列文章《设计模式》,附上文集链接

策略模式

  • 定义:定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换。
  • 作用:首先是封装的算法,然后可相互替换,可以想象出一个场景,就是有很多种的选择,然后可以选择最合适的一种,如果不用策略模式的话,那就是一个一个自行选择,对的。
  • 属于行为类模式

举个例子

之前做外包做一个网站,其中有一个模块是支付的,可供选择的方式有支付宝支付,微信支付和支付宝的跳转支付(H5跳转app支付)。当时是蠢的啊,没想到策略模式这种东西,上代码

// 支付控制器
public class PayController {
	// 微信要自己生成二维码,若是在手机端,则必须要微信浏览器才能使用
	public void wechatPay() {
		// 模拟request收集到订单号,商品描述,总价钱的参数
		String orderNo = System.currentTimeMillis() + "";
		String body = "终极商店—大红苹果";
		long totalFee = 6L;
		// 模拟调用支付api的过程
		System.out.println(
				"使用订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + "调用微信支付工具类,请求下单API,返回支付URL并根据URL生成二维码");
	}
	// 支付宝直接跳转到支付宝收集订单信息的jsp页面来发起网关支付,只能用于PC端
	public void aliPay() {
		// 模拟request收集到订单号,商品描述,总价钱的参数
		String orderNo = System.currentTimeMillis() + "";
		String body = "终极商店—大红苹果";
		long totalFee = 6L;
		// 模拟调用支付api的过程
		System.out.println("将订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + "作为参数,访问支付宝收集订单信息的jsp页面来发起网关支付");
	}
	// 移动端使用支付宝支付,跳转到支付宝收银台,支付宝收银台有唤醒支付宝APP的函数,但不能在微信浏览器打开
	public void mobileAliPay() {
		// 模拟request收集到订单号,商品描述,总价钱的参数
		String orderNo = System.currentTimeMillis() + "";
		String body = "终极商店—大红苹果";
		long totalFee = 6L;
		// 模拟调用支付api的过程
		System.out.println("将订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + "作为参数,访问支付宝的收银台来发起移动支付");
	}
}
// 场景类
public class Client {
	public static void main(String[] args) throws InterruptedException {
		PayController payController = new PayController();
		System.out.println("用户Tom选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在PC端,使用Chrome浏览器操作");
		payController.aliPay();
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Sivan选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在移动端端,使用非微信浏览器操作");
		payController.mobileAliPay();
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Jack选择了商品,然后开始下单支付");
		System.out.println("用户选择了微信支付");
		System.out.println("在移动端端,使用微信浏览器操作");
		payController.wechatPay();
		}
}
结果:
用户Tom选择了商品,然后开始下单支付
用户选择了支付宝支付
在PC端,使用Chrome浏览器操作
将订单号:1491293476471,商品描述:终极商店—大红苹果和总价钱:6作为参数,访问支付宝收集订单信息的jsp页面来发起网关支付
\-----------------------------------------
用户Sivan选择了商品,然后开始下单支付
用户选择了支付宝支付
在移动端端,使用非微信浏览器操作
将订单号:1491293476482,商品描述:终极商店—大红苹果和总价钱:6作为参数,访问支付宝的收银台来发起移动支付
\-----------------------------------------
用户Jack选择了商品,然后开始下单支付
用户选择了微信支付
在移动端端,使用微信浏览器操作
使用订单号:1491293476492,商品描述:终极商店—大红苹果和总价钱:6调用微信支付工具类,请求下单API,返回支付URL并根据URL生成二维码

代码就不解释了,注释都说明了,当时就是傻了,没想到扩展性等的问题。试想下,以后如果多一种支付方式,我就要在PayController多加一个方法,修改已有代码,不符合开闭原则喔。其次,上面的每个pay的方法都有相同的代码(requset获取参数)。。一点复用都没有,贼气(这里倒和策略模式无关,只是对自身实力的一种吐槽)。

用策略模式改造下,如下:

// 支付策略接口
public interface PayStrategy {
	public void pay(String orderNo,String body,long totalFee);
}
//支付宝支付策略,直接跳转到支付宝收集订单信息的jsp页面来发起网关支付,只能用于PC端
public class AliPayStraegy implements PayStrategy {
	@Override
	public void pay(String orderNo, String body, long totalFee) {
		// 模拟调用支付api的过程
		System.out.println("将"
				\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
						\+ "作为参数,访问支付宝收集订单信息的jsp页面来发起网关支付");
	}
}
// 移动端使用支付宝支付策略,跳转到支付宝收银台,支付宝收银台有唤醒支付宝APP的函数,但不能在微信浏览器打开
public class MobileAlipayStrategy implements PayStrategy{
	@Override
	public void pay(String orderNo,String body,long totalFee){
		// 模拟调用支付api的过程
		System.out.println("将"
				\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
						\+ "作为参数,访问支付宝的收银台来发起移动支付");
	}
}
//微信支付策略,要自己生成二维码,若是在手机端,则必须要微信浏览器才能使用
public class WechatPayStrategy implements PayStrategy {
	@Override
	public void pay(String orderNo, String body, long totalFee) {
		// 模拟调用支付api的过程
		System.out.println("使用"
				\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
						\+ "调用微信支付工具类,请求下单API,返回支付URL并根据URL生成二维码");
	}
}
// 支付控制器
public class PayController {
	private PayStrategy payStrategy;
	public void setPayStrategy(PayStrategy payStrategy) {
		this.payStrategy = payStrategy;
	}
	public void pay() {
		// 模拟request收集到订单号,商品描述,总价钱的参数
		String orderNo = System.currentTimeMillis() + "";
		String body = "终极商店—大红苹果";
		long totalFee = 6L;
		// 模拟调用支付api的过程
		payStrategy.pay(orderNo, body, totalFee);
	}
}
// 场景类
public class Client {
	public static void main(String[] args) throws InterruptedException {
		PayController payController = new PayController();
		System.out.println("用户Tom选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在PC端,使用Chrome浏览器操作");
		payController.setPayStrategy(new AliPayStraegy());
		payController.pay();
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Sivan选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在移动端端,使用非微信浏览器操作");
		payController.setPayStrategy(new MobileAlipayStrategy());
		payController.pay();
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Jack选择了商品,然后开始下单支付");
		System.out.println("用户选择了微信支付");
		System.out.println("在移动端端,使用微信浏览器操作");
		payController.setPayStrategy(new WechatPayStrategy());
		payController.pay();
		}
}

我们先定义了一个PayStrategy的接口,然后每一个支付的具体方法都实现这个接口,然后在PayController中组合了一个PayStrategy,定义了一个set方法,这个set方法,我把它称作“策略选择器”,高大上,哈哈。

然后在pay方法中,调用策略来执行pay方法就行了。然后在客户端,每次调用支付的接口的时候,就使用我们的“策略选择器”,使用指定的策略,然后就OK了。PayController在执行pay方法的时候会根据传入的PayStrategy们来选择相应的方法来执行,这就是简单的策略模式。

有啥好处?第一,以后每多一种支付方式,我只需要实现PayStrategy接口来新建一个类,而不需要修改原有代码,符合开闭原则。第二,假设原有的支付方式发生改变,需要修改,我只需要修改对应的策略类,避免了对其他的策略类造成影响的可能。第三,客户端对Controller的了解变少了,因为只需要了解策略的种类,而不需要了解Controller哪个方法具体是干啥的,也符合迪米特原则。

延伸下,当时我在外包中是用@Resource将PayStrategy的实现类都放到IOC容器,然后在PayController的payService(对的,并不是像代码那样在Controller直接组装的,啊哈哈),用@AutoWired组装每一个支付策略的,页面会传一个payType参数(对的,上面的代码还是没提到,啊哈哈),根据payType参数来使用相应的策略,来完成下单那个动作。如果有更好的办法的朋友欢迎拍砖,在此先谢谢了。

延伸

这个真的是延伸了,首先是看书看到的实现方法,觉得挺有意思的一种实现,叫策略枚举,也是666. 代码:

// 策略枚举
public enum Pay {
	//支付宝支付策略,直接跳转到支付宝收集订单信息的jsp页面来发起网关支付,只能用于PC端
	AliPay() {
		@Override
		public void pay(String orderNo, String body, long totalFee) {
			// 模拟调用支付api的过程
			System.out.println("将"
					\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
							\+ "作为参数,访问支付宝收集订单信息的jsp页面来发起网关支付");
		}
	},
	//微信支付策略,要自己生成二维码,若是在手机端,则必须要微信浏览器才能使用
	WechatPay() {
		@Override
		public void pay(String orderNo, String body, long totalFee) {
			// 模拟调用支付api的过程
			System.out.println("使用"
					\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
							\+ "调用微信支付工具类,请求下单API,返回支付URL并根据URL生成二维码");
		}
	},
	// 移动端使用支付宝支付策略,跳转到支付宝收银台,支付宝收银台有唤醒支付宝APP的函数,但不能在微信浏览器打开
	MobileAliPay() {
		@Override
		public void pay(String orderNo,String body,long totalFee){
			// 模拟调用支付api的过程
			System.out.println("将"
					\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
							\+ "作为参数,访问支付宝的收银台来发起移动支付");
		}
	};
	// 定义支付的抽象方法,枚举类型每多一个值,都得实现这个抽象方法,就是这个特性才能666
	public abstract void pay(String orderNo, String body, long totalFee);
}
// 场景类
public class Client {
	public static void main(String[] args) throws InterruptedException {
		// 偷偷懒,直接模拟那些参数了,勿喷
		String orderNo = System.currentTimeMillis() + "";
		String body = "终极商店—大红苹果";
		long totalFee = 6L;
		PayController payController = new PayController();
		System.out.println("用户Tom选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在PC端,使用Chrome浏览器操作");
        // 直接使用策略的枚举值
		Pay.AliPay.pay(orderNo, body, totalFee);
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Sivan选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在移动端端,使用非微信浏览器操作");
        // 直接使用策略的枚举值
		Pay.MobileAliPay.pay(orderNo, body, totalFee);
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Jack选择了商品,然后开始下单支付");
		System.out.println("用户选择了微信支付");
		System.out.println("在移动端端,使用微信浏览器操作");
        // 直接使用策略的枚举值
		Pay.WechatPay.pay(orderNo, body, totalFee);
		}
}

结果:

贴图增加说服力,这酸爽,谁用谁知道

没错,上面就是策略枚举,当时第一次看到的时候惊了个大呆,枚举还可以这样玩。我在上面的示例偷了偷懒,正确的做法应该是在payController那里选择策略的,具体的代码脑补下吧。。。

虽然好像和很多看到的策略模式有很大的出入,但不得不说,这个并没有什么毛病,同一样东西的不同表达。看回定义,把每一个算法封装起来, 并且使它们可相互替换,而他们的算法实现都是在枚举值中实现的,相互替换也没什么毛病。毕竟扩展接口的方法也需要记住哪个类对应哪个算法,而枚举需要的记住某个枚举值对应某个算法,只不过就是如果需要构造函数做点什么事的话,枚举的方法就很蛋疼了。。懂的自然懂,哈哈。

上面那个是看书看到,下面这个就是我看Thinking In Java中看到内部类的时候突发奇想联想到的了,可能看到内部类应该已经想到大概怎么实现了吧,哈哈,来看代码:

// 使用内部类的策略模式
public class PayStrategyWithInnerClass {
	//支付宝支付策略,直接跳转到支付宝收集订单信息的jsp页面来发起网关支付,只能用于PC端
	public class AliPay implements PayStrategy{
		@Override
		public void pay(String orderNo, String body, long totalFee) {
			// 模拟调用支付api的过程
			System.out.println("将"
					\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
							\+ "作为参数,访问支付宝收集订单信息的jsp页面来发起网关支付");
		}
	}
	//微信支付策略,要自己生成二维码,若是在手机端,则必须要微信浏览器才能使用
	public class WechatPay implements PayStrategy{
		@Override
		public void pay(String orderNo, String body, long totalFee) {
			// 模拟调用支付api的过程
			System.out.println("使用"
					\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
							\+ "调用微信支付工具类,请求下单API,返回支付URL并根据URL生成二维码");
		}
	}
	// 移动端使用支付宝支付策略,跳转到支付宝收银台,支付宝收银台有唤醒支付宝APP的函数,但不能在微信浏览器打开
	public class MobileAliPay implements PayStrategy{
		@Override
		public void pay(String orderNo, String body, long totalFee) {
			// 模拟调用支付api的过程
			System.out.println("将"
					\+ "订单号:" + orderNo + ",商品描述:" + body + "和总价钱:" + totalFee + ""
							\+ "作为参数,访问支付宝的收银台来发起移动支付");
		}
	}
}
// 场景类
public class Client {
	public static void main(String[] args) throws InterruptedException {
		PayStrategyWithInnerClass payStrategy = new PayStrategyWithInnerClass();
		PayController payController = new PayController();
		System.out.println("用户Tom选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在PC端,使用Chrome浏览器操作");
        // 实现方式有所差别
		payController.setPayStrategy(payStrategy.new AliPay());
		payController.pay();
		// 直接使用策略的枚举值
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Sivan选择了商品,然后开始下单支付");
		System.out.println("用户选择了支付宝支付");
		System.out.println("在移动端端,使用非微信浏览器操作");
		payController.setPayStrategy(payStrategy.new MobileAliPay());
		payController.pay();
		Thread.sleep(10);
		System.out.println("-----------------------------------------");
		System.out.println("用户Jack选择了商品,然后开始下单支付");
		System.out.println("用户选择了微信支付");
		System.out.println("在移动端端,使用微信浏览器操作");
		payController.setPayStrategy(payStrategy.new WechatPay());
		payController.pay();
		}
}
结果:
用户Tom选择了商品,然后开始下单支付
用户选择了支付宝支付
在PC端,使用Chrome浏览器操作
将订单号:1491315431218,商品描述:终极商店—大红苹果和总价钱:6作为参数,访问支付宝收集订单信息的jsp页面来发起网关支付
\-----------------------------------------
用户Sivan选择了商品,然后开始下单支付
用户选择了支付宝支付
在移动端端,使用非微信浏览器操作
将订单号:1491315431230,商品描述:终极商店—大红苹果和总价钱:6作为参数,访问支付宝的收银台来发起移动支付
\-----------------------------------------
用户Jack选择了商品,然后开始下单支付
用户选择了微信支付
在移动端端,使用微信浏览器操作
使用订单号:1491315431241,商品描述:终极商店—大红苹果和总价钱:6调用微信支付工具类,请求下单API,返回支付URL并根据URL生成二维码

其实也没啥不同,内部类来实现策略PayStrategy接口,然后实现方法,在调用的场景也是实例化一个内部类而已,实现的实质也是策略选择器。。内部类那里的外围类有点像枚举,但是又有点差别。没遇到实际问题也想不出来啥例子,有经验的人士麻烦评论区拍个砖,感激不尽。

以上就是策略模式,水平有限,难免有错,欢迎评论区指责