应对软件需求变化-装饰器模式的应用
一、动机
在软件系统中,由于需求的变化,一个对象的功能实现经常面临着扩展变化,但是功能接口方法比较稳定。如何使“对象功能实现的扩展变化”能够根据需要来动态地实现?
我们来看下怎样使用装饰器模式来应对功能的扩展变化。
二、需求变化过程
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); } }
四、总结
通过采用组合和继承结合的方式,装饰器模式实现了在运行时动态地扩展对象功能的能力,而且可以根据需要扩展多个功能。
装饰器模式的本质在于解决“主体类在多个方向上的扩展功能”。
当有新的需求,需要灵活的增加新的扩展能力时,只需要增加新的扩展能力类,不需要修改原有代码,遵循开闭原则。