应对软件需求变化-装饰器模式的应用

一、动机

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

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

二、需求变化过程

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);
        
    }
}

四、总结

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

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

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

 

posted @ 2020-06-06 17:57  windpoplar  阅读(199)  评论(0编辑  收藏  举报