《设计模式之禅》样章连载8:迷你版的交易系统(1)

来源:互联网 发布:淘宝怎么加心 编辑:程序博客网 时间:2024/05/17 06:17

第35章 工厂方法模式+策略模式

35.1 迷你版的交易系统

大家可能对银行的交易系统充满敬畏之情,一听说是银行的IT人员,立马想当然地认为这是个很厉害的人物,那我们今天就来对银行的交易系统做一个初步探讨。国内一家大型集团(全球500强之一)计划建立全国"一卡通"计划,每个员工配备一张IC卡,该卡基本上就是万能的,门禁系统用它,办公系统使用它作为认证,你想打开自己的邮箱,没有它就甭想了,它还可以用来进行消费,比如到食堂吃饭,到园区内的商店消费等等,甚至洗澡、理发、借书、买书等等都可以 用它,只要这张卡内有余额,在集团内部就是一张借记卡(当然还有一些内部的补助通过该卡发放,合理避税嘛)。我们要讲解的就是"一卡通"项目联机交易子系统, 类似于银行的交易系统,可以说它是交易系统的mini版吧。

该项目还是具有一定的挑战性,集团公司的架构分为三层:总部、省级分部、市级机构,业务要求是"一卡通"推广到全国,一名员工从北京出差到了上海, 凭一卡通他能在北京做的事情在上海同样能完成。对于联机交易子项目,异地分支机构与总部之间的通信采用了MQ(Message Queue,消息队列)传递消息,也就是我们观察者模式的BOSS版,与目前的通过POS机刷信用卡基本上是一个道理。

联机交易子系统有一个非常重要的子模块(Module)--扣款子模块。这个模块太重要了!从业务上来说,扣款失败就代表着所有的商业交易关闭,这 是不允许发生的;从技术上来说,扣款的异常处理、事务处理、鲁棒性都是不容忽视的,特别是饭点时间,并发量是很恐怖的,这对我们架构师提出了很高的要求。

我们详细分析一下扣款子模块,每个员工都有一张IC卡,他的IC卡上有以下两种金额。

固定金额

固定金额是指员工不能提现的金额,即使你的固定金额有10万元,你也只能干瞪眼看着,不能提取现金,那这部分金额有来做什么呢?只能用来特定消费, 什么特定消费?员工日常必需的消费,例如食堂内吃饭、理发、健身等活动。

自由金额

自由金额是可以提现的,当然也可以用于消费。每个月初,总部都会为每个员工的IC卡中打入固定数量的金额,然后提倡大家在集团内的商店消费。

在实际的系统开发中,架构设计的是一张IC卡绑定两个账户:固定账户和自由账号,本书为了简化描述还是使用固定金额和自由金额。既然有消费,系统肯定要扣款处理,系统内有两套扣款规则。

扣款策略一

该类型的扣款会对IC卡上的两个金额都会产生影响,计算公式如下:

IC卡固定余额 = IC卡现有固定余额 - 交易金额 / 2

IC卡自由余额 = IC卡现有自由金额  - 交易金额 / 2

也就是说,该类型的的消费分别在固定金额和自由金额上各扣除一半。它适用于什么消费场景呢?固定消费场景,例如吃饭、理发等情况下的扣款,为什么要这么做呢?为了防止乱请客的情况,你请别人吃饭,好,你自己也要出一半,你乐意呀。

扣款策略二

全部从自由金额的上扣除,由于集团内的各种消费、服务非常齐全,而且比市面价格稍低,员工还是很乐意到这里消费的,而且很多员工本身就住在集团附近,基本上就是"公司即家,家即公司"。

今天要讲的重点就是这两种消费的扣款策略,该怎么设计?先想清楚了再回答,你要知道这种联机交易,日后允许你大规模变更的可能性基本上是零,所以系 统设计的时候要做到可拆卸(Pluggable)的架构,避免日后维护的大量开支。

很明显,这是一个策略模式的实际应用,但是你还记得策略模式是有缺陷的吗?它的具体策略必须暴露出去,而且还要由上层模块初始化,这不合适,与迪米 特原则有冲突,高层次模块对低层次的模块应该仅仅处在"接触"的层次上,而不应该是"耦合"的关系,否则,维护的工作量就会非常大。问题提出了,那我们就 应该想办法来修改这个缺陷,正好工厂方法模式可以帮我们产生指定的对象,但是问题又来了,工厂方法模式要指定一个类,它才能产生对象,怎么办?引入一个配 置文件进行映射,避免系统僵化情况的发生,我们以枚举类完成该任务。

还有一个问题,一个交易的扣款模式是固定的,根据其交易编号而定,那我们怎么把交易编号与扣款策略对应起来呢?采用状态模式或责任链模式都可以,如 果采用状态则认为交易编号就是一个交易对象的状态,对于一笔确定的交易(一个已经生成了的对象),它的状态不会发生从一个状态过渡到另一个状态的情况,也 就是说它的状态只有一个,执行完毕后即结束,不存在多状态的问题;如果采用责任链模式,则可以以交易编码作为链中的判断依据,由每个执行节点进行判断,返 回相应的扣款模式。但是在实际中,采用了关系型数据库存储扣款规则与交易编码的对应关系,为了简化该部分的讲义,我们在下面的设计中使用了条件判断语句来 代替。

还有,我们分析了这么多,这么复杂的扣款模块总要进行一个封装吧,你不能让上层的业务模块直接深入到我们模块的内部吧,于是门面模式又摆在了眼前。

分析完毕,我们要先画出类图,挑柿子就选软的捏,那我们做设计也遵循一下这个原则,先选最简单的业务,然后画出类图,那我们先定义交易中使用到的两 个类:IC卡类和交易类,如图35-1所示。

  图35-1 IC卡类和交易类

每个IC卡有三个属性,分别是IC卡号码、固定金额、自由金额,然后通过getter/setter方法来访问,如代码清单35-1所示。

代码清单35-1  IC卡类

  1. public class Card {  
  2.     //IC卡号码  
  3.     private String cardNo="";  
  4.     //卡内的固定交易金额  
  5.     private int steadyMoney =0;  
  6.     //卡内自由交易金额  
  7.     private int freeMoney =0;     
  8.     //getter/setter方法     
  9.     public String getCardNo() {  
  10.         return cardNo;  
  11.     }  
  12.  
  13.     public void setCardNo(String cardNo) {  
  14.         this.cardNo = cardNo;  
  15.     }  
  16.  
  17.     public int getSteadyMoney() {  
  18.         return steadyMoney;  
  19.     }  
  20.  
  21.     public void setSteadyMoney(int steadyMoney) {  
  22.         this.steadyMoney = steadyMoney;  
  23.     }  
  24.  
  25.     public int getFreeMoney() {  
  26.         return freeMoney;  
  27.     }  
  28.  
  29.     public void setFreeMoney(int freeMoney) {  
  30.         this.freeMoney = freeMoney;  
  31.     }  
  32. }

细心的读者可能注意到,你的金额怎么都是整数类型呀,这不对呀,应该是double类型或者BigDecimal类型呀。是,一般非银行的交易系 统,比如超市的收银系统,系统内都是存放的int整数类型,在显示的时候才转换为货币类型。

交易信息Trade类,负责记录每一笔交易,它是由监听程序监听MQ队列而产生的,有两个属性:交易编号和交易金额,其中的交易编号对整个交易非常重要,18位字符(在银行的交易系统中,这里可不是字符串,一般是十进制数组或二进制数字,要考虑系统的性能,数字运算可比字符运算快得多),包括POS 机编号、商户编号、校验码等等,我们这里暂时用不到,就不多做介绍,我们只要知道它是一个非常有用的编码就成。交易金额为整数类型,实际金额放大100倍 即可。如代码清单35-2所示。

代码清单35-2 交易类

  1. public class Trade {      
  2.     //交易编号  
  3.     private String tradeNo = "";  
  4.     //交易金额  
  5.     private int amount = 0;  
  6.     //getter/setter方法  
  7.     public String getTradeNo() {  
  8.         return tradeNo;  
  9.     }  
  10.  
  11.     public void setTradeNo(String postNo) {  
  12.         this.tradeNo = postNo;  
  13.     }  
  14.  
  15.     public int getAmount() {  
  16.         return amount;  
  17.     }  
  18.  
  19.     public void setAmount(int amount) {  
  20.         this.amount = amount;  
  21.     }  

两个最简单也是在应用中最常使用的对象定义完毕,我们就需要来定义我们的策略了,非常明显的策略模式,类图如图35-2所示。

  图35-2 扣款策略类图

典型的策略模式,扣款有两种策略:固定扣款和自由扣款,比较简单,不多说,我们来看代码,先看抽象策略,也就是扣款接口,如代码清单35-3所示。

代码清单35-3  扣款策略接口

  1. public interface IDeduction {  
  2.     //扣款,提供交易和卡信息,进行扣款,并返回扣款是否成功  
  3.     public boolean exec(Card card,Trade trade);  

固定扣款的规则是固定金额和自由金额各扣除交易金额的一半,如代码清单35-4所示。

代码清单35-4  扣款策略一

  1. public class SteadyDeduction implements IDeduction {  
  2.     //固定**易扣款  
  3.     public boolean exec(Card card, Trade trade) {  
  4.         //固定金额和自由金额各扣除50%  
  5.         int halfMoney = (int)Math.rint(trade.getAmount() / 2.0);  
  6.         card.setFreeMoney(card.getFreeMoney() - halfMoney);  
  7.         card.setSteadyMoney(card.getSteadyMoney() - halfMoney);  
  8.         return true;  
  9.     }  

这个具体策略也非常简单,就是两个金额各自减去交易额的一半(注意除数是2.0,可不是2),然后再四舍五入,算法确实简单。该逻辑没有考虑账户余 额不足的情况,也没有考虑异常情况,比如并发情况,读者可以想想看,一张卡有两笔消费同时发生时,是不是就发生错误了?一张卡同时有两笔消费会出现这种情 况吗?会的,网络阻塞的情况,MQ多通道发送,在网络繁忙的情况下是有可能出现该问题,这里就不多介绍,有兴趣的读者可以看看MQ的资料。我们在这里的讲 解实现的是一个快乐路径,认为所有的交易都是在安全可靠的环境中发生的,并且所有的系统环境都满足我们的要求。我们再来看另一个策略,更简单,如代码清单 35-5所示。

代码清单35-5  扣款策略二

  1. public class FreeDeduction implements IDeduction {  
  2.     //自由扣款  
  3.     public boolean exec(Card card, Trade trade) {  
  4.         //直接从自由余额中扣除  
  5.         card.setFreeMoney(card.getFreeMoney() - trade.getAmount());  
  6.         return true;  
  7.     }  

卡内的自由金额减去交易金额再修改卡内自由金额就完事了,异常情况不考虑。这两个具体的策略与我们的交易类型没有任何关系,也不应该有关系,策略模 式就是提供两个可以相互替换的策略,至于在什么时候使用什么策略,则不是由策略模式来决定的。策略模式还有一个角色没出场,即封装角色,如代码清单 35-6所示。

代码清单35-6  扣款策略的封装

  1. public class DeductionContext {  
  2.     //扣款策略  
  3.     private IDeduction deduction = null;  
  4.     //构造函数传递策略  
  5.     public DeductionContext(IDeduction _deduction){  
  6.         this.deduction = _deduction;  
  7.     }  
  8.     //执行扣款  
  9.     public boolean exec(Card card,Trade trade){  
  10.         return this.deduction.exec(card, trade);  
  11.     }  

《设计模式之禅》目录
《设计模式之禅》前言
《设计模式之禅》样章连载1:原型模式之“个性化电子账单”
《设计模式之禅》样章连载2:原型模式的定义及应用
《设计模式之禅》样章连载3:原型模式的注意事项
《设计模式之禅》样章连载4:代理模式之“我是游戏至尊”
《设计模式之禅》样章连载5:代理模式的定义及应用
《设计模式之禅》样章连载6:代理模式扩展之“普通代理”和“强制代理”
《设计模式之禅》样章连载7:代理的个性和动态代理
《设计模式之禅》互动网已上架首发!

原创粉丝点击