装饰器模式是指允许对一个现有的对象加入其它额外的功能并且不改变其原来的结构,属于结构型模式。这种模式通常会创建一个装饰类来包装原有的类以达到装饰的效果。
装饰器模式,顾名思义,就是对已经存在的某些类进行装饰,以此来扩展一些功能。其结构图如下:
Component为统一接口,也是装饰类和被装饰类的基本类型。
ConcreteComponent为具体实现类,也是被装饰类,他本身是个具有一些功能的完整的类。
Decorator是装饰类,实现了Component接口的同时还在内部维护了一个ConcreteComponent的实例,并可以通过构造函数初始化。而Decorator本身,通常采用默认实现,他的存在仅仅是一个声明:我要生产出一些用于装饰的子类了。而其子类才是赋有具体装饰效果的装饰产品类。
ConcreteDecorator是具体的装饰产品类,每一种装饰产品都具有特定的装饰效果。可以通过构造器声明装饰哪种类型的ConcreteComponent,从而对其进行装饰。
以下举一个电脑配置的例子说明。
DEMON1
首先创建一个电脑的接口,给出两个抽象方法,分别是获取名字和获取价格:
/**
* 抽象电脑类
* @author 樱桃肉丸子
*/
public interface AbstractComputer {
String name();
double price();
}
创建一个普通电脑类实现上面的接口:
/**
* 普通电脑类
* @author 樱桃肉丸子
*/
public class Computer implements AbstractComputer {
@Override
public String name() {
return "普通电脑";
}
@Override
public double price() {
return 3000;
}
}
现在有一个需求就是需要给这台普通的电脑加一个内存或是加一个硬盘,即在现有的基础上对该电脑进行装饰。
首先需要创建一个装饰类接口来确定加入的是哪种配置,这样逻辑上是说得通的,电脑配件依附于电脑,和电脑有相同的属性。
/**
* 装饰类接口
* @author 樱桃肉丸子
*/
public interface Device extends AbstractComputer {
String name();
double price();
}
分别创建装饰类的实现类来定义具体装饰内容:
/**
* 内存类
* @author 樱桃肉丸子
*/
public class Memory implements Device {
public AbstractComputer computer;
public Memory(AbstractComputer computer) {
this.computer = computer;
}
@Override
public String name() {
return computer.name()+"加内存";
}
@Override
public double price() {
return computer.price()+300;
}
}
/**
* 硬盘类
* @author 樱桃肉丸子
*/
public class HardDisk implements Device {
public AbstractComputer computer;
public HardDisk(AbstractComputer computer) {
this.computer = computer;
}
@Override
public String name() {
return computer.name()+"加硬盘";
}
@Override
public double price() {
return computer.price()+500;
}
}
最后写一个demo来看一下装饰效果如何:
public class DecoratorPatternDemo {
public static void main(String[] args) {
//创建普通的电脑
Computer computer = new Computer();
//给普通电脑加硬盘
HardDisk hardDisk = new HardDisk(computer);
System.out.println(hardDisk.name());
System.out.println(hardDisk.price());
//给普通电脑加内存
Memory memory = new Memory(computer);
System.out.println(memory.name());
System.out.println(memory.price());
//给普通电脑加硬盘和内存
Memory memory1 = new Memory(hardDisk);
System.out.println(memory1.name());
System.out.println(memory1.price());
}
}
输出结果:
总结:
装饰器的价值就体现在“装饰”上,他只是装饰,并不会改变原来对象的性质。当然,以上功能也可以通过继承实现,但当一个对象出现一个新的功能就要创建一个子类这样是不合理的。装饰器模式动态添加功能大大提高了拓展性,一定程度上降低了子类的数量,降低了耦合度。
DEMON2
现在很多人都喜欢工作之余喝杯咖啡,现在有两种咖啡:Decaf、Espresso,另有两种调味品:Mocha、Whip,每种咖啡的价格不同,所加调料的价格也不同,那么如何计算不同口味的咖啡的价格?
最简单的方案当然就是组合出四种咖啡,固定他们的价格,想要哪种组合就直接付多少钱,不考虑咖啡和调味品之间的关系,代码冗余不再赘述
但是当咖啡和调味品的种类很多时,将会产生大量的类,如果一种咖啡的价格发生变动,需要找到所有相关的类逐一修改
改进一下,将调味品作为Coffee类的属性,比起设计一,类的数量大大减少,相应的,程序结构也更加清晰
public class Coffee {
private boolean mocha;
private boolean whip;
public double cost(){double price = 0d;if(mocha){
price += 0.5;
}
if(whip){
price += 0.1;
}
return price;}
public void addMocha(){
this.mocha = true;
}
public void addWhip(){
this.whip = true;
}
}
考虑到下面几个问题,设计二有明显的不足:
1,如果调味品的种类较多,Coffee类将会变得相当庞大,难以维护
2,类本身不够灵活,无法处理顾客希望添加双倍的Mocha的场景
3,添加一种新的咖啡IceCoffee,如果IceCoffee不能加Mocha,由于IceCoffee类继承自Coffee类,IceCoffee类依然从父类继承了addMocha()方法,这就需要在IceCoffee类中重写一个空的addMocha()方法
最后,让我们的设计模式登场,对应结构图
抽象组件Component:对应Coffee类
具体组件ConcreteComponent:对应具体的咖啡,如:Decaf,Espresso
装饰者Decorator:对应调味品,如:Mocha,Whip
装饰模式有3个特点:
1,具体组件和装饰者都继承自抽象组件(Decaf、Espresson、Mocha和Whip都继承自Coffee),并且装饰者持有抽象组件的引用
2,可以使用装饰者组合具体组件创造出新的类(Mocha组合Decaf创造出MochaDecaf)
3,过程2可以重复,直到创造出需要的类
看到这里是不是想到了Java中很常用的InputStream和OutputStream类?没错,它们都是使用装饰模式设计的
装饰模式的缺点:
1,装饰模式虽然扩展性较高,但是没有设计二简洁,类的数量略多(比设计一少很多),如何取舍可扩展性和简洁性是个问题。如果需求比较明确,并且后期发生变化的概率不大,没必要直接使用装饰模式,设计二更快速高效
2,很难搞清楚一个类究竟被装饰了多少层,可能是1层,也可能是100层
3,在某些场景下,可能需要按照一定的顺序进行装饰,稍不注意,就会产生异常
装饰模式与建造者模式之间的区别:
装饰模式的构造过程是不稳定的(是否需要装饰,装饰多少层都是可以自由调整的),建造者模式的建造过程是稳定的