概念
定义
动态给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成子类更为灵活,本质:“动态组合”
结构
![这里写图片描述](https://i-blog.csdnimg.cn/blog_migrate/e51e2913cc5c614b75b45b924d69fdbe.png)
该模式有四个角色,Component(组件父类),ConcreteComponent(组件子类),Decorator(装饰类父类),ConcreteDecorator(装饰类子类)
该模式具体可以分为两类,一类组件(被装饰的对象),一类装饰器。
组件父类:可以添加职责,装饰器父类和组件子类都继承它。
组件子类:可以被添加职责的,组件子类一般是被装饰器装饰的原始对象
装饰器父类:需要定义和组件接口一致的接口,并且需要持有一个组件接口对象,也就被装饰的对象
装饰类子类:实际的装饰器对象,实现具体要对装饰对象添加的功能
关键
其关键部分在于组件子类和装饰器类都继承了组件父类。组件子类不实现功能,而是转调给装饰器A,装饰器A实现其额外职责后继续转调给装饰器B,装饰器B继续转调给装饰器C,如此循环下去,等到最后就是被装饰对象返回给上一个装饰器,这样层层返回。类似递归调用,从外层往里层调用,再从里层往外层返回,从
关于递归,可见https://blog.csdn.net/a_good_programer/article/details/53292446
2.既然是层层递增下去,那么传统的装饰器类就应该有构造器可以不断的添加装饰器类,所以装饰器父类需要有构造器,子类只需要继承即可
小实例
被装饰接口
/**
* 组件对象的接口,可以给这些对象动态的添加职责
*/
public abstract class Component {
/**
* 示例方法
*/
public abstract void operation();
}
被装饰对象
/**
* 具体实现组件对象接口的对象
*/
public class ConcreteComponent extends Component {
public void operation() {
//相应的功能处理
}
}
装饰器接口
/**
* 装饰器接口,维持一个指向组件对象的接口对象,
* 并定义一个与组件接口一致的接口
*/
public abstract class Decorator extends Component {
/**
* 持有组件对象
*/
protected Component component;
/**
* 构造方法,传入组件对象
* @param component 组件对象
*/
public Decorator(Component component) {
this.component = component;
}
public void operation() {
//转发请求给组件对象,可以在转发前后执行一些附加动作
component.operation();
}
}
装饰器子类
/**
* 装饰器的具体实现对象,向组件对象添加职责
*/
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() {
//调用父类的方法,可以在调用前后执行一些附加动作
//在这里进行处理的时候,可以使用添加的状态
System.out.println("begin---------->");
super.operation();
System.out.println("begin---------->");
}
}
装饰器子类
/**
* 装饰器的具体实现对象,向组件对象添加职责
*/
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
/**
* 需要添加的职责
*/
private void addedBehavior() {
//需要添加的职责实现
}
public void operation() {
//调用父类的方法,可以在调用前后执行一些附加动作
super.operation();
addedBehavior();
}
}
实例1
这是一个简化后的奖金计算体系。存在三种奖金:
1.每个人当月的业务奖金 = 当月销售额 X 3%
2.每个人累计奖金 = 总的回款额 X 0.1%
3.团队奖金 = 团队总销售额 X 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);
}
}
计算的类
/**
* 计算奖金的对象
*/
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);
}
}
使用设计模式
组件父类
/**
* 计算奖金的组件接口
*/
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;
}
}
装饰器父类
/**
* 装饰器的接口,需要跟被装饰的对象实现同样的接口
*/
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);
}
}
装饰器子类
/**
* 装饰器对象,计算当月团队业务奖金
*/
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 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;
}
}
其类图是这样的
关键注意calcPrize方法
客户端调用后的流程
装饰模式的知识点
1.装饰模式相当灵活,实现动态为被装饰对象添加功能,在外部看来已经不是原始对象,而是重重装饰后的对象了
2.装饰器模式实际就是一种递归调用。所以各个装饰器最好是独立的,不要有先后依赖顺序
3.组件类(被装饰类)和装饰器类继承同一个接口,保证是同一个类型。但是组件类是不知道装饰器类的存在的,而装饰器类在不断给组件类添上包装
Java中经典的装饰模式
java中经典的装饰模式莫过于I/O流
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("IOTest.txt")))
这是一个经常使用的代码,有两个装饰器类和一个被装饰对象。DataInputStream和BufferedInputStream是装饰器类,FileInputStream是被装饰对象。
DataInputStream额外在前面的基础上给添加上新的职责:允许应用程序以与机器无关方式从底层输入流读取基本Java数据类型,可以与DataOutputStream搭配使用
BufferedInputStream额外给添上新的职责:缓冲流
其上面两种装饰器之间可以相互替换的,在于你想展示在最外层给用户调用的装饰对象是怎样的
基于java的IO装饰器
基于java的IO装饰器,为了方便理解,我们新增一个装饰器,这个装饰器的功能是:把传入的英文字符向后移动两个位置,比如:a变成c,b变成d,最后y变成a,z变成b
我们采取两种继承方式,然后对其出现的问题,还有问题的原因,流程再进行分析
新增装饰器继承组件类
我们做的是OutputStream,与上面的类图相似,所以OutputStream就是组件类,我们做的装饰器直接继承组件类,做一个退化版的装饰器,效果会怎样呢?
装饰器类
/**
* 实现简单的加密
*/
public class EncryptOutputStream extends OutputStream{
//持有被装饰的对象
private OutputStream os = null;
public EncryptOutputStream(OutputStream os){
this.os = os;
}
public void write(int a) throws IOException {
//先统一向后移动两位
a = a+2;
//97是小写的a的码值
if(a >= (97+26)){
//如果大于,表示已经是y或者z了,减去26就回到a或者b了
a = a-26;
}
this.os.write(a);
}
}
测试类
public class Client {
public static void main(String[] args) throws Exception {
// 流式输出文件
DataOutputStream dout = new DataOutputStream(
new BufferedOutputStream(
new EncryptOutputStream(
new FileOutputStream("MyEncrypt.txt"))));
dout.write("abcdxy".getBytes());
dout.close();
}
}
正常的测试的话,那么我们会在文件看到相应的字母。但是如果我们把装饰器改变了的话,那就会发生文件内内容空白的情况
DataOutputStream dout = new DataOutputStream(
new EncryptOutputStream(
new BufferedOutputStream(
new FileOutputStream("MyEncrypt.txt"))));
其关键在于close()方法。BufferedOutputStream是一个默认8192byte缓存流,到达8192byte就会自动输出,如果没到达,那么会被缓存。正常测试成功的原因在于,正常close()后,会先调用关闭DataOutputStream的方法,然后转到BufferedOutputStream的close方法,然后该装饰器继承于FilterOutputStream。FilterOutputStream的close方法,会先将输出流flush,然后关闭流,所以BufferedOutputStream流的数据会被强制输出。
而错误输出空白在于装饰器EncryptOutputStream继承于Outputstream类的方法,该方法空白,什么都没做。所以在调用关闭DataOutputStream的方法,然后转到BufferedOutputStream的close方法。结果在这里,并没有flush数据,导致后面那层的BufferedOutputStream无法将缓存区的数据强制输出
解决该方法非常简单,既然不能继承组件类,那么只要继承装饰器类,就可以解决这个装饰器不能前后置换的问题了
新增装饰器继承装饰器父类
/**
* 实现简单的加密
*/
public class EncryptOutputStream2 extends java.io.FilterOutputStream{
public EncryptOutputStream2(OutputStream os){
//调用父类的构造方法
super(os);
}
public void write(int a) throws IOException {
//先统一向后移动两位
a = a+2;
//97是小写的a的码值
if(a >= (97+26)){
//如果大于,表示已经是y或者z了,减去26就回到a或者b了
a = a-26;
}
//调用父类的方法
super.write(a);
}
}
再进行测试,会发现上面出现的问题会消失