模拟场景:在商场中,都会有一个收银软件,营业人员根据客户购买商品的数量与价格,向客户进行收费。现在不考虑其他要求,只完成以上的要求。
Supermarket
1 package com.zqz.dp.strategy; 2 import java.io.BufferedReader; 3 import java.io.IOException; 4 import java.io.InputStreamReader; 5 /** 6 * @author Qin 超市类,表示进行收费等的功能 7 */ 8 public class Supermarket { 9 public static void main(String[] args){ 10 System.out.println("请输入商品单价:"); //提示信息 11 float productPrice=0.0f; //商品代价 12 int productNum=0; //商品数量 13 String str; //接收键盘的输入 14 BufferedReader buf=new BufferedReader(new InputStreamReader(System.in)); //获得字符输入流,从键盘读取 15 try { 16 str=buf.readLine(); //接收键盘输入 17 productPrice=Float.parseFloat(str); //转换为float 18 System.out.println("请输入商品数量:"); 19 str=buf.readLine(); //接收键盘输入 20 productNum=Integer.parseInt(str); // 转换为int 21 } catch (IOException e) { 22 e.printStackTrace(); 23 } 24 System.out.println("应付金额:"+productPrice*productNum); 25 } 26 }
现在的确完成了操作,但是很遗憾,全部代码都写在了main中,感觉很臃肿,难以维护,而且现在如果要添加打折的操作,是否意味着全部的修改操作都在main中进行吗?显然这是不合适的。
现在抽象出一个Client,主要用来构建超市、顾客的购物、打折策略和收银员的收银。而且我们这里只有一个超市,很明显,单例设计可以拿来用了。
SuperMarket
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 4 * 超市类,表示进行收费等的功能 5 */ 6 public class SuperMarket { 7 private static final SuperMarket superMarket=new SuperMarket(); //饿汉式,静态产生一个SuperMarket对象 8 private SuperMarket(){} //构造方法私有化 9 public static SuperMarket getInstance(){ //静态方法,取得SuperMarket实例 10 return superMarket; //返回superMarket实例 11 } 12 public double sale(double productPrice,int productNum){ //卖出商品 13 System.out.println("卖出价格为"+productPrice+"的商品"+productNum+"件"); //反馈 14 return productPrice*productNum; //返回应收价格 15 } 16 public void getMoney(double totalPrice){ //应收总额 17 System.out.println("应收金额为:"+totalPrice); 18 } 19 }
Client
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 客户端, 4 * 主要完成超市的建立,打折的策略,顾客的购物和营业员的收银 5 */ 6 public class Client { 7 private static double totalPrice = 0.0f; // 商品总额 8 private static SuperMarket superMarket=SuperMarket.getInstance(); //单例取得超市实例 9 public static void main(String[] args) { 10 totalPrice+=superMarket.sale(12.5, 3); //超市卖出一件商品 11 totalPrice+=superMarket.sale(13.5, 2); //超市卖出一件商品 12 superMarket.getMoney(totalPrice); //超市应收总价格 13 } 14 }
现在把类抽出来之后,发现代码不那么臃肿,也便于维护了。只是现在超市实行打折策略,所有商品一律八折,那该怎么实现呢?不能说在superMarket的sale进行修改吧?这样就违背了开闭原则,而且我的打折活动是时时改动的,比如今天一律八折,明天消费满300减100,后天A商品打八折,B商品满300减100,那这样修改的话不是全乱套了?
可以说现在有三种打折策略,第一种是正常收费,第二种是打八折,第三种是满300减100;所有的打折都是对总价格进行对应的操作。所以可以抽象出一个接口。该接口只有一个作用,就是进行打折的操作。
IStrategy
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 4 * 打折策略 5 */ 6 public interface IStrategy { 7 double disCount(double totalPrice); //卖出商品,根据不同的打折策略返回不同的money 8 }
现在还需要定义三个具体的打折策略。正常收费最简单了,没有折扣就表示直接把价格返回即可。打N折的就把商品总额乘以折扣就行了。满300减100需要先判断商品总额是否有达到最低消费的要求,有的话才进行操作。
NoneStrategy
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 4 * 没有打折的策略,也就是正常收费 5 */ 6 public class NoneStrategy implements IStrategy { 7 @Override 8 public double disCount(double totalPrice) { 9 return totalPrice; //没有打折,直接返回总额 10 } 11 }
DisCountStrategy
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 4 * 打折的策略,具体折扣外部决定 5 */ 6 public class DisCountStrategy implements IStrategy { 7 private double disCount; //折扣 8 public DisCountStrategy(double disCount) { //实例化的时候指定折扣 9 this.disCount = disCount; 10 } 11 @Override 12 public double disCount(double totalPrice) { 13 return totalPrice*disCount; //直接返回总额乘以折扣 14 } 15 }
ReturnStrategy
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 4 * 满额减钱的折扣,满几减几由外部决定 5 */ 6 public class ReturnStrategy implements IStrategy { 7 private double totalMoney; //需要满多少才有优惠 8 private double returnMoney; //满足要求的后返回多少 9 public ReturnStrategy(double totalMoney, double returnMoney) { //构造方法赋值,初始化时赋值 10 this.totalMoney = totalMoney; 11 this.returnMoney = returnMoney; 12 } 13 @Override 14 public double disCount(double totalPrice) { 15 double returnResult = 0; //定义一个结果,表示根据优惠应该减去的价格 16 if(totalPrice>this.totalMoney){ //购买商品的总额大于活动的最低限消费 17 returnResult=Math.floor(totalPrice/totalMoney)*returnMoney; //math.floor表示取出里面的总数部分 18 } 19 return totalPrice-returnResult; //减去对应的金额 20 } 21 }
修改的SuperMarket
1 package com.zqz.dp.strategy.copy; 2 /** 3 * @author Qin 超市类,表示进行收费等的功能 4 */ 5 public class SuperMarket { 6 private static final SuperMarket superMarket = new SuperMarket(); // 饿汉式,静态产生一个SuperMarket对象 7 private SuperMarket() { 8 } // 构造方法私有化 9 public static SuperMarket getInstance(IStrategy s) { // 静态方法,取得SuperMarket实例 10 strategy=s; //指定赋值的策略 11 return superMarket; // 返回superMarket实例 12 } 13 private static IStrategy strategy; // 声明一个打折策略 14 public double sale(double productPrice, int productNum) { // 卖出商品 15 double totalPrice=productPrice*productNum; //定义一个商品总额,未打折前为两数之乘积 16 totalPrice=strategy.disCount(totalPrice); //进行打折的操作,返回打折之后的总额 17 System.out.println("卖出价格为" + productPrice + "的商品" + productNum + "件,总额为"+totalPrice); // 反馈 18 return totalPrice; // 返回应收价格 19 } 20 public void getMoney(double totalPrice) { // 应收总额 21 System.out.println("应收金额为:" + totalPrice); 22 } 23 }
Client
1 package com.zqz.dp.strategy.copy; 2 /** 3 * @author Qin 客户端, 4 * 主要完成超市的建立,打折的策略,顾客的购物和营业员的收银 5 */ 6 public class Client { 7 private static double totalPrice = 0.0f; // 商品总额 8 private static SuperMarket superMarket=null; //声明超市实例 9 public static void main(String[] args) { 10 superMarket=SuperMarket.getInstance(new NoneStrategy()); //单例取得超市实例,并指明正常收费,没有打折 11 System.out.println("=========今天没有活动============"); 12 totalPrice+=superMarket.sale(12.5, 3); //超市卖出一件商品 13 superMarket=SuperMarket.getInstance(new DisCountStrategy(0.8)); //单例取得超市实例,并指明打八折 14 System.out.println("=======今天所有商品打八折=========="); 15 totalPrice+=superMarket.sale(13.5, 2); //超市卖出一件商品 16 superMarket=SuperMarket.getInstance(new ReturnStrategy(300,100)); //单例取得超市实例,并指明消费满300减100 17 System.out.println("=====今天所有商品满300减100======="); 18 totalPrice+=superMarket.sale(15, 2); //超市卖出一件商品 19 superMarket.getMoney(totalPrice); //超市应收总价格 20 } 21 }
以上就是策略模式的体现。在Client端,我只需要改变不同的策略,就能实现不同的打折活动的要求。
策略模式的定义:定义一组算法,将每个算法都封装起来,并且可以随意的切换算法。
以上的操作不是很好的说明了这一点吗?根据不同的策略,我应用了不同的打折活动。
策略模式有如下三个角色
1、Context封装角色:也叫上下文角色,起着承上启下的作用。屏蔽Client端对策略、算法的直接访问。如上面的SuperMarket
2、Strategy抽象的策略角色:一般的策略、算法的父接口。定义每个策略或算法必备的方法。如上面的IStrategy
3、ConcreteStrategy具体的策略角色:具有具体算法的策略。如上面的ReturnStrategy等。
现在明白了什么是策略模式之后,有没有发现策略模式和代理、装饰模式两个模式有很大的相同点呢?
因为都是在客户端中把一个对象的引用传递给另一个对象,所以看起来三者好像是一样的。但事实上是这样吗?
先来谈一下各个模式
代理模式:是指对对象的访问进行控制。
装饰模式:是指对对象进行装饰,动态的对一个对象添加逻辑。
策略模式:是指定义一组算法,将算法封装起来,在对对象的访问时可以进行随意的切换算法。
代理和装饰中,真实实现类与代理类(装饰类)实现的是同一个接口,或者继承统一个抽象类(在之前的装饰模式中,虽然没有实现同一个接口或抽象类,但是却从实现类中继承,所以是一样的道理的)。而在策略中,并没有实现同一接口或者继承同一父类。
在jdk中,进行排序的时候就是使用策略模式。在我们进行Arrays.sort(Object)时,Object必须要实现Comparable接口,因为在sort()方法里面就是调用了comparable的comparaTo方法。
所以可以根据具体的情况来判断使用何种模式。其实很多模式是可以混合使用的,这样会让模式之间互补。
策略模式的优点:
1、 算法之间可以随意的切换。
2、 可以避免多重条件判断。
3、 扩展性好,如果现在要扩展新的打折策略,只需要实现IStrategy就好了。
策略模式的缺点:
1、 策略的类数量太多:每个策略是一个类,而这些类的功能单一,无扩展性。
2、 所有的策略类都要对外暴露:上面的操作中,superMarket每次都要暴露他的策略给Client知道。
策略模式的使用场景:
1、 多个类只有在算法或者行为上稍有不同的场景;
2、 需要自由切换算法的场景;
3、 需要屏蔽算法的场景;
与工厂模式的结合:
在上面的操作中,会发现在Client端暴露了太多的策略了,而且在建立SuperMarket的时候就指定了策略似乎也说不过去,因为这里是针对不同的商品有不同的打折策略,所以按理来说应该是在购买某一件商品的时候指明一个策略就行了。
而且不应该在有顾客进入超市的时候就得知某件商品有某种打折策略吧,而是在顾客准备购买商品的时候,引购员才会告诉你有优惠对吧。
在这里为了简便起见,我们在购买商品的时候传递一个int参数,表示由何种优惠。然后引用工厂模式,父类引用指向子类对象。
修改的SuperMarket
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 超市类,表示进行收费等的功能 4 */ 5 public class SuperMarket { 6 private static final SuperMarket superMarket = new SuperMarket(); // 饿汉式,静态产生一个SuperMarket对象 7 private SuperMarket() { 8 } // 构造方法私有化 9 public static SuperMarket getInstance() { // 静态方法,取得SuperMarket实例 10 return superMarket; // 返回superMarket实例 11 } 12 private IStrategy strategy; // 声明一个打折策略 13 public double sale(double productPrice, int productNum, int num) { // 卖出商品,num是代表打折的策略 14 String status="今天没打折活动"; //表示记录状态,是否有打折活动,默认没有 15 double totalPrice=productPrice*productNum; //定义一个商品总额,未打折前为两数之乘积 16 switch (num) { 17 case 0: 18 strategy=new NoneStrategy(); //表示今天没有打折活动 19 break; 20 case 1: 21 strategy=new DisCountStrategy(0.8); //表示该商品今天有打折操作 22 status="该商品今天打八折"; 23 break; //跳出循环 24 case 2: 25 strategy=new ReturnStrategy(300, 100); //满300减100 26 status="该商品今天消费满300减100"; 27 break; 28 default: 29 break; 30 } 31 totalPrice=strategy.disCount(totalPrice); //进行打折的操作,返回打折之后的总额 32 System.out.println("卖出价格为" + productPrice + "的商品" + productNum + "件,"+status+",总额为"+totalPrice); // 反馈 33 return totalPrice; // 返回应收价格 34 } 35 public void getMoney(double totalPrice) { // 应收总额 36 System.out.println("应收金额为:" + totalPrice); 37 } 38 }
修改的Client
1 package com.zqz.dp.strategy; 2 /** 3 * @author Qin 客户端, 4 * 主要完成超市的建立,打折的策略,顾客的购物和营业员的收银 5 */ 6 public class Client { 7 private static double totalPrice = 0.0f; // 商品总额 8 private static SuperMarket superMarket=SuperMarket.getInstance(); //单例取得超市实例 9 public static void main(String[] args) { 10 totalPrice+=superMarket.sale(12.5, 3,0); //卖出一件商品,正常收费 11 totalPrice+=superMarket.sale(13.5, 2,1); //卖出一件商品,打八折 12 totalPrice+=superMarket.sale(15, 2,2); //卖出一件商品,满300减100 13 superMarket.getMoney(totalPrice); //超市应收总价格 14 } 15 }
现在上面用switch继续了判断,client端的代码是否减少了好多,感觉清晰了好多。所以在实际应用中,不要拘泥于使用某种模式,而是要结合使用。