设计模式之禅混编2
工厂方法模式+策略模式
- 迷你版的交易系统
- “一卡通”项目联机交易子系统
- 集团公司的架构
- 总部
- 省级分部
- 市级机构
- 业务要求
- 推广到全国,在山西能做的事在全国的其他地方也能做
- 对于联机子项目,异地分支机构与总部之间的通讯
- 联机交易系统有一个非常重要的子模块--扣款子模块
- 从业务上说:扣款失败就代表着所有的商业交易关闭,这是不允许发生的
- 从技术上说:扣款子模块的异常处理、事物处理、鲁棒性都是不容忽视的
- 详细分析一下扣款子系统
- 每个员工都有一张IC卡,它的IC卡上有以下两种金额
- 固定金额
- 员工不可以提现的金额,只能用来特定的消费
- 自由金额
- 可以提现的
- 固定金额
- 每个员工都有一张IC卡,它的IC卡上有以下两种金额
- 实际开发中
- 架构设计采用的是一张IC卡绑定两个账户:固定账户和自由账户;既然有消费,系统肯定有扣款处理,系统内有两套扣款原则
- 扣款策略一
1. 两个金额受影响: 1. IC卡固定余额=IC卡现有固定余额-交易金额/2 2. IC卡自由余额=IC卡现有自由金额-交易金额/2
- 扣款策略二
1. 全部从自由金额上扣除
- 扣款策略一
- 架构设计采用的是一张IC卡绑定两个账户:固定账户和自由账户;既然有消费,系统肯定有扣款处理,系统内有两套扣款原则
项目的重点
- 两种扣款的策略怎们设计?要知道这种联机交易,日后允许大规模变更的可能性基本是零,所以系统设计的时候要做到可拆卸,避免日后维护的大量开支
- 很明显,这是一个策略模式的实际应用,但是策略模式是有缺陷的,它的具体策略必须暴露出去,而且还要有上层模块初始化,这不合适,与迪米特法则有冲突,高层次模块对低层次的调用应该仅仅处在“接触”的层次上,而不应该是耦合的关系,否则的话,维护的工作量会非常的大。
- 修改缺陷--解耦:工厂模式产生对象------新的问题(要指定一个类才能产生对象)
- 修改缺陷--引入一个配置文件进行映射,避免系统僵化情况的发生,以枚举类完成该任务
- 另一个问题:一个交易的扣款模式是固定的,根据其交易的编号而定,那我们怎样把交易编号与扣款策略对应起来?
- 状态模式:认为交易编号就是一个交易对象的状态,状态只有一个,执行完后立即结束,不存在多状态的问题
- 责任链模式:交易编码作为链中的判断依据,由每个执行节点进行判断,返回相应的扣款模式【简化对应关系:使用if的判断语句来进行简化】
- 这么复杂的系统应该封装一下,不能让上层的业务模块直接深入到模块的内部,于是需要门面模式的封装一下
- 修改缺陷--解耦:工厂模式产生对象------新的问题(要指定一个类才能产生对象)
类图
- IC卡类
- 交易类
- 扣款策略类图
- 策略工厂类图
- 扣款子模块完整类图
具体代码
IC卡类
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public class Card { // IC卡号 private String cardNo = ""; // 卡内的固定交易金额 private int steadyMoney = 0; // 卡内的自由金额 private int freeMoney = 0; // getter/setter方法 public String getCardNo() { return cardNo; } public void setCardNo(String cardNo) { this.cardNo = cardNo; } public int getSteadyMoney() { return steadyMoney; } public void setSteadyMoney(int steadyMoney) { this.steadyMoney = steadyMoney; } public int getFreeMoney() { return freeMoney; } public void setFreeMoney(int freeMoney) { this.freeMoney = freeMoney; } }
交易类
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public class Trade { // 交易编号 private String tradeNo = ""; // 交易金额 private int amount = 0; // getter/setter方法 public String getTradeNo() { return tradeNo; } public void setTradeNo(String tradeNo) { this.tradeNo = tradeNo; } public int getAmount() { return amount; } public void setAmount(int amount) { this.amount = amount; } }
扣款策略接口
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public interface IDeduction { // 扣款,提供交易和卡信息,进行扣款,并返回扣款是否成功 public boolean exec(Card card, Trade trade); }
扣款策略一
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public class SteadyDeduction implements IDeduction { // 固定性交易扣款 @Override public boolean exec(Card card, Trade trade) { // 固定金额和自由金额各扣除50% int halfMoney = (int) Math.rint(trade.getAmount() / 2.0); card.setFreeMoney(card.getFreeMoney() - halfMoney); card.setSteadyMoney(card.getSteadyMoney() - halfMoney); return true; } }
扣款策略二
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public class FreeDeduction implements IDeduction { // 自由扣款 @Override public boolean exec(Card card, Trade trade) { // 直接从自由余额中扣除 card.setFreeMoney(card.getFreeMoney() - trade.getAmount()); return true; } }
扣款策略的封装
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public class DeductionContext { // 扣款策略 private IDeduction deduction = null; // 构造函数传递策略 public DeductionContext(IDeduction deduction) { super(); this.deduction = deduction; } // 进行扣款 public boolean exec(Card card, Trade trade) { return this.deduction.exec(card, trade); } }
策略枚举
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description 策略枚举 */ public enum StrategyMan { SteadyDeduction("com.peng.hb2.SteadyDeduction"), FreeDeduction( "com.peng.hb2.FreeDeduction"); String value = ""; private StrategyMan(String _value) { this.value = _value; } public String getValue() { return value; } }
策略工厂
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public class StrategyFactory { // 策略工厂 public static IDeduction getDeduction(StrategyMan strategy) { IDeduction deduction = null; try { deduction = (IDeduction) Class.forName(strategy.getValue()) .newInstance(); } catch (Exception e) { System.err.println(e.getMessage()); } return deduction; } }
扣款模块的封装
package com.peng.hb2; /** * @author kungfu~peng * @data 2018年1月18日 * @description 扣款模块封装 */ public class DeductionFacade { // 对外公布的扣款信息 public static Card deduct(Card card, Trade trade) { // 获得消费策略 StrategyMan reg = gerDeductionType(trade); // 初始化一个消费策略对象 IDeduction deduction = StrategyFactory.getDeduction(reg); // 产生一个策略的上下文 DeductionContext context = new DeductionContext(deduction); // 进行扣款处理 context.exec(card, trade); // 返回扣款处理完毕的数据 return card; } // 获得对应商户的消费策略 private static StrategyMan gerDeductionType(Trade trade) { // 模拟操作 if (trade.getTradeNo().contains("peng")) { return StrategyMan.FreeDeduction; }else{ return StrategyMan.SteadyDeduction; } } }
场景类
package com.peng.hb2; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * @author kungfu~peng * @data 2018年1月18日 * @description */ public class Client { // 模拟交易 public static void main(String[] args) { // 初始化一张IC卡 Card card = initIC(); // 显示一下卡的信息 System.out.println("=======初始化信息========"); showCard(card); // 是否停止运行的标志 boolean flag = true; while (flag) { Trade trade = createTrade(); DeductionFacade.deduct(card, trade); // 交易成功,打印出成功处理信息 System.out.println("\n========交易凭证==========="); System.out.println(trade.getTradeNo() + "交易成功!"); System.out.println("本次发生的交易金额为:" + trade.getAmount() * 100 / 100.0 + "元"); // 展示一下卡内的信息 showCard(card); System.out.println("是否需要退出?(Y/N)"); if (getInput().equalsIgnoreCase("y")) { flag = false; } } } /** * 获取输入 * * @throws IOException */ private static String getInput() { String str = ""; try { str = (new BufferedReader(new InputStreamReader(System.in))) .readLine(); } catch (IOException e) { e.printStackTrace(); } return str; } /** * 产生一个交易 */ private static Trade createTrade() { Trade trade = new Trade(); System.out.println("请输入交易编码:"); trade.setTradeNo(getInput()); System.err.println("请输入交易金额:"); trade.setAmount(Integer.parseInt(getInput())); // 返回交易 return trade; } /** * 功能:打印Card的信息 */ private static void showCard(Card card) { System.out.println("IC卡编号:" + card.getCardNo()); System.out.println("固定类型余额:" + card.getSteadyMoney() * 100 / 100.0 + "元"); System.out.println("自由类型余额:" + card.getFreeMoney() * 100 / 100.0 + "元"); } /** * 功能:初始化一个Card */ private static Card initIC() { Card card = new Card(); card.setCardNo("1104561811"); card.setFreeMoney(100000); card.setSteadyMoney(800000); return card; } }
执行结果
=======初始化信息======== IC卡编号:1104561811 固定类型余额:800000.0元 自由类型余额:100000.0元 请输入交易编码: 666 请输入交易金额: 123 ========交易凭证=========== 666交易成功! 本次发生的交易金额为:123.0元 IC卡编号:1104561811 固定类型余额:800000.0元 自由类型余额:99877.0元 是否需要退出?(Y/N) n 请输入交易编码: 888 请输入交易金额: 345 ========交易凭证=========== 888交易成功! 本次发生的交易金额为:345.0元 IC卡编号:1104561811 固定类型余额:800000.0元 自由类型余额:99532.0元 是否需要退出?(Y/N) y
混编模式小结
- 回顾一下扣款子模块
- 策略模式
- 负责对扣款策略进行封装,保证两个策略可以自由切换,而且日后增加扣款策略也非常简单容易
- 工厂方法模式
- 修正策略模式必须对外暴露具体策略的问题,由工厂方法模式直接产生一个具体的策略对象,而其他模块则不需要依赖具体的策略
- 门面模式
- 负责对复杂的扣款系统进行封装,封装的结果就是避免高层模块深入子系统内部,同事提供系统的高内聚、低耦合的特性
- 策略模式
- 扣款策略变更
- 增加一个新的扣款策略,三步就可以完成:
- 实现IDeduction接口
- 增加StrategyMan配置项
- 扩展扣款策略的利用--也就是门面模式的getDeductionType方法,在实际项目中,这里只需增加数据库的配置项
- 减少一个策略
- 只需修改扣款的利用
- 变更一个策略
- 扩展一个实现类口就可以
- 增加一个新的扣款策略,三步就可以完成:
- 变更扣款策略的利用规则
- 如果系统不想大改,还记得状态模式吗?这个就是为策略的利用服务的,变更它就能满足要求,想把IC卡也纳入策略利用的规则也不复杂。其实这个变更还真发生了,系统投产后,业务提出考虑退休人员的情况,退休人员的IC卡与普通在职员工一样,但是它的扣款不仅仅是根据交易编码,还要根据IC卡对象,系统的变更做法是增加一个扣款策略,同时扩展扣款利用策略,也就是数据库的配置项,在getDeductionType中扩展一个功能:根据IC卡号,确认是否是退休人员,是退休人员,则使用新的扣款策略,这是一个非常简单的扩展
- 交融交易系统没什么复杂的,剩下的问题就是开始考虑系统的鲁棒性--这,才是难点