初学装饰器模式的人可能会觉得很混乱,很多文章只说代码怎么写,但不说为什么这么写。由此产生了很多疑问在:为什么装饰器里面已经有了被装饰的对象,还要继承抽象类或实现接口?不是说不要继承吗?这么做有什么意义?看完这篇文章你将知其然且知其所以然。
假设我们开了一家饭馆,刚开始我们只做两种food,分别是rice和noodle,不使用装饰器模式。
定义food抽象类
public abstract class Food {
public String name;
public abstract String getName();
public abstract void cook();
}
定义rice和noodle类
public class Rice extends Food {
public Rice(){
this.name="饭";
}
@Override
public String getName() {
return this.name;
}
@Override
public void cook() {
System.out.println("炒饭");
}
}
public class Noodle extends Food {
public Noodle(){
this.name="面";
}
@Override
public String getName() {
return this.name;
}
@Override
public void cook() {
System.out.println("煮面");
}
}
继承的问题:假设饭店引入新食材鸡蛋,所以也就有了新菜式蛋炒饭和蛋面。我们可以选择继承的方式,实现饭和面的两个子类蛋炒饭和蛋面。又过了两天,我们可能又要加菜品,加一个肉,新增四个菜品,炒肉饭、猪肉面、蛋肉炒饭和蛋肉煮面,一下子需要增加四个类的定义。随着新食材的加入,子类的数量将越来越庞大。
装饰器的思想:为了解决上述的问题,我们可能会这么想。有没有一种方式,使得我们可以灵活地根据需要对一个food进行改造。当引入一个新食材的时候,我们通过方法调用(构造装饰器对象)而不是定义一个新类,就可以对food进行改造成为一个新的food,并且我们希望这个改造是可以嵌套的。比如一个炒饭food,先用蛋进行改造成为蛋炒饭food,当我们引入肉的时候,又可以嵌套地用肉对炒饭和蛋炒饭进行改造。我们把上述的改造称为装饰,就是装饰器模式的思想。需要牢记的一点是,我们希望装饰器类创建的对象不是一个装饰器,而是经过装饰的food对象。
装饰器的实现原理:虽然我们不需要为每一个菜式定义一个新类,但是食材作为food的装饰器还是需要定义的。一个装饰器之所以要继承food类,是要保证装饰器通过装饰food而构造的新对象也是一个food。这么做的原因,首先从字面理解,装饰是很轻的动作,一个food装饰完当然应该还是food,其次这么做可以方便对food类的多态使用,并且是实现嵌套调用的关键。装饰器从代码层面来看,首先它要继承被装饰对象的抽象类或接口。继承food类后,我们还需要调用原food的方法和属性,才能够加以装饰,构造出装饰后的food方法和属性。这就是为什么我们要把整个被装饰对象传进来,装饰不是无中生有,而是在原先的基础上进行。如以下代码中,使用被装饰对象的cook方法来构造装饰后对象的cook方法,用原food的name构造装饰后的name。
public class EggDecoration extends Food {
private Food food;
public EggDecoration(Food food){
this.food=food;
this.name=food.name+"加蛋";
}
@Override
public String getName() {
return this.name;
}
@Override
public void cook() {
this.food.cook();//被装饰对象的cook方法
System.out.println("煎鸡蛋");//装饰器在原对象基础上新增的装饰
}
}
public class MeatDecoration extends Food{
public Food food;
public MeatDecoration(Food food){
this.food=food;
this.name=food.name+"加肉";
}
@Override
public String getName() {
return this.name;
}
@Override
public void cook() {
this.food.cook();
System.out.println("炒肉");
}
}
使用装饰器
public class Test {
public static void main(String args[]){
Rice rice=new Rice();
EggDecoration eggRice=new EggDecoration(rice);//装饰后的对象不称为eggDecoration,因为装饰后仍然是food
MeatDecoration meatEggRice=new MeatDecoration(eggRice);//同理,如此命名可读性更好
System.out.println(meatEggRice.getName());
meatEggRice.cook();
}
}
运行结果
饭加蛋加肉
炒饭
煎鸡蛋
炒肉
上面的例子已经完成了装饰器模式,但是还存在改进的空间,可以定义装饰器的抽象类对装饰器进行规范
定义装饰器的抽象类
public abstract class FoodDecoration extends Food {
private Food food;
public FoodDecoration(Food food) {
this.food=food;
}
public String getName() {
return this.name;
}
public void cook() {
this.food.cook();
}
}
装饰器对装饰器的抽象进行实现
public class EggDecoration extends FoodDecoration {
private Food food;
public EggDecoration(Food food){
super(food);
this.name=food.name+"加蛋";
}
@Override
public void cook() {
super.cook();
System.out.println("煎鸡蛋");
}
}
public class MeatDecoration extends FoodDecoration{
public Food food;
public MeatDecoration(Food food){
super(food);
this.name=food.name+"加肉";
}
@Override
public void cook() {
super.cook();
System.out.println("炒肉");
}
}
优点:使用装饰器模式后,我们无需为每一种food和新食材的组合预先定义菜品,我们根据需要通过装饰器就可以灵活地装饰出各种新food,大大减少了定义类的数量。装饰器模式还实现了装饰和food的解耦,在不修改原food或定义子类重写原food方法的前提下,通过装饰就可以实现类的功能拓展。
缺点:“套娃”现象严重,代码不美观。当装饰层数多时,内存占用较高。如本例中的蛋肉炒饭有三层装饰,每一层嵌套装饰,除了装饰本身是一个food之外,还需要在内部保存一个被装饰的food对象。为了创建一个蛋肉炒饭占用了三个food对象的空间(若用继承方法只需要一个food的内存占用)。