装饰者模式
装饰者模式是java IO流中使用的一个经典模式,本文会简单介绍装饰者模式的原理和解决的问题,并且以一个咖啡demo来演示装饰者模式。
装饰者模式的定义
装饰者模式遵守的设计原则:开闭原则(类应该支持扩展,而拒绝修改)
装饰者模式通过组合的方式扩展对象的特性,这种方式允许我们在任何时候对对象的功能进行扩展甚至是运行时扩展,而若我们用继承来完成对类的扩展只能在编译时实现,可以比较动态地对对象进行扩展,某些情况下,装饰者模式比继承更加灵活。
装饰者模式的一些特征
- 装饰者(decorator)和被装饰(扩展)的对象有着相同的超类。
- 可以用多个装饰者装饰一个对象。
- 可以用装饰过的对象替换代码中的原对象,(因为有相同的超类)
- 装饰者可以在委托(delegate,即调用被装饰的类的成员完成一些工作)被装饰者的行为完成之前或者之后加上他自己的行为
- 一个对象在任何时候都能被装饰,甚至是运行时。
装饰者模式的结构
**Component:**一般是一个抽象类,表示一组有着某种用途的基类,包含着这些类最基本的特性。
**ConcreteComponent:**继承自Component,一般是一个有实际用途的类,这个就是直接的被装饰者。
**Decorator:**继承自Component,装饰者需要实现的接口(可以是抽象类),用来保证装饰者和被装饰者用共同超类,并保证每一个装饰者有一些必须具有的性质。
**ConcreteDecorator:**继承自Decorator,用来装饰Component类型的类(ConcreteComponent),但是不能装饰抽象类,为其添加新的特性,可以在委托被装饰者的行为完成之前或之后的任意时候。
装饰者模式的实例
一个比较经典的咖啡案例:咖啡馆提供不同种口味的咖啡和咖啡的调料,我们需要一个程序来描述并且计算出任意一种咖啡和任意几种调料搭配在一起的价格。
最初继承的设计
最容易想到的是用继承去实现这个需求,定义一个超类,其中只对cost做一个抽象,而让所有的咖啡和调料品之间的组合都去继承这个超类,这样一个设计的坏处是类会爆炸,并且当一种咖啡中调料要变化的时候,要修改原来的类,并且会造成大量的冗余。
好一点的调料放入超类方案
也可以想到的是将调料作为一个Boolean变量放入超类中,这样真正的咖啡在继承超类的同时也可以设置想配合的不同调料品。但是这样做的一个缺陷点是如果要引入一个新的调料或者丢弃一个原来的调料,则要对超类进行修改,灵活性受到了很大的挑战。
用装饰者模式去做这个需求
这就是装饰者模式解决这个咖啡馆项目的设计图。
- 可以看到有一个超类Drink,这个就相当于我们装饰者模式中的Component,可以看到在这个超类中有描述变量、价格变量和cost方法。同时cost方法是一个抽象方法,而子类去实例化时去初始化对应的描述和价格变量。
对应的代码:
/**
* 装饰者模式:动态的将新功能附加到对象上,在对象扩展方面,它比继承更有弹性
* 咖啡和调料这么一个项目
* 饮料作为装饰者模式的一个主体 定义一些饮料都有的方法 同时主体也要作为变量在装饰者中定义,装饰者也要继承主体
*
* Drink是一个超类
* @date 2019/3/2 17:53
*/
@Setter
public abstract class Drink {
private String desc;
@Getter
private float price=0f;
public String getDesc() {
return desc + "-" + this.getPrice();
}
/**
* 供子类去实现计算价格的方法 抽象方法
* @return
*/
public abstract float cost();
}
- 左边的ShortBlack、Decaf等单体咖啡是装饰者模式中的ConcreteComponent,这些子类中都去实现了cost方法,这里注意这个cost方法的实现,因为每个子类都要去实现cost方法,所以可以做一个中间层去实现这个cost方法,对于这里的cost即为返回子类实例化时在构造函数中填充的父类中的price变量值。
/**
* 单体的一个中间层,这里实现了cost方法
* 这里coffee单体 继承了Drink超类,其实cost的计算就是计算他们给price赋值的价格
*
* 引入新的单体咖啡时,不会修改之前的单体咖啡代码,只需要继承这个中间层类即可
*
* Created by zlj on 2019/3/3.
*/
public class Coffee extends Drink {
@Override
public float cost() {
return super.getPrice();
}
}
一个单体咖啡子类的写法:
/**
* Created by zlj on 2019/3/3.
* 一种单体咖啡
*/
public class LongBlack extends Coffee {
public LongBlack() {
super.setDesc("LongBlack");
super.setPrice(5.0f);
}
}
- Decorator类即装饰者模式中的Decorator,即为装饰者类的一个抽象,这个类由装饰者模式的定义结构我们可以知道也是继承了Component也就是Drink类的,同时,它也定义了一个Drink类型的变量,这里即用父类定义了要装饰的单体咖啡。因为它继承自Drink类,所以它也要去实现抽象方法cost方法,这里注意因为要在调用时展示一个咖啡的描述和价格时,作为装饰者需要将被装饰者和其他组合在一起的装饰者价格和描述一起展示出来,所以实现是将装饰者的描述和价格 同 被装饰者的描述和价格一起递归显示,所以代码如下,这里的Decorator也相当于真正装饰者的一个抽象或者中间层。这里也要注意装饰者可以装饰一个单体咖啡,也可以装饰一个被装饰过的装饰者。
/**
* 装饰者的一个中间层 其实就是装饰者类 这里装饰的是主体咖啡, 而继承装饰者的都是咖啡中的调料
*
* 1. 装饰者类是要继承主体类
* 2. 装饰者可以直接装饰主体,也可以是已经被装饰者包装过的,就是多个调料
* 3. 装饰者要定义一个主体变量在其中
*
* 当要拓展 一个新的装饰者时,直接继承装饰者中间层类即可。
* Created by zlj on 2019/3/3.
*/
public class Decorator extends Drink {
/**
* 被装饰的主体变量
*/
private Drink obj;
public Decorator(Drink obj) {
this.obj = obj;
}
/**
* 这里也去实现cost方法,因为本身装饰者也是有价格的
* 这里的obj.cost() 如果obj是一个被装饰过的类,这里会进行递归计算
* @return
*/
@Override
public float cost() {
return super.getPrice() + obj.cost();
}
@Override
public String getDesc() {
return super.getDesc() + "&&" + obj.getDesc();
}
}
- 图中的Soy、Milk类就是其中的调料类,这个要继承Decorator,作用其实就是ConcreteDecorator。这里的作用其实比较简单,其实就是传入被装饰对象(上边也提到这里可以是单体咖啡和被装饰过的装饰者),还有就是把描述和价格填上。
/**
* 单个继承装饰者的调料类
* 要在构造函数中将装饰的对象 设置进去
*
* Created by zlj on 2019/3/3.
*/
public class Chocolate extends Decorator {
public Chocolate(Drink obj) {
super(obj);
super.setDesc("Chocolate");
super.setPrice(1.0f);
}
}
简单测试
做一个简单的程序测试一下装饰者模式:
public class TestMain {
public static void main(String[] args) {
/**
* 只是点了一个单体咖啡
*/
Drink order1;
order1 = new Espresso();
System.out.println(order1.getDesc());
System.out.println("*******************************");
/**
* 点有调料的咖啡
*/
Drink order2;
// 先是一杯单体咖啡
order2 = new LongBlack();
// 加牛奶
order2 = new Milk(order2);
// 加巧克力
order2 = new Chocolate(order2);
// 加盐
order2 = new Soy(order2);
System.out.println(order2.getDesc() + "总共钱: " + order2.cost());
}
}
// 运行结果:
Espresso-2.0
*******************************
Soy-3.0&&Chocolate-1.0&&Milk-2.0&&LongBlack-5.0总共钱: 11.0
java中的装饰者模式
文章一开始就提到了java中的IO流使用了装饰者模式,这里简单的去看一个FilterInputStream类,而它继承的InputStream类即装饰者中的Component类,该类其实就是装饰者的中间层,可以看到其中定义了InputStream的变量引用,而其子类BufferInputStream、DataInputStream、LineNumberInputStream等是ConcreteDecorator。结构如下:
也可以看到代码:
public
class FilterInputStream extends InputStream {
// 被装饰的变量
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return in.read(b, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public int available() throws IOException {
return in.available();
}
public void close() throws IOException {
in.close();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
public synchronized void reset() throws IOException {
in.reset();
}
public boolean markSupported() {
return in.markSupported();
}
}
再唠叨几句
装饰者和继承
装饰者模式中用到了继承,但是这里装饰者模式用继承是为了保证装饰者和被装饰者有共同的超类,而不是为了给被装饰的类扩展新的特性,而装饰者模式中新的特性是通过类与类的组合(has-a的关系)而得到的,所以把装饰者模式和继承看做同一类设计思想是不恰当的。
装饰者模式的优点
针对第一种继承的方案:装饰者模式减少了代码的类,只定义了基础的Component和Decorator,而是通过组合的方式实现具体的咖啡的。
针对第二种在超类中定义Boolean变量的方案,装饰者模式提高了灵活性,当你需要加入新的调料或者单体咖啡时,只需定义新的ConcreteComponent和ConcreteDecorator即可。而当你想对一个已有加过调料的咖啡进行再装饰时,也只需要将对应的调料装饰上去即可,价格和描述即可递归显示。
参考
http://www.cnblogs.com/coffeeSS/p/5405787.html
《headFirst 设计模式》
代码在github: https://github.com/zhanglijun1217/design_mode/tree/master/src/main/java/decorator