【代码洁癖症】第2回-策略模式
序言
在一个宁静的午后,我有幸拜读了程杰大鸟的《大话设计模式》
觉得这是一本不可多得的好书
奈何里面都是C++代码写的示例,对于学Java的同学不是很友好
于是想将书中的核心提炼出来并结合Java示例与大家分享
并且加入一些我曾在生产环境下的应用来“学以致用”
这是第一次开始写CSDN专栏,内容会持续更新,感兴趣的小伙伴可以来个三连
本人水平有限,难免会有不足之处,希望大佬们不吝赐教!
文章目录
面试第二轮
前情回顾
张三顺利通过了第一轮面试,也步入了设计模式的大门
通过几次修改自己代码结构
让自己的代码变得更容易扩展和维护
今天,他西装革履又走进了面试官的办公室
面试官: “张三你好,我们又见面了”
张三: “废话少说,我们开始面试吧,这次是什么题目?”
面试官: “我们公司今天来了个大客户——沃尔玛,要我们公司给他们开发一个收银程序,这样吧,你写个简单的收银程序给我看看”
张三: “So easy!别急面试官,我这就开始写”
张三第一版商场收银程序
public class Market01 {
private static double totalPrice = 0.0;
public static void main(String[] args) {
while (true){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入单价(元)");
String singlePrice = scanner.nextLine();
System.out.println("请输入数量");
String count = scanner.nextLine();
totalPrice += Double.valueOf(singlePrice)*Double.valueOf(count);
System.out.println("总价为:"+totalPrice+"元");
}
}
}
张三:“面试官你看,我的程序还能不断累加总价”
面试官笑了一下
面试官: “现在商场搞活动,所有商品打8折,你怎么办?”
张三: “这还不简单,每次计算总价之前都乘上一个0.8就OK了,看我的”
修改后的商场收银程序
public class Market01 {
private static double totalPrice = 0.0;
public static void main(String[] args) {
while (true){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入单价(元)");
String singlePrice = scanner.nextLine();
System.out.println("请输入数量");
String count = scanner.nextLine();
/*累加之前乘以0.8(打八折)*/
totalPrice += (Double.valueOf(singlePrice)*Double.valueOf(count))*0.8;
System.out.println("总价为:"+totalPrice+"元");
}
}
}
效果图
面试官: “后面的‘000004’和‘000005’是什么鬼?”
张三: “这个我知道,这是Java中的精度丢失问题”
面试官: “你简单说说”
张三: “精度丢失是因为十进制的 double 类型的数据在进行计算的时候,计算机会先将其转换为二进制数据,然后再进行相关的运算然,而在十进制转二进制的过程中,有些十进制数是无法使用一个有限的二进制数来表达的,换言之就是转换的时候出现了精度的丢失问题,所以导致最后在运算的过程中,自然就出现了我们看到的一幕”
面试官: “回答得很好,那我们怎么去解决这个问题呢?”
张三: “BigDecimal ”
面试官: “回答得很好,我们这个问题就不深究了,来说说你写的程序: 万一商场活动结束了怎么办?难道还要改一遍代码?还有如果打七折、六折、五折怎么办?”
张三: “这个难不倒我”
增加了打折选项的商城收银程序
public class Market01 {
private static double totalPrice = 0.0;
private static double discount = 1.0;//默认不打折
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请选择折扣");
System.out.println("0.不打折");
System.out.println("1.八折");
System.out.println("2.七折");
System.out.println("3.六折");
System.out.println("4.五折");
String choice = scanner.nextLine();
switch (choice){
case "0":
// do nothing
break;
case "1":
discount = 0.8;
break;
case "2":
discount = 0.7;
break;
case "3":
discount = 0.6;
break;
case "4":
discount = 0.5;
break;
default:
System.out.println("输入错误!");
System.exit(-1);
}
while (true){
System.out.println("请输入单价(元)");
String singlePrice = scanner.nextLine();
System.out.println("请输入数量");
String count = scanner.nextLine();
/*累加之前乘以打折系数*/
totalPrice += (Double.valueOf(singlePrice)*Double.valueOf(count))*discount;
System.out.println("总价为:"+String.format("%.2f",totalPrice)+"元");
}
}
}
运行效果
面试官: “好,现在商场又新加了一个需求: 又增加了满减活动,满300减100,满500减200,这个怎么办?”
张三欲暴起而伤人
面试官: “息怒息怒,这样吧,后半场我来写,首先我们用第一次面试时教你的简单工厂改造下你的代码”
简单工厂
“首先我们要知道在面向对象编程中,并不是类越多越好,类的划分是为了更好的封装,但是封装的基础就是抽象,只有对事物进行一个抽象才能分好类,我们将具有相同属性和功能的对象的抽象集合叫做类”
具体来说,就是打一折和九折只是形式不同,抽象分析后,优惠的方式(算法)应该是一个类。
现金收费抽象类CashSuper
public abstract class CashSuper {
/**
* @Description TODO 实际收取现金算法
* @author LaoQin
* @date 2020/03/13
* @param money 原价
* @return double 当前价
*/
public abstract double acceptCash(double money);
}
正常收费子类CashNormal
public class CashNormal extends CashSuper {
/**
* @Description TODO 原价收费
* @author LaoQin
* @date 2020/03/13
* @param money
* @return double
*/
@Override
public double acceptCash(double money) {
return money;
}
}
打折收费子类CashRebate
public class CashRebate extends CashSuper{
private double rebate = 1.0;
public CashRebate(double rebate){
this.rebate = rebate;
}
/**
* @Description TODO 打折后返回打折金额
* @author LaoQin
* @date 2020/03/13
* @param money
* @return double
*/
@Override
public double acceptCash(double money) {
return money*rebate;
}
}
满减收费子类CashReturn
public class CashReturn extends CashSuper{
private double moneyCondition = 0.0;//满
private double moneyReturn = 0.0;//减
public CashReturn(double moneyCondition, double returnCondition) {
this.moneyCondition = moneyCondition;
this.moneyReturn = returnCondition;
}
/**
* @Description TODO 满减
* @author LaoQin
* @date 2020/03/13
* @param money
* @return double
*/
@Override
public double acceptCash(double money) {
if(money>moneyCondition){//满足条件才能满减
return money-moneyReturn;
}
return money;
}
}
简单收费工厂CashFactory
public class CashFactory {
/**
* @param type 优惠分类
* @return CashSuper
* @Description TODO 现金收取工厂
* @author LaoQin
* @date 2020/03/15
*/
public static CashSuper createCashAccept(String type) {
CashSuper cashSuper = null;
switch (type) {
case "正常收费":
cashSuper = new CashNormal();
break;
case "满300减100":
cashSuper = new CashReturn(300, 100);
break;
case "8折":
cashSuper = new CashRebate(0.8);
break;
default:
throw new RuntimeException("输入错误");
}
return cashSuper;
}
}
测试类FactoryTestApplication
import java.util.Scanner;
public class FactoryTestApplication {
/**
* @Description TODO 测试简单工厂实现
* @author LaoQin
* @date 2020/03/15
* @param args
* @return void
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入金额");
double money = Double.valueOf(scanner.nextLine());
System.out.println("请选择折扣类型:");
System.out.println("正常收费");
System.out.println("满300减100:");
System.out.println("8折");
String type = scanner.nextLine();
//创建对应收费子类
CashSuper cashAccept = CashFactory.createCashAccept(type);
double result = cashAccept.acceptCash(money);
System.out.println("优惠后金额为:"+result);
}
}
运行结果如图
面试官: “但是简单工厂只是为了解决对象创建的问题,商场如果要更改打折额度和返利额度就会频繁改动这个工厂,显然这不是最好的处理方式”
“我们设计模式可以大致分为以下4类”
- 创建型
- 结构型
- 行为型
- J2EE型
而工厂模式显然是属于创建型的,而我们这个问题显然不是一个创建型的问题,而是“行为型”,根据商场运营战略去动态调整我们的打折策略,所以这种场景比较适合的是“策略模式”
策略模式
什么是策略模式?
策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。
张三: “Talk is cheap,show me the code”
面试官: “先别急,我们来看下策略模式的UML图”
首先有个策略父类,然后有几个不同算法类去继承并实现各自算法,乍一看和简单工厂很像
他们的区别就在于策略模式还是基于继承,但是却少了工厂类(因为不是创建型模式)
其核心就在于用一个Context(上下文)去管理维护一个对象的引用
张三: “说人话”
面试官: “概念有点抽象,我们直接看代码”
Context上下文管理类
public class Context {
private CashSuper cashSuper;
public Context(CashSuper cashSuper) {//构造方法传入具体收费策略
this.cashSuper = cashSuper;
}
public double getResult(double money){//根据不同收费策略返回不同计算结果
return cashSuper.acceptCash(money);
}
}
这样我们就能忽略算法具体实现而直接调用
而我们就能使用以下代码去调用:
打8折调用示例
/**
* @Description TODO 策略模式下打8折调用示例
* @author LaoQin
* @date 2020/03/15
* @param args
* @return void
*/
public static void main(String[] args) {
Context context = new Context(new CashRebate(0.8));
double result = context.getResult(100);
System.out.println("打折后的金额为:"+result);
}
输出结果
打折后的金额为:80.0
面试官: “策略模式实现其实很简单,就是运用了OOP(面向对象程序设计)中继承特性,然后通过一个Context类的构造方法传入父类去实例化一个子类对象,调用其方法”
既然知道了策略模式,那么我们将 简单工厂和策略模式结合起来 实现
张三: “这么一说我就明白了,剩下的改造交给我来吧”
改造后的Context(加入工厂)
public class Context {
private CashSuper cashSuper;
public Context(String type) {//这里特别注意:传入的不再是一个对象,而是收费类型字符串
switch (type) {
case "正常收费":
cashSuper = new CashNormal();
break;
case "满300减100":
cashSuper = new CashReturn(300, 100);
break;
case "8折":
cashSuper = new CashRebate(0.8);
break;
default:
throw new RuntimeException("输入错误");
}
}
public double getResult(double money){//根据不同收费策略返回不同计算结果
return cashSuper.acceptCash(money);
}
}
注意和改动的点都写到代码注释里了
调用示例
/**
* @Description TODO 简单工厂+策略模式测试方法
* @author LaoQin
* @date 2020/03/15
* @param args
* @return void
*/
public static void main(String[] args) {
Context context = new Context("8折");
double result = context.getResult(100);
System.out.println("打折后的金额为:"+result);
}
这样我们会发现两种方式结合后我们在调用的时候看不到CashSuper和相关子类的影子
简单工厂会让客户端认识两个类CashSuper和CashFactory
而两种结合后客户端只需要认识Context一个类就行了
这样就降低了我们程序的耦合度
面试官: “张三你说的很好,我们继续深入解析一下‘策略模式’”
"回过头来反思一下 策略模式,策略模式是一种定义一系列算法的方法,从概念上来看,所有这些
算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类
与使用算法类之间的耦合[DPE]。”大鸟总结道。
“策略模式还有些什么优点?”小菜问道。
“策略模式的Strategy 类层次为Context 定义了-系列的可供重用的算法或行为。继承有助于析取
出这些算法中的公共功能[DP]。对于打折、返利或者其他的算法,其实都是对实际商品收费的一一种计算
方式,通过继承,可以得到它们的公共功能,你说这公共功能指什么?”
“公共的功能就是获得计算费用的结果GetResult,这使得算法间有了抽象的父类CashSuper。”
“对,很好。另外-一个策略模式的优点是简化了单元测试,因为每个算法都有自己的类,可以通过
自己的接口单独测试[DPE]。”
“每个算法可保证它没有错误,修改其中任-一个时也不会影响其他的算法。这真的是非常好。”——摘自《大话设计模式》
张三通过面试
张三: “那我这算过了吗?”
面试官: “虽然你对设计模式不是很了解,但是你的学习能力还是很强的,综合考虑,给你通过吧!”
张三: “太好了,那我需要准备什么呢?”
面试官: “明天过来填个入职申请,回去看一看‘单一职责原则’,明天到公司提交一份学习报告”
张三: 好嘞!
-完-
下期预告:《张三的学习报告——单一职责原则》
注: 专栏《代码洁癖症》所有代码均已同步至github,详情请访问github主页