为什么要用装饰者模式?首先就抛出一个尖锐的问题。不得不继续吐槽我们泛滥使用的继承了。当频繁的继承使我们的代码成一条线的时候,或许就该考虑,怎么样在运行的时候扩展类的属性,或者说是装饰类,而不仅仅是在编译前期直接构思好继承逻辑。
继续引用经典的例子。带上场景,我们为一家急速发展的饮品店开发一款订单软件。
饮品作为订单的核心我们选择作为一个父抽象类,所有子类型饮品必须继承父类。作为饮品,我们需要标注它的价钱以及类型。
但是,如果饮品材料价钱修改/增加新的饮品,我们就不得不依次修改每个价钱/增加一个新的类,如果我们有100多种类饮品,一个一个修改,简直噩梦好吧。
也许这时候你会想,我为什么要设计这么多 类,直接使用实例变量和继承,就可以跟踪饮料的材料。
恩,接着画图。
这个思路是我们从父类(Beverage)入手,加上boolean属性,来确定子类饮品是否需要某种调料。然后cost()内通过材料的价钱和算出饮料价格。
但是呢,这种设计也存在着一些问题。
1.调料价格改变必须要修改代码。
2.一旦出现新的调料,我们不得不加上新方法,同时需要修改父类中的cost方法
引入一个设计原则:类应该对扩展开发,对修改关闭。简单的说,就是允许我们的我们的类进行扩展,在不修改现有代码的情况 下,适应新的行为改变。
我们以DarkRoast举例,他的成分为Mocha和Whip,我们需要通过cost()计算所有调料和。我们的想法是所有的调料都继承一个类。一步一步的价格累加最终获得总价钱。
1 DarkRoast对象继承Beverage,拥有cost()方法。
2 添加第一种调料Mocha,建立第一个装饰者。
3 添加第二种调料Whip,建立第二个装饰者。
最最外围开始计算价钱,Whip的价钱为a,加上里面一层Mocha的价钱b,最里面就为DarkRoast价钱a+b,一层一层递进。
我们发现装饰者(调料)和被装饰者拥有同一个方法cost(),也就是说,他们也许拥有同一个父类,但是基于调料的属性,我们应该使调料继承调料类,调料总类和饮品类拥有同一个父类。
看下装饰者模式的说明。动态的将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的扩展方案。
我们用一个网上的标准类图来定义一下装饰者模式。
我们的成分类CondimentDececorator于各种饮品处于同一位置,也就是说,装饰者于被装饰者必须是一样的类型。通过继承的方式,不是继承行为,而是为了达到类型匹配。当我们需要新的饮料类型时,不是通过继承获得行为,而是通过组建间的组合获得新的饮料。
既然类图已经确定,直接上代码吧。
1.首先我们定义饮料父类。
public abstract class Beverage {//父类作为一个抽象类呈现
String descriptionString = "未知饮料";
//类型描述父类实现
public String getDescription() {
return descriptionString;
}
//子类需要实现的抽象方法
public abstract double cost();
}
2.实现调料的父类,必须能够替代饮料类,所以继承自饮料类,从而获得同样的类型。
public abstract class CondimentDecorator extends Beverage {
// 调料材料子类必须实现的抽象方法
public abstract String getDescription();
}
3.关于2个基类已经定义完毕,我们设置下具体的被装饰着(具体的饮料类型)。具体饮料必须需要描述,让客人明白这是什么饮料,也必须定义价格,让客人可以付款。
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "House Blend Coffee";
}
@Override
public double cost() {
// TODO Auto-generated method stub
return 5;
}
}
仔细推敲下我们写的类图,我们已经有了抽象组件Beverage,具体组件HouseBlend,也有了抽象装饰者CondimentDecorator,我们需要实现具体的装饰者——具体的材料。
/*饮料配料-摩卡*/
public class Mocha extends CondimentDecorator {
Beverage beverage;
double myCost = 20;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDesCription() {
return beverage.getDescription() + ",Mocha";
}
@Override
public double cost() {
return myCost + beverage.cost();
}
}
/*饮料配料-烤培*/
public class Whip extends CondimentDecorator{
Beverage beverage;
double myCost = 20;
public Whip(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDesCription() + ",Whip";
}
@Override
public double cost() {
return myCost + beverage.cost();
}
}
关于构造方法,我们需要引入被修饰对象的实例变量,所以就作为构造参数输入了。
关于description和cost方法,我们都是采用获取被修饰对象的属性,然后才去累加的方法作为新的输入,这样,新的属性能够一直叠加下去(比如计算价格)。也就是说我们计算Mocha的价钱,我们首先获取被装饰对象的价钱,然后加上自身价值,算出总的金额。
我们用一段测试代码测试下我们的订单。
public class TestBeverage {
public static void main(String args[]) {
//不加调料的HouseBlend
Beverage beverage = new HouseBlend();
System.out.println(beverage.getDescription()+" $"+beverage.cost());
//加入调料的HouseBlend
Beverage beverage2 = new HouseBlend();
beverage2=new Soy(beverage2);
beverage2=new Mocha(beverage2);
System.out.println(beverage2.getDescription()+" $"+beverage2.cost());
}
}
结果如下:
据说,java.io包内的类很多都是装饰者,我们常见的文件读写会用到的哟~下面举个典型的例子。
BufferInputStream,LineNumberInputStream都是扩展自抽象的装饰类FileInputStream。
java.io符合装饰者模式所有特点。顺着看一看,比如Writer和Reader流和输入输出流十分类似。但是这些我们也有很多困扰,IO包中的小类实在太多了,我到现在记不清,有时候还是避免不了麻烦百度童鞋。