装饰者模式
手抓饼问题:我们可以单点一个手抓饼,也可以给手抓饼添加一些配料,比如可以添加鸡蛋、培根、火腿等,有了这些配料之后,那么我们可以点一个鸡蛋手抓饼、鸡蛋火腿手抓饼、还可以点一个加了两个鸡蛋一个火腿的手抓饼……,组合有很多种,另外,我们还要能够计算出每个手抓饼的价格!
第一种方案:我首先想到的设计一个抽象父类Food(食物类),该抽象类包含一个抽象方法cost,用于计算手抓饼的价格,然后再根据客户的需求,创建各种子类去继承父类,实现cost方法,比如可以创建手抓饼类,鸡蛋手抓饼类,鸡蛋火腿手抓饼类……,有人可能会说,这样能够满足ocp原则,便于扩展,无可否认,这种方案确实满足了ocp原则,客户要点一个培根火腿手抓饼,那么我们就去创建一个培根火腿手抓饼子类去继承Food类,便于扩展,而且不会修改原来的代码;但是我们应该清楚,客户的需求有千万种,也就是说手抓饼的组合有千万种,如果客户每来一个需求我们就去创建一个子类,就会出现类爆炸,代价岂不是太大了?
方案一类图:
我们换一种方案:还是设计一个抽象父类(Food),让手抓饼类和调料类分别去继承Food类,这里我们将调料类设计成抽象类,该类聚合了Food类,cost方法提供空实现,让具体的鸡蛋类、培根类、火腿类去重写即可。
类图:
代码如下:
(为了便于知道某个类的功能,下面代码对类的命名并没有满足代码规范)
public abstract class Food {
//计算价格
public abstract int cost();
}
public class SZB extends Food{
@Override
public int cost() {
return 4;//单点手抓饼的价格是4元
}
}
public abstract class tiaoliao extends Food{
public Food food=null;
//注意:tiaoliao是一个抽象类,不能直接通过new创建对象,类的构造方法是为了给子类去调用的
public tiaoliao(Food food) {
this.food=food;
}
//默认空实现
@Override
public int cost() {
return 0;
}
}
public class Egg extends tiaoliao{
public Egg(Food food){
super(food);//调用父类的构造方法
}
@Override
public int cost() {
int value=2;//鸡蛋的价格
int cost = super.food.cost();//父类的价格
return value+cost;//总价格
}
}
public class huotui extends tiaoliao{
public huotui(Food food){
super(food);//调用父类的构造方法
}
@Override
public int cost() {
int value=3;//火腿的价格
int cost = super.food.cost();//父类的价格
return value+cost;//总价格
}
}
public class peigen extends tiaoliao{
public peigen(Food food){
super(food);//调用父类的构造方法
}
@Override
public int cost() {
int value=4;//培根的价格
int cost = super.food.cost();//父类的价格
return value+cost;//总价格
}
}
public class test {
public static void main(String[] args) {
Food food1=new SZB();
System.out.println("手抓饼的价格为:"+food1.cost());
Food food2=new Egg(food1);
System.out.println("鸡蛋手抓饼的价格为:"+food2.cost());
Food food3=new huotui(food2);
System.out.println("火腿鸡蛋手抓饼的价格为:"+food3.cost());
Food food4=new peigen(food3);
System.out.println("培根火腿鸡蛋手抓饼的价格为:"+food4.cost());
}
}
手抓饼的价格为:4
鸡蛋手抓饼的价格为:6
火腿鸡蛋手抓饼的价格为:9
培根火腿鸡蛋手抓饼的价格为:13
以上的方案就是装饰者模式,装饰者模式就是动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性, 装饰者模式也体现了开闭原则(ocp)
之前还在另一篇博客上看到关于装饰者模式的另一个定义:装饰模式是在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
装饰者模式类图:
上面例子中,手抓饼类就是被装饰者,而调料类就是装饰者,被装饰者被聚合到装饰者类里面,可以动态的将各种调料附加到被装饰者上,如果客户有新的需求或者要添加新的调料,都非常的方便,无需修改原来的代码,扩展性很好!
装饰者模式的思想就是使用了继承+聚合/组合,核心就是将被装饰者聚合到装饰者类里面,有人可能会问为什么不将装饰者聚合到被装饰者里面,例如上面的例子中就是将调料类装饰到手抓饼类,这样做的话被装饰者类里面需要进行逻辑判断,针对不同的调料执行不同的方法,那么如果增加新的调料需要修改原来的代码,违反ocp,不可取。
在生活中,我们能举出很多用到装饰者模式的例子:
- 奶茶问题:我们可以单点一杯奶茶,也可以给奶茶加珍珠、加椰果、加奶盖……
- 咖啡问题:我们可以点单品咖啡,也可以给咖啡加牛奶、加巧克力、加豆浆……
- 快递问题:我们可以给我们的物件加上报纸填充、塑料泡沫、纸板、木板等……
装饰者模式的优点
- 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加。虽然说装饰者类可能也有很多的子类,但是却比继承的方式大大减少子类的个数;比如前面说的手抓饼加调料的例子中,调料类(装饰者)虽然有火腿、鸡蛋、培根等子类,但是对于这些调料的组合却是可以使用多重装饰的方式,不断叠加,不需要更多的子类;相比于继承,火腿、鸡蛋、培根是子类,但是对于这些调料类的每一种组合又是新的子类,比如火腿鸡蛋类、火腿培根类;会导致子类的个数大大增加。
多重装饰指的是:一个已被装饰者加强的类也可以继续再被加强,有点像链式调用;例如手抓饼被鸡蛋装饰后,变成鸡蛋手抓饼,此时要实现火腿鸡蛋手抓饼,只需要再次对鸡蛋手抓饼做加强就可以了,而不是直接用火腿类去加强手抓饼类。
public class test {
public static void main(String[] args) {
Food food1=new SZB();
System.out.println("手抓饼的价格为:"+food1.cost());
Food food2=new Egg(food1);
System.out.println("鸡蛋手抓饼的价格为:"+food2.cost());
Food food3=new huotui(food2);
System.out.println("火腿鸡蛋手抓饼的价格为:"+food3.cost());
}
}
- 在一定程度上继承的方式也能解决装饰者模式的功能,使用子类的方式扩展一个类的功能,也满足ocp;但是在一些继承受限的场景下,就可以用装饰者模式代替继承,比如java中,如果要扩展功能的类被final修饰了,那么就不能被子类继承了。
装饰者模式在jdk源码中的体现
JAVA的IO就用到了装饰者模式,我们先附上类图:
- InputStream 是抽象类, 类似我们前面例子中的Food
- FileInputStream、StringBufferInputStream、ByteArrayInputStream是 InputStream 子类,类似我们前面的SZB,是被装饰者
- FilterInputStream 是 InputStream 子类:类似我们前面的tiaoliao,也就是装饰者抽象类,类内部聚合了InputStream类,源码是
protected volatile InputStream in
- BufferedInputStream、DataInputStream 是 FilterInputStream的子类,具体的装饰者,类似前面的Egg、peigen、huotui
从java的io结构中,可以充分体会到装饰器模式的灵活运用,我们创建的一个FileInputstream对象,可以使用各种装饰器让它具有不同的特别的功能,比如我们可以把它装饰成BufferedInputStream,它提供我们mark,reset的功能,这正是动态扩展一个类的功能的最佳体现。
//普通的FileInputStream对象
InputStream inputStream = new FileInputStream(filePath);
//装饰成 BufferedInputStream,提供mark、reset功能
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
//再进行一层包装,装饰成DataInputStream
DataInputStream dataInputStream = new DataInputStream(bufferedInputStream);