zoukankan      html  css  js  c++  java
  • 应对软件需求变化-装饰器模式的应用

    一、动机

    在软件系统中,由于需求的变化,一个对象的功能实现经常面临着扩展变化,但是功能接口方法比较稳定。如何使“对象功能实现的扩展变化”能够根据需要来动态地实现?

    我们来看下怎样使用装饰器模式来应对功能的扩展变化。

    二、需求变化过程

    1、软件需求

     考虑一个实际应用:实现灵活的奖金计算。

    奖金计算的特点就是业务功能复杂,还有一个变化点就是计算方式经常需要变动,因为业务部门要通过调整奖金的计算方式来激励士气。奖金计算体系包括三种:每个人当月业务奖金、每个人累计奖金、团队奖金。

    2、不用模式的解决方案

    普通作法:一个人的奖金分成很多个部分,要实现奖金计算,主要就是要按照各个奖金计算的规则,把这个人可以获取的每部分奖金计算出来,然后计算一个总和,这就是这个人可以得到的奖金。

    测试数据:

    import java.util.*;
    /**
     * 在内存中模拟数据库,准备点测试数据,好计算奖金
     */
    public class TempDB {
        private TempDB(){}
        /**
         * 记录每个人的月度销售额,只用了人员,月份没有用
         */
        public static Map<String,Double> mapMonthSaleMoney = new HashMap<String,Double>();
        
        static{
            //填充测试数据
            mapMonthSaleMoney.put("张三",10000.0);
            mapMonthSaleMoney.put("李四",20000.0);
            mapMonthSaleMoney.put("王五",30000.0);
        }
    }

    奖金计算类:

    import java.util.Date;
    /**
     * 计算奖金的对象
     */
    public class Prize {
        /**
         * 计算某人在某段时间内的奖金,有些参数在演示中并不会使用,
         * 但是在实际业务实现上是会用的,为了表示这是个具体的业务方法,
         * 因此这些参数被保留了
         * @param user 被计算奖金的人员
         * @param begin 计算奖金的开始时间
         * @param end 计算奖金的结束时间
         * @return 某人在某段时间内的奖金
         */
        public  double calcPrize(String user,Date begin,Date end){
            double prize = 0.0;
            
            //计算当月业务奖金,所有人都会计算
            prize = this.monthPrize(user, begin, end);
            //计算累计奖金
            prize += this.sumPrize(user, begin, end);
            
            //需要判断该人员是普通人员还是业务经理,团队奖金只有业务经理才有
            if(this.isManager(user)){
                prize += this.groupPrize(user, begin, end);
            }
            
            return prize;
        }
        /**
         * 计算某人的当月业务奖金,参数重复,就不再注释了
         */
        private double monthPrize(String user, Date begin, Date end) {
            //计算当月业务奖金,按照人员去获取当月的业务额,然后再乘以3%
            double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;
            System.out.println(user+"当月业务奖金"+prize);
            return prize;
        }
        /**
         * 计算某人的累计奖金,参数重复,就不再注释了
         */
        public double sumPrize(String user, Date begin, Date end) {
            //计算累计奖金,其实这里应该按照人员去获取累计的业务额,然后再乘以0.1%
            //简单演示一下,假定大家的累计业务额都是1000000元
            double prize = 1000000 * 0.001;
            System.out.println(user+"累计奖金"+prize);
            return prize;
        }    
        /**
         * 判断人员是普通人员还是业务经理
         * @param user 被判断的人员
         * @return true表示是业务经理,false表示是普通人员
         */
        private boolean isManager(String user){
            //应该从数据库中获取人员对应的职务
            //为了演示,简单点判断,只有王五是经理
            if("王五".equals(user)){
                return true;            
            }
            return false;
        }
        /**
         * 计算当月团队业务奖,参数重复,就不再注释了
         */
        public double groupPrize(String user, Date begin, Date end) {
            //计算当月团队业务奖金,先计算出团队总的业务额,然后再乘以1%,假设都是一个团队的
            double group = 0.0;
            for(double d : TempDB.mapMonthSaleMoney.values()){
                group += d;
            }
            double prize = group * 0.01;
            System.out.println(user+"当月团队业务奖金"+prize);
            return prize;
        }
    }

    客户端:

    public class Client {
        public static void main(String[] args) {
            //先创建计算奖金的对象
            Prize p = new Prize();
            
            //日期对象都没有用上,所以传null就可以了
            double zs = p.calcPrize("张三",null,null);        
            System.out.println("==========张三应得奖金:"+zs);
            double ls = p.calcPrize("李四",null,null);
            System.out.println("==========李四应得奖金:"+ls);        
            double ww = p.calcPrize("王五",null,null);
            System.out.println("==========王经理应得奖金:"+ww);
        }
    }

    3、问题

    如果软件没有需求变化,不使用设计模式是没有问题的。但是在这个应用中,奖金的计算方式经常发生变动,几乎每个季度都会有小调整,每年都有大调整,这就要求软件的实现要足够灵活,要能够很快进行相应的调整和修改,否则就不能满足实际业务的需要。

    比如现在根据业务需要,增加一个“环比增长奖金”,那么就需要在奖金计算类中,添加新的功能方法,在计算奖金时调用新的功能方法。过了两个月,业务奖励的策略发生了变化,不再需要这个奖金了,或者换了一个新的奖金方式,又要修改奖金计算类。违反了开闭原则。

    三、使用装饰者模式解决问题

    我们看下,使用装饰者模式来重写刚才的案例,应对扩展功能经常变化的问题。

    首先定义奖金计算组件的接口(抽象类),里面定义了计算奖金的方法。代码如下:

    /**
     * 计算奖金的组件接口
     */
    public abstract class Component {
        /**
         * 计算某人在某段时间内的奖金,有些参数在演示中并不会使用,
         * 但是在实际业务实现上是会用的,为了表示这是个具体的业务方法,
         * 因此这些参数被保留了
         * @param user 被计算奖金的人员
         * @param begin 计算奖金的开始时间
         * @param end 计算奖金的结束时间
         * @return 某人在某段时间内的奖金
         */
        public abstract double calcPrize(String user,Date begin,Date end);
    }

    奖金计算接口(抽象类)的基本实现,实现了计算奖金方法的默认实现:

    /**
     * 基本的实现计算奖金的类,也是被装饰器装饰的对象
     */
    public class ConcreteComponent extends Component{
        
        public double calcPrize(String user, Date begin, Date end) {
            //只是一个默认的实现,默认没有奖金
            return 0;
        }
    }

    定义抽象的装饰器,也就是各个装饰器的父类,这个父类继承了奖金计算组件的抽象类,同时持有被装饰的组件对象:

    import java.util.Date;
    
    /**
     * 装饰器的接口,需要跟被装饰的对象实现同样的接口
     */
    public abstract class Decorator extends Component{
        /**
         * 持有被装饰的组件对象
         */
        protected Component c;
        /**
         * 通过构造方法传入被装饰的对象
         * @param c被装饰的对象
         */
        public Decorator(Component c){
            this.c = c;
        }
    
        public double calcPrize(String user, Date begin, Date end) {
            //转调组件对象的方法
            return c.calcPrize(user, begin, end);
        }
    }

    定义一系列的具体装饰者对象。

    用一个具体的装饰者对象,来实现一条计算奖金的规则。对应三个装饰者对象。

    实现计算当月业务奖金的装饰器:

    import java.util.Date;
    /**
     * 装饰器对象,计算当月业务奖金
     */
    public class MonthPrizeDecorator extends Decorator{
        public MonthPrizeDecorator(Component c){
            super(c);
        }
        
        public double calcPrize(String user, Date begin, Date end) {
            //1:先获取前面运算出来的奖金
            double money = super.calcPrize(user, begin, end);
            //2:然后计算当月业务奖金,按照人员和时间去获取当月的业务额,然后再乘以3%
            double prize = TempDB.mapMonthSaleMoney.get(user) * 0.03;
            System.out.println(user+"当月业务奖金"+prize);
            return money + prize;
        }
    
    }

    实现计算累计奖金的装饰器:

    import java.util.Date;
    /**
     * 装饰器对象,计算累计奖金
     */
    public class SumPrizeDecorator extends Decorator{
        public SumPrizeDecorator(Component c){
            super(c);
        }
        
        public double calcPrize(String user, Date begin, Date end) {
            //1:先获取前面运算出来的奖金
            double money = super.calcPrize(user, begin, end);
            //2:然后计算累计奖金,其实这里应该按照人员去获取累计的业务额,然后再乘以0.1%
            //简单演示一下,假定大家的累计业务额都是1000000元
            double prize = 1000000 * 0.001;
            System.out.println(user+"累计奖金"+prize);
            return money + prize;
        }
    
    }

    实现计算当月团队业务奖金的装饰器:

    import java.util.Date;
    /**
     * 装饰器对象,计算当月团队业务奖金
     */
    public class GroupPrizeDecorator extends Decorator{
        public GroupPrizeDecorator(Component c){
            super(c);
        }
        
        public double calcPrize(String user, Date begin, Date end) {
            //1:先获取前面运算出来的奖金
            double money = super.calcPrize(user, begin, end);
            //2:然后计算当月团队业务奖金,先计算出团队总的业务额,然后再乘以1%
            //假设都是一个团队的
            double group = 0.0;
            for(double d : TempDB.mapMonthSaleMoney.values()){
                group += d;
            }
            double prize = group * 0.01;
            System.out.println(user+"当月团队业务奖金"+prize);
            return money + prize;
        }
    
    }

    使用装饰器的客户端:

    /**
     * 使用装饰模式的客户端
     */
    public class Client {
        public static void main(String[] args) {
            //先创建计算基本奖金的类,这也是被装饰的对象
            Component c1 = new ConcreteComponent();
            
            //然后对计算的基本奖金进行装饰,这里要组合各个装饰
            //说明,各个装饰者之间最好是不要有先后顺序的限制,也就是先装饰谁和后装饰谁都应该是一样的
            
            //先组合普通业务人员的奖金计算
            Decorator d1 = new MonthPrizeDecorator(c1);
            Decorator d2 = new SumPrizeDecorator(d1);    
            
            //注意:这里只需要使用最后组合好的对象调用业务方法即可,会依次调用回去
            //日期对象都没有用上,所以传null就可以了
            double zs = d2.calcPrize("张三",null,null);        
            System.out.println("==========张三应得奖金:"+zs);
            double ls = d2.calcPrize("李四",null,null);
            System.out.println("==========李四应得奖金:"+ls);
            
            //如果是业务经理,还需要一个计算团队的奖金计算
            Decorator d3 = new GroupPrizeDecorator(d2);
            double ww = d3.calcPrize("王五",null,null);
            System.out.println("==========王经理应得奖金:"+ww);
            
        }
    }

    四、总结

    通过采用组合和继承结合的方式,装饰器模式实现了在运行时动态地扩展对象功能的能力,而且可以根据需要扩展多个功能。

    装饰器模式的本质在于解决“主体类在多个方向上的扩展功能”。

    当有新的需求,需要灵活的增加新的扩展能力时,只需要增加新的扩展能力类,不需要修改原有代码,遵循开闭原则。

  • 相关阅读:
    网站的安全架构
    Charles Proxy for Mac & Windows (4.1.3)破解激活工具
    charles抓包工具的中文乱码解决方法
    Charles 从入门到精通
    go语言知识点
    Golang Import使用入门
    算法图解之选择排序
    算法图解之数组和链表
    算法图解之大O表示法
    算法图解之内存的工作原理
  • 原文地址:https://www.cnblogs.com/windpoplar/p/13055666.html
Copyright © 2011-2022 走看看