作者:zuoxiaolong8810(左潇龙),转载请注明出处。
上章我们着重讲解了观察者模式和事件驱动,那么本章来讨论一个个人认为在开发过程中出场率极高的设计模式,策略模式。
策略模式在LZ第一次接触到的时候,LZ是这么理解的,就是如果我们想往一个方法当中插入随便一段代码的话,就是策略模式。即如下形式。
- public class MyClass {
-
- public void myMethod(){
- System.out.println("方法里的代码");
-
- System.out.println("方法里的代码");
- }
- }
在JAVA中,接口可以满足LZ的这一过分要求,我们可以设计一个接口,并当做参数传进去,就能达到这个效果了。我们来看,先定义一个接口。
- public interface MyInterface {
-
- void insertCode();
-
- }
将原来的类改成这样,传递一个接口进去。
- public class MyClass {
-
- public void myMethod(MyInterface myInterface){
- System.out.println("方法里的代码");
-
- myInterface.insertCode();
- System.out.println("方法里的代码");
- }
- }
我们只要实现了MyInterface这个接口,在insertCode方法中写入我们想要插进去的代码,再将这个类传递给myMethod方法,就可以将我们随手写的代码插到这个方法当中。比如这样。
- class InsertCode1 implements MyInterface{
-
- public void insertCode() {
- System.out.println("我想插进去的代码,第一种");
- }
-
- }
-
- class InsertCode2 implements MyInterface{
-
- public void insertCode() {
- System.out.println("我想插进去的代码,第二种");
- }
-
- }
这样我们在调用myMethod方法时就可以随意往里面插入代码了,比如。
-
- public class Client {
-
- public static void main(String[] args) {
- MyClass myClass = new MyClass();
- myClass.myMethod(new InsertCode1());
- System.out.println("--------------------");
- myClass.myMethod(new InsertCode2());
- }
-
- }
那么运行出来的结果就是我们成功的将两端代码插入到了myMethod方法中,以上所讲的算是JAVA中一种技术层面的实现,就是传入一个接口,封装代码。那么既然谈到设计模式,就要有设计模式的应用场景,有关策略模式,所产生的形式就和上述是一模一样的,只是我们适当的给予模式的应用场景,就会让它变的更有价值。
下面LZ给出策略模式的标准定义,引自百度百科。
定义:策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
分析下定义,策略模式定义和封装了一系列的算法,它们是可以相互替换的,也就是说它们具有共性,而它们的共性就体现在策略接口的行为上,另外为了达到最后一句话的目的,也就是说让算法独立于使用它的客户而独立变化,我们需要让客户端依赖于策略接口。
下面给出策略模式的类图,引自百度百科。
这个类图并不复杂,右边是策略接口以及它的实现类,左边会有一个上下文,这个上下文会拥有一个策略,而具体这个策略是哪一种,我们是可以随意替换的。
LZ下面使用JAVA代码诠释上面的类图,方便各位理解各个类之间的关系。
首先是策略接口以及它的实现类。
- package net;
-
- public interface Strategy {
-
- void algorithm();
-
- }
-
- class ConcreteStrategyA implements Strategy{
-
- public void algorithm() {
- System.out.println("采用策略A计算");
- }
-
- }
- class ConcreteStrategyB implements Strategy{
-
- public void algorithm() {
- System.out.println("采用策略B计算");
- }
-
- }
- class ConcreteStrategyC implements Strategy{
-
- public void algorithm() {
- System.out.println("采用策略C计算");
- }
-
- }
下面是我们的上下文,它会拥有一个策略接口。
- package net;
-
- public class Context {
-
- Strategy strategy;
-
- public void method(){
- strategy.algorithm();
- }
-
- public void setStrategy(Strategy strategy) {
- this.strategy = strategy;
- }
- }
method方法是上下文类的一个公开方法,实际当中一般会和业务相关,这里就暂且取名为method方法。下面我们使用客户端调用一下。
- package net;
-
-
- public class Client {
-
- public static void main(String[] args) throws Exception {
- Context context = new Context();
- context.setStrategy(new ConcreteStrategyA());
- context.method();
-
- context.setStrategy(new ConcreteStrategyB());
- context.method();
-
- context.setStrategy(new ConcreteStrategyC());
- context.method();
- }
- }
上面我们替换了两次策略,但是调用方式不变,下面我们看下运行结果。
上面的例子代码清晰但却理解起来很生硬,下面LZ举一个具有实际意义的例子。
就比如我们要做一个商店的收银系统,这个商店有普通顾客,会员,超级会员以及金牌会员的区别,针对各个顾客,有不同的打折方式,并且一个顾客每在商店消费1000就增加一个级别,那么我们就可以使用策略模式,因为策略模式描述的就是算法的不同,而且这个算法往往非常繁多,并且可能需要经常性的互相替换。
这里我们举例就采用最简单的,以上四种顾客分别采用原价,八折,七折和半价的收钱方式。
那么我们首先要有一个计算价格的策略接口,如下。
- public interface CalPrice {
-
- Double calPrice(Double originalPrice);
-
- }
下面我们给出四个计算方式。
- class Common implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice;
- }
-
- }
- class Vip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.8;
- }
-
- }
- class SuperVip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.7;
- }
-
- }
- class GoldVip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.5;
- }
-
- }
以上四种计算方式非常清晰,分别是原价,八折,七折和半价。下面我们看客户类,我们需要客户类帮我们完成客户升级的功能。
-
- public class Customer {
-
- private Double totalAmount = 0D;
- private Double amount = 0D;
- private CalPrice calPrice = new Common();
-
-
- public void buy(Double amount){
- this.amount = amount;
- totalAmount += amount;
- if (totalAmount > 3000) {
- calPrice = new GoldVip();
- }else if (totalAmount > 2000) {
- calPrice = new SuperVip();
- }else if (totalAmount > 1000) {
- calPrice = new Vip();
- }
- }
-
- public Double calLastAmount(){
- return calPrice.calPrice(amount);
- }
- }
下面我们看客户端调用,系统会帮我们自动调整收费策略。
-
- public class Client {
-
- public static void main(String[] args) {
- Customer customer = new Customer();
- customer.buy(500D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- customer.buy(1200D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- customer.buy(1200D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- customer.buy(1200D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- }
-
- }
运行以后会发现,第一次是原价,第二次是八折,第三次是七折,最后一次则是半价。我们这样设计的好处是,客户不再依赖于具体的收费策略,依赖于抽象永远是正确的。不过上述的客户类实在有点难看,尤其是buy方法,我们可以使用简单工厂来稍微改进一下它。我们建立如下策略工厂。
-
- public class CalPriceFactory {
-
- private CalPriceFactory(){}
-
- public static CalPrice createCalPrice(Customer customer){
- if (customer.getTotalAmount() > 3000) {
- return new GoldVip();
- }else if (customer.getTotalAmount() > 2000) {
- return new SuperVip();
- }else if (customer.getTotalAmount() > 1000) {
- return new Vip();
- }else {
- return new Common();
- }
- }
- }
这样我们就将制定策略的功能从客户类分离了出来,我们的客户类可以变成这样。
-
- public class Customer {
-
- private Double totalAmount = 0D;
- private Double amount = 0D;
- private CalPrice calPrice = new Common();
-
-
- public void buy(Double amount){
- this.amount = amount;
- totalAmount += amount;
-
- calPrice = CalPriceFactory.createCalPrice(this);
- }
-
- public Double calLastAmount(){
- return calPrice.calPrice(amount);
- }
-
- public Double getTotalAmount() {
- return totalAmount;
- }
-
- public Double getAmount() {
- return amount;
- }
-
- }
现在比之前来讲,我们的策略模式更加灵活一点,但是相信看过LZ博文的都知道,LZ最不喜欢elseif,所以策略模式也是有缺点的,就是当策略改变时,我们需要使用elseif去判断到底使用哪一个策略,哪怕使用简单工厂,也避免不了这一点。比如我们又添加一类会员,那么你需要去添加elseif。再比如我们的会员现在打九折了,那么你需要添加一个九折的策略,这没问题,我们对扩展开放,但是你需要修改elseif的分支,将会员的策略从八折替换为九折,这是简单工厂的诟病,在之前已经提到过,对修改开放。
在简单工厂模式一章中,LZ就遗留了一堆的elseif,并在工厂方法一章提供了解决方案,但是LZ没有写上来,这次LZ将整个解决方案搬上来,之前的简单工厂可以使用相同的方法改进。
LZ在工厂方法一章中已经指明可以使用注解来处理这一问题,但是LZ只给出了实现的思路,没有给出具体实现的方式,本次刚好将之前的实现方式补上,如果各位掌握了这里的实现方式,那么对于简单工厂模式那一章的问题也自然而然可以轻松解决。
不过简单工厂那一章的问题相对这里更简单一点,因为那里我们只需要做一个servlet名称与Class引用的映射关系,就可以消除掉elseif,使用现有的映射直接创造servlet实例,但是在这里就不行了,因为我们涉及到了客户总金额的判断,这不再是一个简单的名称与策略的对应关系。
所以我们需要给注解加入属性上限和下限,用来表示策略生效的区间,用来解决总金额判断的问题。
下面我们一步一步来,首先我们做一个注解,这个注解是用来给策略添加的,当中可以设置它的上下限,我们来看。
- package com.calprice;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
-
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface TotalValidRegion {
-
- int max() default Integer.MAX_VALUE;
- int min() default Integer.MIN_VALUE;
- }
这个注解很简单,我们只是用它来记录每一个策略的生效区间,下面我们就可以在我们的各个策略类里去设置我们的生效区间了,我们将策略类全部改成如下形式。
- package com.calprice;
-
- @TotalValidRegion(max=1000)
- class Common implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice;
- }
-
- }
- @TotalValidRegion(min=1000,max=2000)
- class Vip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.8;
- }
-
- }
- @TotalValidRegion(min=2000,max=3000)
- class SuperVip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.7;
- }
-
- }
- @TotalValidRegion(min=3000)
- class GoldVip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.5;
- }
-
- }
好了,我们使用注解表示每一个策略生效的区间,下面我们要做最重要的工作,就是处理注解。我们在策略工厂处理注解。这个类会变的比较复杂一点,所以LZ直接加注释。我们来看策略工厂,即改善后的简单工厂。
上述便是我们改善后的简单工厂,虽然比刚开始的简单工厂复杂了很多,但是我们的收益是很明显的,现在我们随便加入一个策略,并设置好它的生效区间,策略工厂就可以帮我们自动找到适应的策略。LZ在上面已经加了详细的注释,相信可以帮助各位看懂了。在这里再特别说明一点,我们的策略实现类最好放在一个包中,这样我们可以扫描特定的包,可以加快初始化速度。
有了这个基于注解的简单工厂,我们还需要稍微改变下客户类,因为客户类本来是调用的工厂的静态方法,现在我们将工厂做成了单例,所以应该改成如下形式。
- calPrice = CalPriceFactory.getInstance().createCalPrice(this);
好了,现在各位直接再调用客户端代码,会产生与之前一样的结果,说明我们的策略正确选择了。各位可以自己再试一下,比如到4000就变成四折,然后修改下客户端,测试一下看是否能起效。
现在看来,我们已经使用简单工厂,注解,反射等技术将策略模式优化的非常完美了,我们可以随意新增策略,并且不需要修改原有的任何代码。
但是,其实我们的设计还是有些不完美的,因为它无法支持策略的重叠,这是什么意思呢?
就是说我们同一时间只能采用一种策略,假设我们商店现在有这么一个需求,假设到端午节了,我们商店要采取满1000返200,满2000返400的方式,并且原有的打折还要继续,这就相当于将返现金的活动与打折重叠计算了。
比如我是个金牌会员,假设我买了2000的东西,那么计算方式应该是先减去400为1600,再打五折,为800。最后这个会员只需要付800(靠,这减价有点狠,不过我们只是举个例子,各位不要太在意数字)。
这就相当于将两个策略重叠使用了,我们现在的设计无法支持这种方式。那怎么办?你可能会想,可恶的老板就爱改需求。不过请永远记得,优秀的程序猿面对各种***难的需求,都可以轻松解决,而不是抱怨需求变的太频繁或者太不合理,因为现实会告诉你,抱怨是没用的,而且我们很多时候可以使用一些编程的技巧去容纳这种变化。
刚才的需求,也是在提醒我们在设计一个系统时要考虑全面,我们虽然不应该考虑一些本不存在或者发生概率很小的需求,但像商店或者商场这种灵活的促销方式,却是我们刚开始就应该考虑到的。
现在我们的需求变了,即我们任意的策略都可以随意组合,并且我们要求工厂帮我们自动判断,并将策略叠加返回给我们。那么针对上面的设计我们还需要改善,如果要改善一个设计,我们就需要考虑现有的设计不能支持什么需求。我们考虑上述设计不能支持什么。LZ列出以下两条。
1,我们只能根据客户消费的总金额去处理,而不能根据客户当次消费的金额去处理。
2,我们的设计只能支持单一策略,不能支持策略叠加。
为了满足这两个要求,我们需要添加一个类型的注解,去针对单次消费产生计费策略,另外,我们需要让策略工厂能够产生叠加的策略接口,那么冲着这个目标,我们首先定义如下三个注解,采用嵌套注解。
- package com.calprice;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
-
- @Target(ElementType.ANNOTATION_TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface ValidRegion {
- int max() default Integer.MAX_VALUE;
- int min() default Integer.MIN_VALUE;
-
-
-
- int order() default 0;
- }
我们定义上面这个嵌套注解是为了避免代码的重复,因为这三个属性我们在总额消费的策略注解和单次消费的策略注解中都要包括。下面给出另外两个注解。
- package com.calprice;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
-
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface TotalValidRegion {
-
- ValidRegion value() default @ValidRegion;
-
- }
上述这个总额注解与之前的注解基本一样,直接换成了嵌套注解。下面还有一个一次性消费的注解。如下。
- package com.calprice;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
-
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface OnceValidRegion{
-
- ValidRegion value() default @ValidRegion;
- }
以上三个注解我们就可以支持刚才的第一个要求了,我们可以针对一次消费进行策略判断,接下来我们需要修改策略工厂,去支持单次消费判断,并且还要支持策略重叠。如下。
上面我们改动的地方并不多,主要是添加了一个单次消费的判断,另外就是没有直接返回策略实例,而是将满足条件的策略类信息传递给代理,产生一个代理,从而满足我们第二个要求,即策略可以重叠,下面LZ给出代理类,相信如果各位看过LZ的代理模式,并完全理解了,那么看懂这里面的道理是非常简单的,为此,LZ不在多做解释。代理类如下。
- package com.calprice;
-
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.Method;
- import java.lang.reflect.Proxy;
- import java.util.SortedMap;
-
- public class CalPriceProxy implements InvocationHandler{
-
- private SortedMap<Integer, Class<? extends CalPrice>> clazzMap;
-
- private CalPriceProxy(SortedMap<Integer, Class<? extends CalPrice>> clazzMap) {
- super();
- this.clazzMap = clazzMap;
- }
-
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- Double result = 0D;
- if (method.getName().equals("calPrice")) {
- for (Class<? extends CalPrice> clazz : clazzMap.values()) {
- if (result == 0) {
- result = (Double) method.invoke(clazz.newInstance(), args);
- }else {
- result = (Double) method.invoke(clazz.newInstance(), result);
- }
- }
- return result;
- }
- return null;
- }
-
- public static CalPrice getProxy(SortedMap<Integer, Class<? extends CalPrice>> clazzMap){
- return (CalPrice) Proxy.newProxyInstance(CalPriceProxy.class.getClassLoader(), new Class<?>[]{CalPrice.class}, new CalPriceProxy(clazzMap));
- }
-
- }
好了,这下我们可以支持策略重叠了,我给各位一个指定好的一系列策略,如下。
- package com.calprice;
-
-
- @TotalValidRegion(@ValidRegion(max=1000,order=99))
- class Common implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice;
- }
-
- }
- @TotalValidRegion(@ValidRegion(min=1000,max=2000,order=99))
- class Vip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.8;
- }
-
- }
- @TotalValidRegion(@ValidRegion(min=2000,max=3000,order=99))
- class SuperVip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.7;
- }
-
- }
- @TotalValidRegion(@ValidRegion(min=3000,order=99))
- class GoldVip implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice * 0.5;
- }
-
- }
- @OnceValidRegion(@ValidRegion(min=1000,max=2000,order=40))
- class OneTDTwoH implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice - 200;
- }
-
- }
-
- @OnceValidRegion(@ValidRegion(min=2000,order=40))
- class TwotDFourH implements CalPrice{
-
- public Double calPrice(Double originalPrice) {
- return originalPrice - 400;
- }
-
- }
这里面相比上面,又添了两种策略,即满1000返200和满2000返400,并且优先级高于打折,也就是说会先计算现金返回,再打折。各位可以使用如下客户端测试一下。
- package com.calprice;
-
-
- public class Client {
-
- public static void main(String[] args) {
- Customer customer = new Customer();
- customer.buy(500D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- customer.buy(1200D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- customer.buy(1200D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- customer.buy(1200D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- customer.buy(2600D);
- System.out.println("客户需要付钱:" + customer.calLastAmount());
- }
-
- }
当然你还可以多写几个buy方法,看下我们的策略是否在发挥作用。这下我们的策略模式就更加灵活了,不仅可以支持策略的随意增加,而且还可以重叠。相信这下老板再要搞什么促销活动,都可以轻松应付了吧。当然,没有最好的设计只有更适合的设计,只要可以满足大部分需求,容纳大部分变化就算是很好的设计了。实在容纳和满足不了,我们还可以重构,而且重构往往会比预先设计更加凑效。
本次讲解策略模式算是由浅及深的方式,刚开始给出的是LZ作为一个JAVA新人的时候对策略模式的理解,后面是一个代入了业务场景的例子,以及后面的逐渐使用简单工厂,注解,反射,代理等方式改善我们的策略工厂的过程,算是LZ和各位读者一起进行一个设计思想的锻炼吧。
策略模式本身并不太复杂,实现也比较简单,但是我们却花费了大量的篇幅去完善它,这是因为完善策略模式往往比使用更加复杂。
最后总结一下策略模式的使用场景,就是有一系列的可相互替换的算法的时候,我们就可以使用策略模式将这些算法做成接口的实现,并让我们依赖于算法的类依赖于抽象的算法接口,这样可以彻底消除类与具体算法之间的耦合。
比如我们现在客户类,它就知道有个CalPrice接口可以计算最终价格,其它的它什么都不知道了,这不正是我们之前总纲中提到的最小知道原则吗?
当然策略模式也有缺点,就是我们不停的在各个算法间切换,造成很多逻辑判断,不过我们本章已经给各位提供了思路,我们是可以使用一些其他的模式或者JAVA的技术去消除这种逻辑判断的,对吧?
最最后,感谢各位的收看。
下期预告,适配器模式。