策略模式
俗话说:条条大路通罗马。在很多情况下,实现某个目标的途径不止一条,例如我们在外出旅游时可以选择多种不同的出行方式,如骑自行车、坐汽车、坐火车或者坐飞机,可根据实际情况(目的地、旅游预算、旅游时间等)来选择一种最适合的出行方式。即怎么来不重要,人到达目的地就行了!
在软件开发中,我们也常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。即为了适应算法灵活性而产生的设计模式——策略模式
在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可
常规思路下我们是将一种行为写成一个类方法,比如计算器类中有加、减、乘、除四种方法,而策略模式则是将每一种算法都写成一个类,然后动态的选择使用哪一个算法。
实际代码很简单,具体如下:
-
Calculate 实现类--加法和减法:
public class Calculator { public int add(int a, int b){ return a+b; } public int subtract(int a,int b){ return a-b; } }
-
main 方法:
public static void main(String[] args) { Calculator calculator = new Calculator(); int result =calculator.add(1,2); System.out.println(result); }
从上图可以看到,我们将操作封装到类中,他们实现了同一个接口,然后在 Context 中调用.
add() 方法封装到 OperationA类中,subtract() 方法封装到 OperationB() 类中.即为加法和减法分别创建了一个类,实现一个共同的接口.
**类比交通出行就是:我们可以选择不同的运算规则去得出最终结果,可以加,可以减,可以乘,可以除,(例如最终结果为5,那么可以是 3+2=5,也可以是8-3=5)选用什么运算规则不重要,重要的是得到我们需要的最终结果。**我们可以根据自己的喜好决定使用什么运算规则,选用不同的运算规则就相当于选择了不同的策略,所以这个模式叫策略模式。
实际代码也很简单,具体如下:
-
Operation 接口:
public interface Operation { public int doOperation(int a,int b); }
-
两个实现类——加法和减法
public class OperationAdd implements Operation{ @Override public int doOperation(int a, int b) { return a+b; } }
public class OperationSubtract implements Operation { @Override public int doOperation(int a, int b) { return a-b; } }
-
计算类
public class Calculator { private Operation operation; public void setOperation(Operation operation) { this.operation = operation; } public int doOperation(int a, int b){ return operation.doOperation(a,b); } }
-
main 方法
public static void main(String[] args) { Calculator calculator = new Calculator(); calculator.setOperation(new OperationAdd()); int result = calculator.doOperation(3,4); System.out.println(result); }
用策略模式使用计算器类时,如果要进行加法运算,就 new 一个加法类传入,减法也是同理。因此要改动的代码永远只有一行,即:
calculator.setOperation(new OperationAdd());
,只要改变setOperation里传入的参数即可。
看到这里,相信大家一定会有疑惑,为什么要把加、减、乘、除四则运算分别封装到类中?直接使用常规思路在 Calculator 中写 add()、subtract() 等方法不是更方便吗?
甚至如果要添加其他的运算方法,每次都要创建一个类,反而更麻烦。
的确用了策略模式之后代码比普通写法多了一些,但是这里假设一种场景:
假设把写好的计算器代码打包好作为一个库发布出去给其他人用,其他人发现你的计算器中只有加、减、乘、除四个方法,而他想增加平方、开方等功能,咋办?
如果是用普通写法写的计算器,想要增加功能唯一的办法就是修改你写好的 Calculator,增加平方和开方两个 method。
可是你提供的是一个 jar 包啊,jar 包,jar...jar...jar...jar...包……
就算你提供的是源码,你希望其他人可以随意的修改你写好的代码吗?一般我们发布出去的开源框架或库都是经过千锤百炼,经过测试的代码,其他人随意修改我们的源码很容易产生不可预知的错误。
如果你用的是策略模式,那么其他人想要增加平方或开平方功能,只需要自己定义一个类实现你的 Operation 接口,然后调用 calculator.setOperation(new 平方类()); 即可。
看到这里相信你已经对策略模式有了一定的好感,甚至惊叹一声:哇,还有这种操作?
顺便提一嘴,这里很好的体现了一个设计模式的基本原则:开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
Strategy模式中的登场角色,在设计策略模式时要找到并区分这些角色:
- Strategy(策略):策略(算法)的抽象类,定义统一的接口,规定每一个子类必须实现的方法。在示例程序中就是 Operation 接口
- ConcreteStrategy(具体的策略):策略的具体实现者,可以有多个不同的(算法或规则)实现。在示例程序中就是 OperationAdd 和 OperationSubtract 这两个实现类。
- Context(具体的策略):起着承上启下的封装作用,屏蔽上层应用对策略(算法)的直接访问,封装可能存在的变化。在示例程序中就是 Calculator 类。
-
案例1:
实现上文的交通出行
-
Vehicle (车辆)接口:
public interface Vehicle { public void arrival(); }
-
两个实现类--骑共享单车和开私家车
public class SharedBicycle implements Vehicle{ @Override public void arrival() { System.out.println("我骑共享单车去");; } }
public class Car implements Vehicle { @Override public void arrival() { System.out.println("我开车去"); } }
-
Travel 旅游类
public class Travel { private Vehicle vehicle; public void setVehicle(Vehicle vehicle) { this.vehicle = vehicle; } public void advent(){ vehicle.arrival(); } }
-
main 方法
public class test { public static void main(String[] args) { Travel travel = new Travel(); travel.setVehicle(new Car()); travel.advent(); } }
示例类图如下:
-
-
案例2:
电影票打折方案:
Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:
(1) 学生凭学生证可享受票价8折优惠;
(2) 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);
(3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。
(4)该系统在将来可能还要根据需要引入新的打折方式。
常规方案:
-
MovicTicket 类
public class MovieTicket { private double price; private String type; public MovieTicket() { } public MovieTicket(double price, String type) { this.price = price; this.type = type; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public String getType() { return type; } public void setType(String type) { this.type = type; } public double calculatePrice(){ if("students".equals(type)){ System.out.println("学生票"); price = price*0.8; } if("children".equals(type) && price>=20){ System.out.println("儿童票"); price = price-10; } if("vip".equals(type)){ System.out.println("vip,增加积分!"); price = price*0.5; } return price; } }
-
main 方法
public static void main(String[] args) { MovieTicket movieTicket = new MovieTicket(80,"students"); double price = movieTicket.calculatePrice(); System.out.println(price); }
常规思路至少至少存在如下三个问题:
(1) MovieTicket类的calculatePrice()方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护。
(2) 增加新的打折算法或者对原有打折算法进行修改时必须修改MovieTicket类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。
(3) 算法的复用性差,如果在另一个系统(如商场销售管理系统)中需要重用某些打折算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法(重用较为麻烦)。
如何解决这三个问题?导致产生这些问题的主要原因在于MovieTicket类职责过重,它将各种打折算法都定义在一个类中,这既不便于算法的重用,也不便于算法的扩展。因此我们需要对MovieTicket类进行重构,将原本庞大的MovieTicket类的职责进行分解,将算法的定义和使用分离,这就是策略模式所要解决的问题。
策略模式:
-
Discount 接口:
public interface Discount { public double calculate(double price); }
-
三个实现类——ChildrenDiscount,StudentDiscount,VipDiscount
public class ChildrenDiscount implements Discount{ @Override public double calculate(double price) { System.out.println("儿童票"); price = price - 10; return price; } }
public class StudentDiscount implements Discount{ @Override public double calculate(double price) { System.out.println("学生票"); price = price * 0.8; return price; } }
public class VipDiscount implements Discount{ @Override public double calculate(double price) { System.out.println("Vip票"); price = price*0.5; return price; } }
-
Context 类
public class MovieTicket { private Discount discount; public void setDiscount(Discount discount) { this.discount = discount; } public double calculatePrice(double price){ return this.discount.calculate(price); } }
-
main 方法
public static void main(String[] args) { MovieTicket movieTicket = new MovieTicket(); movieTicket.setDiscount(new StudentDiscount()); double price = movieTicket.calculatePrice(30); System.out.println(price); }
-
示例类图如下:
看完这两个案例代码,你会发现跟计算器惊人的相似,没错,所谓设计模式就是前人总结出来的武功套路,经常可以直接套用。当然也要灵活的根据实际情况进行修改,设计模式想要传达给我们的更多的是一种编程思想。策略模式的模板可于参考链接获取!
movieTicket.setDiscount(new StudentDiscount());
在这里 new 一个学生折扣对象,如果系统在将来升级引入新的打折方式,那么其他开发者引入新的打折方式还要修改这行代码,new 开发者自定义的打折方式对象。根据开闭原则,我们不希望其他人修改我们的任何一行代码,否则拔刀相见。因此可以将打折方式的包名和类名写到配置文件中,利用 java 的反射机制动态生成打折方式对象,因此更换打折方式也只要修改配置文件即可。详情参考链接!
策略模式的优点:
- 算法(规则)可自由地切换。
- 避免使用多重条件判断。
- 方便拓展和增加新的算法(规则)。
- 遵循了开闭原则,扩展性良好
策略模式的缺点:所有策略类都需要对外暴露。如果你在实际开发中使用了策略模式,一定要记得写好文档让你的伙伴们知道已有哪些策略,否则根本不知道如何使用。
参考链接: