人物:小菜,大鸟
事件:做一个商场收银软件,营业员根据客户所购买的商品的单价和数量,向客户收费
策略模式:
1.大鸟让小菜做一个小软件,能输入单价和数量,计算总价,小菜第一次很粗糙地完成了
2.大鸟指出了小菜软件的不可扩展性,不灵活等缺点,然后小菜回想着上次学习的简单工厂模式,接着进行了小菜的第二次实现
3.大鸟又指出频繁的策略变动可能会导致重复的代码上线,让小菜继续改进,于是在大鸟的引导下,开始先了解起了策略模式
4.在了解了策模式后,小菜将策略模式融入软件,又发现又得在客户端进行判断(这个缺点具体参考简单工厂模式一章)
5.小菜最后将简单工厂模式和策略模式相结合,完成了设计与实现
6.小结了策略模式
小菜初次尝试
用两个文本框输入单价和数量,一个确定按钮算出每种商品费用,列表框记录商品清单,一个标签记录总计,最后加上一个重置按钮,关键代码如下:
@Slf4j public class CashierSystem { BigDecimal totalPrice = BigDecimal.ZERO; private BigDecimal getTotalPrice(BigDecimal price, BigDecimal num) { totalPrice = totalPrice.add(price.multiply(num)).setScale(2, RoundingMode.HALF_UP); return totalPrice; } public static void main(String[] args) { CashierSystem result = new CashierSystem(); BigDecimal applePrice = new BigDecimal("2"); BigDecimal appleNumber = new BigDecimal("5"); result.getTotalPrice(applePrice, appleNumber).toString(); BigDecimal peachPrice = new BigDecimal("6.6"); BigDecimal peachNumber = new BigDecimal("6"); String resultPrice = result.getTotalPrice(peachPrice, peachNumber).toString(); log.info("总价为:{}", resultPrice); } }
大鸟:那现在商场要求对商品搞活动,所有商品打八折
小菜:那在最后的totalPrice乘以0.8不就行了?
大鸟:那这样不是活动完了,还要把代码再改一遍?
小菜:那我增加一个下拉框,可以选择是打8折还是原价
小菜尝试将可能打折的内容全部列出:
public enum DiscountTypeEnum { NO_DISCOUNT(BigDecimal.ONE, "原价"), DISCOUNT_EIGHTY(new BigDecimal("0.8"), "打八折"), DISCOUNT_HALF(new BigDecimal("0.5"), "打半折"); private BigDecimal code; private String message; DiscountTypeEnum(BigDecimal code, String message) { this.code = code; this.message = message; } public BigDecimal getCode() { return code; } public String getMessage() { return message; } }
然后前端页面只要选了相应的折扣,就能直接在totalPrice上做打折处理:
@Slf4j public class CashierSystem { BigDecimal totalPrice = BigDecimal.ZERO; private BigDecimal getTotalPrice(BigDecimal price, BigDecimal num) { totalPrice = totalPrice.add(price.multiply(num)).setScale(2, RoundingMode.HALF_UP); return totalPrice; } public static void main(String[] args) { CashierSystem result = new CashierSystem(); BigDecimal peachPrice = new BigDecimal("6.6"); BigDecimal peachNumber = new BigDecimal("6"); String resultPrice = result.getTotalPrice(peachPrice, peachNumber) .multiply(DiscountTypeEnum.DISCOUNT_EIGHTY.getCode()) .toString(); log.info("总价为:{}", resultPrice); } }
大鸟:但是你列出的打折可能性有限,如果又出现满300返100,满700返300的活动,那就不只是做乘法了,肯定还会改变原来代码逻辑的,那又该怎么办?
小菜:那用简单工厂模式,根据需求,子类有几个写几个,如:打八折,打半折,满300返100等,都写上
大鸟:用设计模式的时候先想想,怎么用合理,难道后面打三折还要再加一个子类?要知道哪些是同一类型的,哪些是不同的
小菜用简单工厂模式的尝试:
先划分子类的类型,现在可以区分为三种,一种是正常售卖,一种是打折类型,直接初始化参数即可,最后一种是满减,用两个参数做传参即可:
现金收费抽象类:
public abstract class AbstractCashierSystem { public abstract BigDecimal acceptCash(BigDecimal money); }
原价收费子类:
public class CashNormal extends AbstractCashierSystem { @Override public BigDecimal acceptCash(BigDecimal money) { return money; } }
打折收费子类:
@Data public class CashRebate extends AbstractCashierSystem { private String discountType; @Override public BigDecimal acceptCash(BigDecimal money) { //从客户端传来的discountType,这里举例:"NO_DISCOUNT" String discountType = "NO_DISCOUNT"; return money.multiply(DiscountTypeEnum.valueOf(discountType).getCode()); } CashRebate(String discountType) { this.discountType = discountType; } }
返利收费子类:
@Data public class CashReturn extends AbstractCashierSystem { private BigDecimal moneyCondition; private BigDecimal moneyReturn; @Override public BigDecimal acceptCash(BigDecimal money) { BigDecimal result = money; if (money.compareTo(moneyCondition) > 0) { result = money.subtract( money.divide(moneyCondition, 4, RoundingMode.HALF_UP).multiply(moneyReturn) ); } return result; } public CashReturn(BigDecimal moneyCondition, BigDecimal moneyReturn) { this.moneyCondition = moneyCondition; this.moneyReturn = moneyReturn; } }
收费工厂类:
public class CashFactory { public static AbstractCashierSystem createCashAccept(String type) { AbstractCashierSystem cs = null; switch (type) { case "正常收费": cs = new CashNormal(); break; case "满300返100": CashReturn cr1 = new CashReturn(new BigDecimal("300"), new BigDecimal("100")); cs = cr1; break; case "打8折": CashRebate cr2 = new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage()); cs = cr2; break; } return cs; } }
客户端:
@Slf4j public class CashOperation { public static void main(String[] args) { BigDecimal totalPrice; //比如客户端选择的是满返,满300返100 AbstractCashierSystem cs = CashFactory.createCashAccept("满300返100"); totalPrice = cs.acceptCash(new BigDecimal("400")); log.info("合计总价为:{}", totalPrice); } }
综上小结:面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。
小菜:这样设计好后
1.如果是再加满500返200,则客户端加一个选项即可
2.如果再出新的促销方式,如满100积分10分,则再出一个子类分支即可,不会影响之前代码
大鸟:不错,简单工厂模式看似已经解决了这个问题,但是只是解决了对象的创建,从实际情况出发,工厂本身就包含了多种收费模式,可能经常性地更改打折额度和返回额度,那么每次都要重新改代码重新部署,那还有没有更好的方法来解决呢?
试用策略模式
定义:策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变换,不会影响到试用算法的用户
什么是算法:从上面的例子来看,打折和返利都是一种算法
为什么用策略模式:用工厂生成算法对象,这没有错,但算法本身是一种策略,最重要的是算法是随时可以相互替换的,这就是变化点,而封装变化点是面向对象的一种很重要的方式
策略模式简要实现:
抽象算法类(定义支持的所有算法的公共接口):
public abstract class Strategy { public abstract void AlgorithmInterface(); }
具体算法A:
public class ConcreteStrategyA extends Strategy { @Override public void AlgorithmInterface() { } }
具体算法B:
public class ConcreteStrategyB extends Strategy { @Override public void AlgorithmInterface() { } }
Context:用于对Strategy对象的引用
public class Context { Strategy strategy; /** * 初始化时传入的策略 * @param strategy */ public Context(Strategy strategy) { this.strategy = strategy; } /** * 根据具体策略对象,调用其算法的方法 */ public void ContextInterface() { strategy.AlgorithmInterface(); } }
客户端代码:
public class StrategyCashOperation { public static void main(String[] args) { Context context; context = new Context(new ConcreteStrategyA()); context.ContextInterface(); context = new Context(new ConcreteStrategyB()); context.ContextInterface(); } }
小菜尝试将策略模式融入收银系统
其实正常收费,返利,满减都是一种具体策略,AbstractCashierSystem是抽象策略,现在只要加入Context类,再改下客户端即可:
添加CashContext类:
public class CashContext { private AbstractCashierSystem cs; public CashContext(AbstractCashierSystem cs) { this.cs = cs; } public BigDecimal GetResult(BigDecimal money) { return cs.acceptCash(money); } }
客户端代码调整:
@Slf4j public class CashOperation { public static void main(String[] args) { BigDecimal totalPrice; CashContext cs = null; BigDecimal money; //客户端传入策略对象type String type = "正常收费"; switch (type) { case "正常收费": cs = new CashContext(new CashNormal()); break; case "满300返100": cs = new CashContext(new CashReturn(new BigDecimal("300"), new BigDecimal("100"))); break; case "打8折": cs = new CashContext(new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage())); break; } //客户端传入金额500 money = new BigDecimal("500"); totalPrice = cs.GetResult(money); log.info("总额:{}", totalPrice); } }
大鸟:策略模式用进去了,但是在客户端判断策略模式,不是又走了之前的老路么,怎么讲判断转移?试试策略模式和工厂模式的结合
改造后的CashContext:
public class CashContext { private AbstractCashierSystem cs; public CashContext(String type) { switch (type) { case "正常收费": CashNormal cs0 = new CashNormal(); cs = cs0; break; case "满300返100": CashReturn cs1 = new CashReturn(new BigDecimal("300"), new BigDecimal("100")); cs = cs1; break; case "打8折": CashRebate cs2 = new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage()); cs = cs2; break; } } public BigDecimal GetResult(BigDecimal money) { return cs.acceptCash(money); } }
客户端改造:
@Slf4j public class CashOperation { public static void main(String[] args) { //客户端传入策略对象type String type = "正常收费"; CashContext cs = new CashContext(type); //客户端传入金额500 BigDecimal money = new BigDecimal("500"); //通过对Context的GetResult方法的调用,可以得到收取费用的结果,让具体算法与客户进行了格隔离 BigDecimal totalPrice = cs.GetResult(money); log.info("总额:{}", totalPrice); } }
思考:原来简单工厂模式并非只有建立一个工厂类的做法,也可以这样做,那么简单工厂模式和上面两种模式的结果到底有什么不同呢?
简单工厂模式:
AbstractCashierSystem cs = CashFactory.createCashAccept("满300返100");
两者结合:
CashContext cs = new CashContext(type);
答:可以看出,简单工厂模式要识别AbstractCashierSystem和CashFactory两个类,而两种模式结合后,只用识别CashContext一个类就行了
这样的好处 --> 耦合度更低,我们在客户端实例化的是CashContect的对象,调用的是CashContext的getResult方法,这使得具体的收费算法与客户端彻底分离
策略模式解析
1.什么是策略模式:
(1)策略模式是一种定义一系列算法的方法
(2)从概念来看,所有算法完成工作相同只是实现不同,策略模式可以以相同的方式调用所有算法,减少了算法类与使用算法类之间的耦合
2.有什么好处:
(1)解耦,如上面所说
(2)策略模式的Strategy类层次为Context定义了一系列可重用的算法或行为,继承有助于析取出这些算法的公共功能,比如这里的Strategy类是:AbstractCashierSystem,然后在Context里定义了getResult()的方法,这样所有继承了AbstractCashierSystem的子类,都可以用Context里的getResutl()方法
(3)简化了单元测试,每个算法都有自己的类,可以通过自己的接口单独测试
3.小结:
策略模式封装了变化。它可以用来封装任何类型的规则,所以只要在分析过程中遇到需要不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。