装饰器模式
以生活中的场景来举例,一个蛋糕胚,给它涂上奶油就变成了奶油蛋糕,再加上巧克力和草莓,它就变成了巧克力草莓蛋糕。
像这样在不改变原有对象的基础之上,将功能附加到原始对象上的设计模式就称为装饰模式(Decorator模式),属于结构型模式。
装饰器模式中主要有四个角色:
Component : 定义被装饰对象的接口,装饰器也需要实现一样的接口
ConcreteComponent: 具体的装饰对象,实现了Component接口,通常就是被装饰的原始对象
Decorator: 所有装饰器的抽象父类,需要定义与Component一致的接口,并且持有一个被装饰的Component对象
ConcreteDecorator: 具体装饰器对象
代码示例
假设我们需要设计一个奖金的系统,目前一个工作人员的奖金包含三部分:
本月销售额的奖金 : 当月销售额的3%
累计销售额的奖金 : 累计销售额的0.1%
团队销售额的奖金 : 团队销售额的1%,只有经理才有
使用装饰模式的实现如下 : 定义一个所有奖金的抽象接口,然后定义一个BasicPrize的初始奖金对象,不同的人奖金的组成不同,就为其添加不同的装饰器
public abstract class Component {
abstract double calPrize(String userName);
}
public class BasicPrize extends Component {
//保存了一个员工与销售额的对应关系的map
public static Map<String,Double> saleMoney = new HashMap<String,Double>();
static {
saleMoney.put("小明",9000.0);
saleMoney.put("小陈",20000.0);
saleMoney.put("小王",30000.0);
saleMoney.put("张经理",55000.0);
}
public double calPrize(String userName) {
return 0;
}
}
/**
*
* 所有装饰器的抽象父类,持有一个被装饰对象
*/
public abstract class Decorator extends Component {
protected Component component;
public Decorator(Component component){
this.component = component;
}
public abstract double calPrize(String userName);
}
/**
*
* 当月奖金计算规则
*/
public class MonthPrizeDecorator extends Decorator{
public MonthPrizeDecorator(Component component) {
super(component);
}
public double calPrize(String userName) {
//先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)
double money = this.component.calPrize(userName);
//计算本奖金 对应员工的业务额的3%
double prize = BasicPrize.saleMoney.get(userName)*0.03;
System.out.println(userName+"当月 业务奖金:"+prize);
return money + prize;
}
}
/**
* 累计奖金
*/
public class SumPrizeDecorator extends Decorator {
public SumPrizeDecorator(Component component) {
super(component);
}
public double calPrize(String userName) {
//先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)
double money = this.component.calPrize(userName);
//计算本奖金 累计业务额的0.1% 假设是100000
double prize = 100000*0.001;
System.out.println(userName+"当月 累计奖金:"+prize);
return money + prize;
}
}
/**
* 团队奖金
*/
public class GroupPrizeDecorator extends Decorator {
public GroupPrizeDecorator(Component component) {
super(component);
}
public double calPrize(String userName) {
//先计算被装饰对象的奖金(就是在加上本奖金之前的奖金)
double money = this.component.calPrize(userName);
//计算本奖金 本团队的业务额的1%
double sumSale = 0;
for(double sale: BasicPrize.saleMoney.values()){
sumSale += sale;
}
double prize = sumSale*0.01;
System.out.println(userName+"当月 团队奖金:"+prize);
return money + prize;
}
}
/**
* @Description
*/
public class Client {
public static void main(String[] args) throws FileNotFoundException {
//基础对象 奖金0
Component basePrice = new BasicPrize();
//当月奖金
Component salePrize = new MonthPrizeDecorator(basePrice);
//累计奖金
Component sumPrize = new SumPrizeDecorator(salePrize);
//团队奖金
Component groupPrize = new GroupPrizeDecorator(sumPrize);
//普通员工的奖金由两部分组成
double d1 = sumPrize.calPrize("小明");
System.out.println("========================小明总奖金:"+d1);
double d2 = sumPrize.calPrize("小陈");
System.out.println("========================小陈总奖金:"+d2);
double d3 = sumPrize.calPrize("小王");
System.out.println("========================小王总奖金:"+d3);
//王经理的奖金由三部分组成
double d4 = groupPrize.calPrize("张经理");
System.out.println("========================张经理总奖金:"+d4);
}
}
输出结果如下:
这里我们使用装饰器模式,主要是为了方便组合和复用。在一个继承的体系中,子类往往是互斥的,比方在一个奶茶店,它会有丝袜奶茶,红茶,果茶等,用户想要一杯饮料,一般都会在这些种类中选一种,不能一杯饮料既是果茶又是奶茶。然后用户可以根据自己的喜好添加任何想要的decorators,珍珠,椰果,布丁等,这些添加物对所有茶类饮品都是相互兼容的,并且是可以被允许反复添加的(同样的装饰器是否允许在同一个对象上装饰多次,视情况而定,像上面的奖金场景显然是不被允许的)
jdk中的装饰器模式
java.io包是用于输入输出的包,这里使用了大量的装饰器模式,我们再来体会一下装饰器模式的优点。
下图是jdk 输出流的一部分类图,很明显是一个装饰模式的类图,OutputStream是顶层父类,FileOutputStream和ObjectOutputStream是具体的被装饰类,FilterOutputStream是所有输出流装饰器的抽象父类
使用的代码如下:
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
为什么Java IO设计的时候要使用装饰器模式, 而J不设计⼀个继承 FileInputStream 并且⽀持缓存的 BufferedFileInputStream 类呢?
如果InputStream 只有⼀个⼦类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计⼀个孙⼦类BufferedFileInputStream,也是可以接受的。但实际上,继承 InputStream 的⼦类有很多。我们需要给每⼀个 InputStream 的⼦类,
再继续派⽣⽀持缓存读取的⼦类。
除此之外,我们还需要对功能进⾏其他⽅⾯的增强,⽐如下⾯的DataInputStream 类,⽀持按照基本数据类型(int、boolean、long 等)来读取数据。这种情形下,使用继承的方式的话类的继承结构变得⽆⽐复杂,代码维护起来也比较费劲。
按照装饰器模式的结构,我们可以继承FilterOutputStream实现自定义的装饰器,并且在使用的时候可以和jdk自带的装饰器对象任意组合。
我们可以实现一个简单的复制输出内容的OutputStream装饰器
public class DuplicateOutputStream2 extends FilterOutputStream {
/**
* Creates an output stream filter built on top of the specified
* underlying output stream.
*
* @param out the underlying output stream to be assigned to
* the field <tt>this.out</tt> for later use, or
* <code>null</code> if this instance is to be
* created without an underlying stream.
*/
public DuplicateOutputStream2(OutputStream out) {
super(out);
}
//将所有的内容复制一份输出 ab 变成aabb
public void write(int b) throws IOException {
super.write(b);
super.write(b);
}
}
装饰器类是否可以直接实现Component父类?
以输出流为例,如果直接继承OutputStream来实现自定义装饰器
public class DuplicateOutputStream extends OutputStream {
private OutputStream os;
public DuplicateOutputStream(OutputStream os){
this.os = os;
}
//将所有的内容复制一份输出 ab 变成aabb
public void write(int b) throws IOException {
os.write(b);
os.write(b);
}
}
public class ClientTest {
public static void main(String[] args) throws IOException {
DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(new DuplicateOutputStream2(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream);
DataOutputStream dataOutputStream1 = new DataOutputStream(new BufferedOutputStream(new DuplicateOutputStream(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream1);
}
public static void testOutputStream(DataOutputStream dataOutputStream) throws IOException {
DataInputStream dataInputStream = new DataInputStream(new FileInputStream("1.txt"));
dataOutputStream.write("bdsaq".getBytes());
dataOutputStream.close();
System.out.println(dataInputStream.available());
byte[] bytes3 = new byte[dataInputStream.available()];
dataInputStream.read(bytes3);
System.out.println("文件内容:"+new String(bytes3));
}
}
输出结果:
乍一看好像没什么区别,但是如果把BufferedOutputStream装饰器和自定义的装饰器互换。
DataOutputStream dataOutputStream2 = new DataOutputStream(new DuplicateOutputStream2(new BufferedOutputStream(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream2);
DataOutputStream dataOutputStream3 = new DataOutputStream(new DuplicateOutputStream(new BufferedOutputStream(new FileOutputStream("1.txt"))));
testOutputStream(dataOutputStream3);
输出结果:
使用了实现OutputStream的DuplicateOutputStream会出现没有正常输出数据,这是因为我们使用了BufferedOutputStream这个带缓存区的输出流,缓存区的输出流在缓存区没有满的情形下是不会进行输出操作的。一般情形下我们在调用jdk的DataOutputStream的close方法的时候会调用其传入的输出流的flush()方法,并且向下传递调用,BufferedOutputStream里的数据会正常输出。
在使用DuplicateOutputStream2的时候其调用关系是这样的:
dataOutputStream.close()–>duplicateOutputStream2.flush()–>bufferedOutputStream.flush()
使用DuplicateOutputStream的时候由于DuplicateOutputStream继承的OutputStream的flush()方法是空实现,所以不会继续往下调用bufferedOutputStream的flush()方法,故而最后没有得到输出内容
所以装饰器类需要继承Decorator抽象父类,而不是直接继承Component抽象类,我认为是为了在Decorator里实现一些共性的代码,以便在使用装饰器的时候能够更加自由,无视其组合顺序 (当然如果你的Decorator里没有任何逻辑代码,在合适的场景下你可以不定义抽象装饰器类)
总结
装饰器模式主要用于解决继承关系过于复杂的问题,通过组合来替代继承。
它主要的作⽤是给原始类添加增强功能。
除此之外,装饰器模式还有⼀个特点,那就是可以对原始类嵌套使⽤多个装饰器。为了满⾜这个应⽤场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接⼝。