设计模式之禅之混编【工厂方法模式+策略模式】

设计模式之禅混编2

工厂方法模式+策略模式

  • 迷你版的交易系统
    • “一卡通”项目联机交易子系统
    • 集团公司的架构
      • 总部
      • 省级分部
      • 市级机构
    • 业务要求
      • 推广到全国,在山西能做的事在全国的其他地方也能做
      • 对于联机子项目,异地分支机构与总部之间的通讯
    • 联机交易系统有一个非常重要的子模块--扣款子模块
      • 从业务上说:扣款失败就代表着所有的商业交易关闭,这是不允许发生的
      • 从技术上说:扣款子模块的异常处理、事物处理、鲁棒性都是不容忽视的
  • 详细分析一下扣款子系统
    • 每个员工都有一张IC卡,它的IC卡上有以下两种金额
      1. 固定金额
        • 员工不可以提现的金额,只能用来特定的消费
      2. 自由金额
        • 可以提现的
  • 实际开发中
    • 架构设计采用的是一张IC卡绑定两个账户:固定账户和自由账户;既然有消费,系统肯定有扣款处理,系统内有两套扣款原则
      • 扣款策略一

        1. 两个金额受影响:
            1. IC卡固定余额=IC卡现有固定余额-交易金额/2
            2. IC卡自由余额=IC卡现有自由金额-交易金额/2
        
      • 扣款策略二

        1. 全部从自由金额上扣除
        

项目的重点

  • 两种扣款的策略怎们设计?要知道这种联机交易,日后允许大规模变更的可能性基本是零,所以系统设计的时候要做到可拆卸,避免日后维护的大量开支
  • 很明显,这是一个策略模式的实际应用,但是策略模式是有缺陷的,它的具体策略必须暴露出去,而且还要有上层模块初始化,这不合适,与迪米特法则有冲突,高层次模块对低层次的调用应该仅仅处在“接触”的层次上,而不应该是耦合的关系,否则的话,维护的工作量会非常的大。
    • 修改缺陷--解耦:工厂模式产生对象------新的问题(要指定一个类才能产生对象)
      • 修改缺陷--引入一个配置文件进行映射,避免系统僵化情况的发生,以枚举类完成该任务
    • 另一个问题:一个交易的扣款模式是固定的,根据其交易的编号而定,那我们怎样把交易编号与扣款策略对应起来?
      1. 状态模式:认为交易编号就是一个交易对象的状态,状态只有一个,执行完后立即结束,不存在多状态的问题
      2. 责任链模式:交易编码作为链中的判断依据,由每个执行节点进行判断,返回相应的扣款模式【简化对应关系:使用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
    

混编模式小结

  • 回顾一下扣款子模块
    • 策略模式
      • 负责对扣款策略进行封装,保证两个策略可以自由切换,而且日后增加扣款策略也非常简单容易
    • 工厂方法模式
      • 修正策略模式必须对外暴露具体策略的问题,由工厂方法模式直接产生一个具体的策略对象,而其他模块则不需要依赖具体的策略
    • 门面模式
      • 负责对复杂的扣款系统进行封装,封装的结果就是避免高层模块深入子系统内部,同事提供系统的高内聚、低耦合的特性
  • 扣款策略变更
    • 增加一个新的扣款策略,三步就可以完成:
      1. 实现IDeduction接口
      2. 增加StrategyMan配置项
      3. 扩展扣款策略的利用--也就是门面模式的getDeductionType方法,在实际项目中,这里只需增加数据库的配置项
    • 减少一个策略
      • 只需修改扣款的利用
    • 变更一个策略
      • 扩展一个实现类口就可以
  • 变更扣款策略的利用规则
    • 如果系统不想大改,还记得状态模式吗?这个就是为策略的利用服务的,变更它就能满足要求,想把IC卡也纳入策略利用的规则也不复杂。其实这个变更还真发生了,系统投产后,业务提出考虑退休人员的情况,退休人员的IC卡与普通在职员工一样,但是它的扣款不仅仅是根据交易编码,还要根据IC卡对象,系统的变更做法是增加一个扣款策略,同时扩展扣款利用策略,也就是数据库的配置项,在getDeductionType中扩展一个功能:根据IC卡号,确认是否是退休人员,是退休人员,则使用新的扣款策略,这是一个非常简单的扩展
  • 交融交易系统没什么复杂的,剩下的问题就是开始考虑系统的鲁棒性--这,才是难点

你可能感兴趣的:(设计模式专栏,设计模式之禅)