Java设计模式之java装饰者模式详解
作者:大忽悠爱忽悠
介绍
装饰者模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。
在装饰者模式中,为了让系统具有更好的灵活性和可扩展性,我们通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类
装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任。换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不使用创造更多子类的情况下,将对象的功能加以扩展。
装饰模式的类图如下:
角色
- Component(抽象构件):给出一个抽象接口,以规范准备接收附加责任的对象。
- ConcreteComponent(具体构件):定义一个将要接收附加责任的类。
- Decorator(抽象装饰类):持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
- ConcreteDecorator(具体装饰类):负责给构件对象“贴上”附加的责任。
由于具体构件类和装饰类都实现了相同的抽象构件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
装饰模式的核心在于抽象装饰类的设计。
示例代码
抽象构件角色
public interface Component { public void sampleOperation(); }
具体构件角色
public class ConcreteComponent implements Component { @Override public void sampleOperation() { // 写相关的业务代码 } }
装饰角色
public class Decorator implements Component{ private Component component; public Decorator(Component component){ this.component = component; } @Override public void sampleOperation() { // 委派给构件 component.sampleOperation(); } }
具体装饰角色
public class ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component component) { super(component); } @Override public void sampleOperation() { super.sampleOperation(); // 写相关的业务代码 } }
public class ConcreteDecoratorB extends Decorator { public ConcreteDecoratorB(Component component) { super(component); } @Override public void sampleOperation() { super.sampleOperation(); // 写相关的业务代码 } }
星巴克咖啡的例子
方案一
加入不同调料的咖啡,例如:蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha,也就是巧克力风味)或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分。
方案二 :将调料内置到Drink类中
这种设计虽然满足了现在的需求,但是我们想一下,如果出现下面情况,我们怎么办,
①、调料价钱的改变会使我们更改现有代码。
②、一旦出现新的调料,我们就需要加上新的方法,并改变超类中的cost()方法。
③、以后可能会开发出新饮料。对这些饮料而言(例如:冰茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法,例如:hasWhip()(加奶泡)。
④、万一顾客想要双倍摩卡咖啡,怎么办?
很明显,上面的设计并不能够从根本上解决我们所碰到的问题。并且这种设计违反了 开放关闭原则(类应该对扩展开放,对修改关闭。)。
那我们怎么办呢?好啦,装饰者可以非常完美的解决以上的所有问题,让我们有一个设计非常nice的咖啡馆。
方案三:装饰者模式
这里的Coffee是一个缓冲层,负责将抽取出所有具体咖啡的共同点
代码演示
饮料抽象类:
public abstract class Drink { protected String decription="";//描述 public String getDecription() { return decription; } public abstract Integer cost();//返回饮料的价格 }
缓冲层:抽取出所有咖啡类的共同特征,即计算价钱
//缓冲层----所有种类咖啡的共同点抽取出来 public abstract class Coffee extends Drink { //共同特点:计算价格 @Override public Integer cost() { //价格从0累加 return 0; } }
具体的咖啡类:
public class LongBlack extends Coffee { LongBlack() { decription="美式咖啡"; } @Override public Integer cost() { return 15; } } public class ChinaBlack extends Coffee { ChinaBlack() { decription="中式咖啡"; } @Override public Integer cost() { return 10; } } public class Espresso extends Coffee { //设置描述信息 Espresso() { decription="意大利咖啡"; } @Override public Integer cost() { //意大利咖啡20元 return 20; } }
抽象装饰者
//装饰者 public abstract class Decorator extends Drink { @Override public abstract String getDecription(); }
具体装饰者—即调料
public class Milk extends Decorator{ Drink drink; Milk(Drink drink) { this.drink=drink; } @Override public String getDecription() { return "加了牛奶的"+this.drink.getDecription(); } @Override public Integer cost() { return this.drink.cost()+3; } } public class Chocolate extends Decorator{ //用一个实例变量记录饮料,也就是被装饰者 Drink drink; Chocolate(Drink drink) { this.drink=drink; } @Override public String getDecription() { return "加了巧克力的"+drink.getDecription(); } @Override public Integer cost() { //在原有饮料价格的基础上加上调料味的价格 return 5+drink.cost(); } }
测试
public class test { @Test public void test() { //模拟下单 //首先点一个美式咖啡,不加任何调料 Drink drink=new LongBlack(); System.out.println("购买了"+drink.getDecription()+" 花了"+drink.cost()); //给美式咖啡加一个巧克力 drink=new Chocolate(drink); System.out.println("购买了"+drink.getDecription()+" 花了"+drink.cost()); //给美式咖啡再加一个牛奶 drink=new Milk(drink); System.out.println("购买了"+drink.getDecription()+" 花了"+drink.cost()); //再把牛奶和巧克力加一次 drink=new Chocolate(drink); System.out.println("购买了"+drink.getDecription()+" 花了"+drink.cost()); drink=new Milk(drink); System.out.println("购买了"+drink.getDecription()+" 花了"+drink.cost()); System.out.println("===================================================="); //简化写法 Drink d=new Chocolate(new Milk(new ChinaBlack())); System.out.println("购买了"+d.getDecription()+" 花了"+d.cost()); } }
装饰者模式的简化
大多数情况下,装饰模式的实现都要比上面给出的示意性例子要简单。
如果只有一个ConcreteComponent类,那么可以考虑去掉抽象的Component类(接口),把Decorator作为一个ConcreteComponent子类。如下图所示:
如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。甚至在只有两个ConcreteDecorator类的情况下,都可以这样做。如下图所示:
透明性的要求
装饰模式对客户端的透明性要求程序不要声明一个ConcreteComponent类型的变量,而应当声明一个Component类型的变量。
用顶层抽象父类指向具体子类,以多态的形式实现透明性要求
应该像下面这样写:
Drink drink=new LongBlack(); //给美式咖啡加一个巧克力 drink=new Chocolate(drink);
而不是这样写
Drink drink=new LongBlack(); //给美式咖啡加一个巧克力 Chocolate drink=new Chocolate(drink);
半透明的装饰模式
然而,纯粹的装饰模式很难找到。装饰模式的用意是在不改变接口的前提下,增强所考虑的类的性能。
在增强性能的时候,往往需要建立新的公开的方法。
比如巧克力可以单独售卖,即售卖巧克力棒,那么这里巧克力类里面需要新增加一个sell方法,用于单独售卖
这就导致了大多数的装饰模式的实现都是“半透明”的,而不是完全透明的。换言之,允许装饰模式改变接口,增加新的方法。这意味着客户端可以声明ConcreteDecorator类型的变量,从而可以调用ConcreteDecorator类中才有的方法:
Drink drink=new LongBlack(); //给美式咖啡加一个巧克力 Chocolate drink=new Chocolate(drink); //售卖巧克力棒 drink.sell();
半透明的装饰模式是介于装饰模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰模式实际上是半透明的装饰模式,这样的装饰模式也称做半装饰、半适配器模式。
装饰模式的优点
- 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
- 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合 “开闭原则”。
装饰模式的缺点
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
- 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
装饰模式注意事项
(1) 尽量保持装饰类的接口与被装饰类的接口相同,这样,对于客户端而言,无论是装饰之前的对象还是装饰之后的对象都可以一致对待。这也就是说,在可能的情况下,我们应该尽量使用透明装饰模式。
(2) 尽量保持具体构件类是一个“轻”类,也就是说不要把太多的行为放在具体构件类中,我们可以通过装饰类对其进行扩展。
(3) 如果只有一个具体构件类,那么抽象装饰类可以作为该具体构件类的直接子类。
适用场景
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)
设计模式在JAVA I/O库中的应用
装饰模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了
由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式。
Java I/O库的对象结构图如下,由于Java I/O的对象众多,因此只画出InputStream的部分。
下面是使用I/O流读取文件内容的简单操作示例。
public class IOTest { public static void main(String[] args) throws IOException { // 流式读取文件 DataInputStream dis = null; try{ dis = new DataInputStream( new BufferedInputStream( new FileInputStream("test.txt") ) ); //读取文件内容 byte[] bs = new byte[dis.available()]; dis.read(bs); String content = new String(bs); System.out.println(content); }finally{ dis.close(); } } }
观察上面的代码,会发现最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream处理,再把处理后的对象传递给了DataInputStream对象进行处理,这个过程其实就是装饰器的组装过程,FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器。
透明和半透明的装饰模式的区别
理想的装饰模式在对被装饰对象进行功能增强的同时,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。
而适配器模式则不然,一般而言,适配器模式并不要求对源对象的功能进行增强,但是会改变源对象的接口,以便和目标接口相符合。
装饰模式有透明和半透明两种,这两种的区别就在于装饰角色的接口与抽象构件角色的接口是否完全一致。
透明的装饰模式也就是理想的装饰模式,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。
相反,如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰模式也是可以接受的,称为“半透明”的装饰模式,如下图所示
在适配器模式里面,适配器类的接口通常会与目标类的接口重叠,但往往并不完全相同。换言之,适配器类的接口会比被装饰的目标类接口宽。
显然,半透明的装饰模式实际上就是处于适配器模式与装饰模式之间的灰色地带。如果将装饰模式与适配器模式合并成为一个“包装模式”的话,那么半透明的装饰模式倒可以成为这种合并后的“包装模式”的代表。
参考文章
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!