java装饰者模式(Decorator)
装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。—搜狗百科
我们先来看一个小案例:实现一个发放工资的财务系统。要求按照每个员工不同的级别和绩效来发放工资。
模拟销售部发放工资的场景:
每个销售人员,按照销售额、绩效来发放工资,
销售额不超过5000,提成3%;
销售额超过5000,在刚才基础上,增加1%,相当于4%;
销售经理,基础提成+(如果超过5000后超额奖金)+团队销售总额的1%。
不用设计模式实现
public class User {
private String name;//员工名字
private double sales;//销售额
private String position;//职位
public User(String name, double sales, String position) {
this.name = name;
this.sales = sales;
this.position = position;
}
get()...
}
//一个集合,用来装好多员工,这个类用来充当数据库
public class UserBox {
//存储员工的map集合
private static Map<String,User> userBox;
//初始化集合数据
static {
userBox = new HashMap<>();
userBox.put("Tom",new User("Tom",4000,"普通"));
userBox.put("Ben",new User("Ben",5000,"普通"));
userBox.put("Lisa",new User("Lisa",8000,"经理"));
userBox.put("Honghong",new User("Honghong",4000,"普通"));
}
//根据用户名获取一个用户
public static User getUser(String name){
return userBox.get(name);
}
//获取集合
public static Map<String,User> getUserBox(){
return userBox;
}
}
//这个类用来计算每个员工的奖金
public class Bonus {
//计算奖金
public double caculateBonus(String name){
//1.创建一个变量,存储最终的结果
double result = 0;
//2.获取用户
User user = UserBox.getUser(name);
//3.每个人都有一个基本奖金
result+=user.getSales()*0.03;
System.out.println(user.getName()+"的基本奖金"+result);
//4.判断销售额是否超过5000
if (user.getSales() >=5000){
result += user.getSales()*0.01;
System.out.println(user.getName()+"销售额超5000后的应得奖金"+result);
}
//5.如果这个人是经理
if("经理".equals(user.getPosition())){//字符串写前边比较安全
//计算整个销售部的总业绩,总业绩的1%
double sum = 0;//总额
Map<String,User> map = UserBox.getUserBox();
for(String key : map.keySet()){
sum += map.get(key).getSales();
}
//总额的百分之1,加到经理的奖金里
result += sum*0.01;
System.out.println(user.getName()+"作为经理,加上团队业绩百分之一后的奖金总额为"+result);
}
return result;
}
}
测试函数:
public class TestMain {
public static void main(String[] args) {
Bonus b = new Bonus();
double result = b.caculateBonus("Lisa");
System.out.println(result);
}
}
结果:
Lisa的基本奖金240.0
Lisa销售额超5000后的应得奖金320.0
Lisa作为经理,加上团队业绩百分之一后的奖金总额为530.0
530.0
程序写完了,这样写有什么问题呢?
不难发现Bonus类中caculateBonus()方法太过臃肿,我们面向对象的设计原则认为,一个方法最好只做一件事情。我们把复杂的判断逻辑和计算奖金都放在了一个方法里,这样肯定是不行的。我们一贯的做法是用多个方法对一个臃肿的逻辑进行拆分。
3类人,具有3种不同的计算奖金的方式,它们可以设计成3个独立的方法,每个方法只负责做自己的事情。
拆开后的方法如下:
public class Bonus {
public double calcBonus(String name){
double result = 0;
result+=this.baseBonus(name);
result+=this.extraBonus(name);
result+=this.teamBonus(name);
return result;
}
//计算基本奖金
private double baseBonus(String name){
User user = UserBox.getUser(name);
double bonus = user.getSales()*0.03;
System.out.println(name+"基本奖金"+bonus);
return bonus;
}
//计算超过标准后的额外奖金
private double extraBonus(String name){
User user = UserBox.getUser(name);
if (user.getSales() >= 5000){
double bonus = user.getSales()*0.01;
System.out.println(name+"超过标准后的额外奖金"+bonus);
return bonus;
}
return 0;
}
//如果是经理,计算团队奖金
private double teamBonus(String name){
User user = UserBox.getUser(name);
if ("经理".equals(user.getPosition())){
double sum = 0;
Map<String,User> map = UserBox.getUserBox();
for(String key:map.keySet()){
sum+=map.get(key).getSales();
}
double bonus = sum*0.01;
System.out.println(name+"作为经理,团队业绩百分之一奖金总额为"+bonus);
return bonus;
}
return 0;
}
}
代码拆分后,会更加符合面向对象的编程思想。
那么请问,拆分后,代码有什么问题?
代码冗余都不是最关键的,关键是扩展性非常差!
比如下个月,公司要搞一个促销活动,为了提高员工的工作积极性,我们希望临时地给每个员工多增加0.05的提成比例。
每多一种计算方式,就要多写一个方法,再改代码,而且代码还不止一处需要修改。
如果临时增加的提成等活动结束了,代码还得删回去。
这个我们叫“开闭原则”(Open Close Principle)
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
软件对象(类、模块、方法等)应该对于扩展是开放的,对修改是关闭的。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
就是写完后的代码要打包成压缩包,包装在一起,如果需要修改代码就需要拆开这个压缩包,修改后再包回去。它就不符合开闭原则了。我们希望写完后的代码,打包之后就不要再更改了,我们想要加功能在外面加,不影响原来的代码。
我们可以使用接口,通过多添加其实现类来扩展功能。
或者是修改配置文件,我让文件的内容改变,里面的代码是读取文件从而执行,这样一来里面的代码是不用动的。
如何实现某一个配置临时地增加和删除呢?
策略模式是不可以的。策略模式解决的是:一个整体的流程固定,流程中某一个地方不一样,抽象出不同的策略,再把策略插进入,它同一个时间,只能有一个策略生效,策略之间没有累加的关系。
所以,我们考虑使用装饰者模式(Decorator),随时加,随时改。能装饰上,能再装饰的基础上再装饰,还能把装饰给移除。
装饰模式逐步演变+思路
public class Bonus {
public double caculateBonus(String name){
//每一个人都有3%
//可能有一天,老板不开心,疫情产生了,业务都做不下去,所有员工的基本奖金都取消了
//我们认为那个3%的奖金也可能不发,我们想要把所有额外的部分拆分出去
System.out.println("默认没有奖金,发的是死工资");
return 0;
}
//========================================================
//a对象 中有一个方法testA
//b对象 有一个自己的方法testB
//想要让b对象同时拥有testA方法和testB方法
//方式1: 继承A类 B extends A
//但是继承的缺点在于,b对象继承过来的testA方法只能拿来用,而不能修改
//方式2:a对象和 b对象组合
/* public class B{
private A a = new A();
public void testA(){
//可以多添加逻辑
a.testA;
//多添加逻辑
}
public void testB(){
System.out.println("自己的testB方法");
}
}*/
//总结:组合更加灵活,继承则比较死板
}
public class BaseBonus {
private Bonus b;
public BaseBonus(Bonus b) {
this.b = b;
}
//1.先有一个基本方法
//2.自己增加的方法 3%提成
public double calcBonus(String name){
//1.先有一个基本方法
double money = b.caculateBonus(name);
//2.基本结果之上,添加自己的处理
double bonus = UserBox.getUser(name).getSales()*0.03;
System.out.println("当月基本奖金"+bonus);
//返回总奖金
return money+bonus;
}
}
public class ExtraBonus {
private BaseBonus bb;
public ExtraBonus(BaseBonus bb) {
this.bb = bb;
}
public double calcBonus(String name){
double money = bb.calcBonus(name);
//自己的处理
double sales = UserBox.getUser(name).getSales();
//这里需要判断该员工的销售额是否大于等于5000
double bonus = sales>=5000?sales*0.01:0;
System.out.println("超额绩效奖金"+bonus);
return money+bonus;
}
}
测试方法:
public class TestMain {
public static void main(String[] args) {
Bonus bonus = new Bonus();
//double result = bonus.caculateBonus("Lisa");
BaseBonus bb = new BaseBonus(bonus);
//double result = bb.calcBonus("Lisa");
ExtraBonus eb = new ExtraBonus(bb);
double result = eb.calcBonus("Lisa");
System.out.println(result);
//刚才那个嵌套过程的问题是:嵌套必须是有顺序的
}
}
默认没有奖金
当月基本奖金240.0
超额绩效奖金80.0
320.0
刚才那个嵌套过程的问题是:嵌套必须是有顺序的,代码显得很死板。
因此我们认为以上的装饰者模式功能上已经具备,但是不够灵活。
可以看到BaseBonus中嵌套了一个private Bonus b
;
ExtraBonus中嵌套了一个private BaseBonus bb;
我们可不可以让它们嵌套同一个类呢?
我们可以添加一个规则:
/**
* 这个规则的出现,就是装饰者
* 装饰者的出现是为了让所有添加的对象都可以随意,
* 而不是非得按照某种顺序
*/
public abstract class Decorator {
public abstract double calcBonus(String name);
}
修改BaseBonus:
让它持有一个属性:装饰者,该属性通过构造方法赋值。
public class BaseBonus {
private Decorator decor;
public BaseBonus(Decorator decor) {
this.decor = decor;
}
//1.先有一个基本方法
//2.自己增加的方法 3%提成
public double calcBonus(String name){
//1.先有一个基本方法
double money = decor.calcBonus(name);
//2.基本结果之上,添加自己的处理
double bonus = UserBox.getUser(name).getSales()*0.03;
System.out.println("当月基本奖金"+bonus);
//返回总奖金
return money+bonus;
}
}
同样的道理,修改ExtraBonus:
public class ExtraBonus {
private Decorator decor;
public ExtraBonus(BaseBonus bb) {
this.decor = decor;
}
//省略...
}
然后所有的规则继承装饰者(那个抽象类)即可:
public class BaseBonus extends Decorator
public class ExtraBonus extends Decorator
注意:这里只是BaseBonus和ExtraBonus继承装饰者Decorator,而原始对象Bonus并不继承Decorator
继承好了之后,calcBonus(String)方法就相当于重写。
但是发现一个新的问题:每一个规则类中都有一个一摸一样的属性:
private Decorator decor;
还要通过构造方法给它传递给子类对象;
public BaseBonus(Decorator decor) {
this.decor = decor;
}
既然继承可以继承属性,我们就可以把这个属性放到父类里去。
同时我们应该改变权限修饰符为protected,否则子类无法继承该属性:
public abstract class Decorator {
protected Decorator decor;
public abstract double calcBonus(String name);
}
然后在子类的构造方法中这样赋值:
public BaseBonus(Decorator decor) {
super.decor = decor;
}
或者这样:
public abstract class Decorator {
protected Decorator decor;
//在父类中加一个构造方法
public Decorator(Decorator decor) {
this.decor = decor;
}
public abstract double calcBonus(String name);
}
public BaseBonus(Decorator decor) {
//子类中使用super()调用父类构造器
super(decor);
}
这样一来,我们通过一个抽象类,让所有的装饰者变得统一。
区分原始对象和装饰者对象:
原始对象中不用存储其他属性(它并不知道别人要装饰它),而装饰者对象中需要存储一个属性(需要给原始对象赋值)。
如何将原始对象(Bonus)和装饰者(XxxBonus extends Decorator)连在一块呢?
这是一个值得深思的问题。
装饰者现在可以在装饰的基础上再装饰,但不能直接装饰原始对象:
因为原始对象(Bonus)它现在没有继承装饰者和实现任何接口,它不能被装饰者所装饰。
我们想要让原始对象中计算奖金的方法名和装饰者对象的方法名一致,让它们用起来是统一的,不仅如此,更加迫切的是我们希望装饰者可以直接装饰原始对象,即我们想让上面的代码编译不报错。
我们可以再使用一个接口(顶级规则):
定义这个接口的目的是为了让原始对象的方法和 Decorator的方法名统一,这样用起来就有了一致性,并且原始对象不直接继承装饰规则(Decorator),而是继承顶级规则,这样让我们的代码更符合人类面向对象的设计思想。
public abstract class Top {
abstract public double calcBonus(String name);
}
让原始对象和装饰者继承它:
public class Bonus extends Top
public abstract class Decorator extends Top {
protected Decorator decorator;
//继承之后,它自己的calcBonus(String name)方法可以删除
}
同时我们为了让装饰者对象不仅能够装饰装饰后的对象,也能够装饰原始对象,我们可以再次修改装饰者类的构造方法:
public abstract class Decorator extends Top {
protected Top top;
public Decorator(Top top){
this.top = top;
}
//继承之后,它自己的calcBonus(String name)方法可以删除
}
public BaseBonus(Top top) {
super(top);
}
public ExtraBonus(Top top) {
super(top);
}
经过上面的修改,你会发现前面TestMain中的main函数编译通过了。
如此一来就可以装饰者之间就可以互相嵌套,而无关乎顺序问题了:
public class TestMain {
public static void main(String[] args) {
Bonus bonus = new Bonus();
ExtraBonus eb = new ExtraBonus(bonus);
BaseBonus bb = new BaseBonus(eb);
double result = bb.calcBonus("Lisa");
System.out.println(result);
}
}
运行结果:
默认没有奖金
超额绩效奖金80.0
当月基本奖金240.0
320.0
可以看到,嵌套的顺序可以不固定,运算工资的顺序也发生了改变,不会那么死板(不会每次先算基本工资再算超额工资,而是各算各的),代码具有更好的灵活性。
如果需要扩展,添加计算经理的奖金的功能,多加一种计算方式,再多加一个装饰者即可:
public class TeamBonus extends Decorator{
public TeamBonus(Top top) {
super(top);
}
@Override
public double calcBonus(String name) {
double money = top.calcBonus(name);
double bonus = 0;
//这里需要判断该职工是否是经理
User user = UserBox.getUser(name);
if("经理".equals(user.getPosition())){
double sum = 0;
Map<String, User> map = UserBox.getUserBox();
for(String key:map.keySet()){
sum+=map.get(key).getSales();
}
bonus = sum*0.01;
System.out.println(name+"作为经理,加上团队业绩百分之一后的奖金总额为"+bonus);
}
return money + bonus;
}
}
然后测试:
public static void main(String[] args) {
Bonus bonus = new Bonus();
TeamBonus tb = new TeamBonus(bonus);
ExtraBonus eb = new ExtraBonus(tb);
BaseBonus bb = new BaseBonus(eb);
double result = bb.calcBonus("Lisa");
System.out.println(result);
}
结果:
默认没有奖金
Lisa作为经理,加上团队业绩百分之一后的奖金总额为210.0
超额绩效奖金80.0
当月基本奖金240.0
530.0
它们之间可以互相嵌套,没有先后次序关系,如果不想算哪部分,就不写对应的装饰着,相当灵活。
每一个类只做一件事情,这个类想要做的事情,是基于上一个类的结果开始做,有点像递归。
这就是所谓的装饰者模式。
其实文件流中就体现了装饰者模式;
File file = new File("D:\\Test");
FileInputStream fis = new FileInputStream(file);//第一个装饰者
BufferedInputStream bis = new BufferedInputStream(fis);//第二个装饰者
如果以上步骤看起来很头疼,这里直接粘贴出前面实现的完整版的装饰者模式供大家参考:
首先,该案例两个基本的类:
public class User {
private String name;//员工名字
private double sales;//销售额
private String position;//职位
public User(String name, double sales, String position) {
this.name = name;
this.sales = sales;
this.position = position;
}
public String getName() {
return name;
}
public double getSales() {
return sales;
}
public String getPosition() {
return position;
}
}
import java.util.HashMap;
import java.util.Map;
//一个集合,用来装好多员工,这个类用来充当数据库
public class UserBox {
//存储员工的map集合
private static Map<String,User> userBox;
//初始化集合数据
static {
userBox = new HashMap<>();
userBox.put("Tom",new User("Tom",4000,"普通"));
userBox.put("Ben",new User("Ben",5000,"普通"));
userBox.put("Lisa",new User("Lisa",8000,"经理"));
userBox.put("Honghong",new User("Honghong",4000,"经理"));
}
//根据用户名获取一个用户
public static User getUser(String name){
return userBox.get(name);
}
//获取集合
public static Map<String,User> getUserBox(){
return userBox;
}
}
顶层接口(这里是一个抽象类,改成接口也可以的):
public abstract class Top {
abstract public double calcBonus(String name);
}
装饰规则
public abstract class Decorator extends Top {
protected Top top;
public Decorator(Top top){
this.top = top;
}
//继承之后,它自己的calcBonus(String name)方法可以删除
}
原始对象:
//这个类用来计算每个员工的奖金
public class Bonus extends Top {
@Override
public double calcBonus(String name){
double result = 0;
System.out.println("默认没有奖金,发的是死工资");
return result;
}
}
3个装饰者:
public class BaseBonus extends Decorator {
public BaseBonus(Top top) {
super(top);
}
//1.先有一个基本方法
//2.自己增加的方法 3%提成
public double calcBonus(String name){
//1.先有一个基本方法
double money = top.calcBonus(name);
//2.基本结果之上,添加自己的处理
double bonus = UserBox.getUser(name).getSales()*0.03;
System.out.println("当月基本奖金"+bonus);
//返回总奖金
return money+bonus;
}
}
public class ExtraBonus extends Decorator {
public ExtraBonus(Top top) {
super(top);
}
public double calcBonus(String name){
double money = top.calcBonus(name);
//自己的处理
double sales = UserBox.getUser(name).getSales();
//这里需要判断该员工的销售额是否大于等于5000了
double bonus = sales>=5000?sales*0.01:0;
System.out.println("超额绩效奖金"+bonus);
return money+bonus;
}
}
public class TeamBonus extends Decorator{
public TeamBonus(Top top) {
super(top);
}
@Override
public double calcBonus(String name) {
double money = top.calcBonus(name);
double bonus = 0;
//这里需要判断该职工是否是经理
User user = UserBox.getUser(name);
if("经理".equals(user.getPosition())){
double sum = 0;
Map<String, User> map = UserBox.getUserBox();
for(String key:map.keySet()){
sum+=map.get(key).getSales();
}
bonus = sum*0.01;
System.out.println(name+"作为经理,加上团队业绩百分之一后的奖金总额为"+bonus);
}
return money + bonus;
}
}
测试代码:
public class TestMain {
public static void main(String[] args) {
Bonus bonus = new Bonus();
ExtraBonus eb = new ExtraBonus(bonus);
BaseBonus bb = new BaseBonus(eb);
TeamBonus tb = new TeamBonus(bb);
double result = tb.calcBonus("Lisa");
System.out.println(result);
}
}
模型:
方法的执行原理: