1 场景问题
1.1 复杂的奖金计算
考虑这样一个实际应用:就是如何实现灵活的奖金计算。
奖金计算是相对复杂的功能,尤其是对于业务部门的奖金计算方式,是非常复杂的,
除了业务功能复杂外,另外一个麻烦之处是计算方式还经常需要变动,因为业务部门经常通过调整奖金的计算方式来激励士气。
先从业务上看看现有的奖金计算方式的复杂性:
首先是奖金分类:对于个人,大致有个人当月业务奖金、个人累计奖金、个人业务增长奖金、及时回款奖金、限时成交加码奖金等等;
\
对于业务主管或者是业务经理,除了个人奖金外,还有:团队累计奖金、团队业务增长奖金、团队盈利奖金等等。
\
其次是计算奖金的金额,又有这么几个基数:销售额、销售毛利、实际回款、业务成本、奖金基数等等;
\
另外一个就是计算的公式,针对不同的人、不同的奖金类别、不同的计算奖金的金额,计算的公式是不同的,就算是同一个公式,里面计算的比例参数也有可能是不同的。
1.2 简化后的奖金计算体系
看了上面奖金计算的问题,所幸我们只是来学习设计模式,并不是真的要去实现整个奖金计算体系的业务,
因此也没有必要把所有的计算业务都罗列在这里,为了后面演示的需要,简化一下,演示用的奖金计算体系如下:
每个人当月业务奖金 = 当月销售额 X 3%
\
每个人累计奖金 = 总的回款额 X 0.1%
\
团队奖金 = 团队总销售额 X 1%
1.3 不用模式的解决方案
一个人的奖金分成很多个部分,要实现奖金计算,主要就是要按照各个奖金计算的规则,
把这个人可以获取的每部分奖金计算出来,然后计算一个总和,这就是这个人可以得到的奖金。
1.为了演示,先准备点测试数据,在内存中模拟数据库,示例代码如下:
/**
* 在内存中模拟数据库,准备点测试数据,好计算奖金
*/
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);
}
}
2.按照奖金计算的规则,实现奖金计算,示例代码如下:
/**
* 计算奖金的对象
*/
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;
}
}
3.写个客户端来测试一下,看看是否能正确地计算奖金,示例代码如下:
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);
}
}
1.4 有何问题
看了上面的实现,挺简单的嘛,就是计算方式麻烦点,每个规则都要实现。
对于奖金计算,光是计算方式复杂,也就罢了,不过是实现起来会困难点,
相对而言还是比较好解决的,不过是用程序把已有的算法表达出来。
最痛苦的是,这些奖金的计算方式,经常发生变动,几乎是每个季度都会有小调整,
每年都有大调整,这就要求软件的实现要足够灵活,要能够很快进行相应调整和修改,
否则就不能满足实际业务的需要。
现在的问题就是:如何才能够透明的给一个对象增加功能,并实现功能的动态组合呢?
2 解决方案
2.1 装饰模式来解决
在装饰模式的实现中,为了能够和原来使用被装饰对象的代码实现无缝结合,是通过定义一个抽象类,
让这个类实现与被装饰对象相同的接口,然后在具体实现类里面,转调被装饰的对象,在转调的前后添加新的功能,
这就实现了给被装饰对象增加功能,这个思路跟“对象组合”非常类似。
在转调的时候,如果觉得被装饰的对象的功能不再需要了,还可以直接替换掉,也就是不再转调,而是在装饰对象里面完全全新的实现。
2.2 装饰模式示例代码
1.先来看看组件对象的接口定义,示例代码如下:
/**
* 组件对象的接口,可以给这些对象动态的添加职责
*/
public abstract class Component {
/**
* 示例方法
*/
public abstract void operation();
}
2.定义了接口,那就看看具体组件实现对象示意吧,示例代码如下:
/**
* 具体实现组件对象接口的对象
*/
public class ConcreteComponent extends Component {
public void operation() {
//相应的功能处理
}
}
3.接下来看看抽象的装饰器对象,示例代码如下:
/**
* 装饰器接口,维持一个指向组件对象的接口对象,并定义一个与组件接口一致的接口
*/
public abstract class Decorator extends Component {
/**
* 持有组件对象
*/
protected Component component;
/**
* 构造方法,传入组件对象
* @param component 组件对象
*/
public Decorator(Component component) {
this.component = component;
}
public void operation() {
//转发请求给组件对象,可以在转发前后执行一些附加动作
component.operation();
}
}
4.该来看看具体的装饰器实现对象了,这里有两个示意对象,一个示意了添加状态
一个示意了添加职责。先看添加了状态的示意对象吧,示例代码如下:
/**
* 装饰器的具体实现对象,向组件对象添加职责
*/
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
/**
* 添加的状态
*/
private String addedState;
public String getAddedState() {
return addedState;
}
public void setAddedState(String addedState) {
this.addedState = addedState;
}
public void operation() {
//调用父类的方法,可以在调用前后执行一些附加动作
//在这里进行处理的时候,可以使用添加的状态
super.operation();
}
}
5.接下来看看添加职责的示意对象,示例代码如下:
/**
* 装饰器的具体实现对象,向组件对象添加职责
*/
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
/**
* 需要添加的职责
*/
private void addedBehavior() {
//需要添加的职责实现
}
public void operation() {
//调用父类的方法,可以在调用前后执行一些附加动作
super.operation();
addedBehavior();
}
}
2.3 使用装饰模式重写示例
首先需要定义一个组件对象的接口,在这个接口里面定义计算奖金的业务方法,
因为外部就是使用这个接口来操作装饰模式构成的对象结构中的对象
需要添加一个基本的实现组件接口的对象,可以让它返回奖金为0就可以了
把各个计算奖金的规则当作装饰器对象,需要为它们定义一个统一的抽象的装饰器对象,好约束各个具体的装饰器的接口
把各个计算奖金的规则实现成为具体的装饰器对象
1.计算奖金的组件接口和基本的实现对象:
在计算奖金的组件接口中,需要定义原本的业务方法,也就是实现奖金计算的方法,示例代码如下:
/**
* 计算奖金的组件接口
*/
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;
}
}
2.定义抽象的装饰器:
/**
* 装饰器的接口,需要跟被装饰的对象实现同样的接口
*/
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);
}
}
3.定义一系列的装饰器对象
/**
* 装饰器对象,计算当月业务奖金
*/
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;
}
}
/**
* 装饰器对象,计算累计奖金
*/
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;
}
}
/**
* 装饰器对象,计算当月团队业务奖金
*/
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;
}
}
4.使用装饰器的客户端:
/**
* 使用装饰模式的客户端
*/
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);
}
}