一、什么是Decorator模式
假如现在有一块蛋糕,如果只涂上奶油,其他什么都不加,就是奶油蛋糕。如果加上草莓,就是草莓奶油蛋糕。如果再加上一块黑色巧克力板,上面用白色巧克力写上姓名,然后插上代表年龄的蜡烛,就变成了一块生日蛋糕。
不论是蛋糕、奶油蛋糕、草莓蛋糕还是生日蛋糕,它们的核心都是蛋糕。不过,经过涂上奶油,加上草莓等装饰后,蛋糕的味道变得更加甜美了,目的也变得更加明确了。
程序中的对象与蛋糕十分相似。首先有一个相当于蛋糕的对象,然后像不断地装饰蛋糕一样地不断地对其增加功能,它就变成了使用目的更加明确的对象。
像这样不断地为对象添加装饰的设计模式被称为Decorator模式。Decorator指的是“装饰物”。
Decorator模式保证装饰边框与被装饰物的一致性。
Decorator模式的一个缺点是会导致程序中增加许多功能类似的很小的类。
二、Decorator模式示例代码
本章中的示例程序的功能是给文字添加装饰边框。这里所谓的装饰边框是指用“-”“+”“|”等字符组成的边框。下面是一个输出结果示例。
+-----------+
|Hello,world.|
+-----------+
2.1 类之间的关系
类的功能:
类图:
2.2 Display类
Display类是可以显示多行字符串的抽象类。
show是显示所有行的字符串的方法。在show方法内部,程序会调用getRows方法获取行数,调用getRowText获取该行需要显示的字符串,然后通过for循环语句将所有的字符串显示出来。show方法使用了getRows和 getRowText等抽象方法,这属于Tempate Method模式。
想要了解Tempate Method模式的朋友可以看我的文章:设计模式学习(六):Template Method模板方法模式
public abstract class Display {
/**
* 获取横向字符数
*/
public abstract int getColumns();
/**
* 获取纵向行数
*/
public abstract int getRows();
/**
* 获取第row行的字符串
*/
public abstract String getRowText(int row);
/**
* 全部显示
*/
public final void show() {
for (int i = 0; i < getRows(); i++) {
System.out.println(getRowText(i));
}
}
}
2.3 StringDisplay类
仅查看Display类的代码是不能明白程序究竟要做什么的。下面我们来看看它的子类——StringDisplay类。
StringDisplay类是用于显示单行字符串的类。由于StringDisplay类是Display类的子类,因此它肩负着实现Display类中声明的抽象方法的重任。
为了简单起见,这里我们以内存上的一字节长度的字符占界面上的一列为前提。
此外,仅当要获取第0行的内容时getRowText方法才会返回string字段。以本章开头的蛋糕的比喻来说,StringDisplay类就相当于生日蛋糕中的核心蛋糕。
public class StringDisplay extends Display{
//要显示的字符串
private String string;
public StringDisplay(String string) {
this.string = string;
}
/**
* @return 字符数
*/
@Override
public int getColumns() {
return string.getBytes().length;
}
/**
* 行数是 1
*/
@Override
public int getRows() {
return 1;
}
/**
* 仅当row为0时返回值
*/
@Override
public String getRowText(int row) {
if (row == 0) {
return string;
} else {
return null;
}
}
}
2.4 Border类
Border类是装饰边框的抽象类。虽然它所表示的是装饰边框,但它也是Display类的子类。
也就是说,通过继承,装饰边框与被装饰物具有了相同的方法。具体而言,Border类继承了父类的getcolumns、getRows、getRowText、show等各方法。从接口(API)角度而言,装饰边框(Border )与被装饰物( Display)具有相同的方法也就意味着它们具有一致性。
在装饰边框Border类中有一个 Display类型的display字段,它表示被装饰物。不过,display字段所表示的被装饰物并仅不限于StringDisplay的实例。因为,Border也是Display类的子类,display字段所表示的也可能是其他的装饰边框(Border类的子类的实例),而且那个边框中也有一个display字段。这样,大家应该能大致理解Decorator模式的结构了吧。
public abstract class Border extends Display {
//表示被装饰物
protected Display display;
protected Border(Display display) {
this.display = display;
}
}
2.5 SideBorder类
SideBorder类是一种具体的装饰边框,是Border类的子类。SideBorder类用指定的字符(borderchar)装饰字符串的左右两侧。例如,如果指定borderchar字段的值是“|”,那么我们就可以调用show方法,像下面这样在“被装饰物”的两侧加上“”。还可以通过构造函数指定borderchar字段。
|被装饰物|
SideBorder类并非抽象类,这是因为它实现了父类中声明的所有抽象方法。
display字段的可见性是protected,因此sideBorder类的子类都可以使用该字段。
public class SideBorder extends Border{
// 表示装饰边框的字符
private char borderChar;
/**
* @param display 被装饰字符串
* @param ch 装饰边框的字符
*/
protected SideBorder(Display display, char ch) {
super(display);
this.borderChar = ch;
}
@Override
public int getColumns() {
return 1 + display.getColumns() + 1;
}
@Override
public int getRows() {
return display.getRows();
}
@Override
public String getRowText(int row) {
// 被装饰物的字符串 加上 两侧边框字符
return borderChar + display.getRowText(row) + borderChar;
}
}
2.6 FullBorder类
FullBorder类与SideBorder类一样,也是Border类的子类。SideBorder类会在字符串的左右两侧加上装饰边框,而FullBorder类则会在字符串的上下左右都加上装饰边框。不过,在SideBorder类中可以指定边框的字符,而在FullBorder类中,边框的字符是固定的。
public class FullBorder extends Border {
public FullBorder(Display display) {
super(display);
}
@Override
public int getColumns() {
return 1 + display.getColumns() + 1;
}
@Override
public int getRows() {
return 1 + display.getRows() + 1;
}
@Override
public String getRowText(int row) {
if (row == 0) {
//下边框
return "+" + makeLine('-', display.getColumns()) + "+";
} else if (row == display.getRows() + 1) {
//上边框
return "+" + makeLine('-', display.getColumns()) + "+";
} else {
//其他边框
return "|" + display.getRowText(row - 1) + "|";
}
}
/**
* 连续地显示某个指定字符
* @param ch 指定的显示字符
* @param count 显示次数
* @return 显示出的字符
*/
private String makeLine(char ch, int count) {
StringBuffer buf = new StringBuffer();
for (int i = 0; i < count; i++) {
buf.append(ch);
}
return buf.toString();
}
}
2.7 Main类
Main类是用于测试程序行为的类。在Main类中一共生成了4个实例,即b1~b4,它们的作用分别是:
b1:将"Hello, world.”不加装饰地直接显示出来
b2:在b1的两侧加上装饰边框'#'
b3:在b2的上下左右加上装饰边框
b4:为"你好,世界。"加上多重边框
public class Main {
public static void main(String[] args) {
// b1:将"Hello, world.”不加装饰地直接显示出来
Display b1 = new StringDisplay("Hello, world");
// b2:在b1的两侧加上装饰边框'#'
Display b2 = new SideBorder(b1, '#');
// b3:在b2的上下左右加上装饰边框
Display b3 = new FullBorder(b2);
b1.show();
b2.show();
b3.show();
// b4:为"你好,世界。"加上多重边框
Display b4 = new SideBorder(
new FullBorder(
new FullBorder(
new SideBorder(
new FullBorder(
new StringDisplay("你好,世界。")
),'*'
)
)
),'/');
b4.show();
}
}
2.8 运行结果
b3、b2、b1对象图:
三、拓展思路的要点
3.1 接口(API)的透明性
在Decorator模式中,装饰边框与被装饰物具有一致性。具体而言,在示例程序中,表示装饰边框的Border类是表示被装饰物的Display类的子类,这就体现了它们之间的一致性。也就是说,Border类(以及它的子类)与表示被装饰物的Display类具有相同的接口(API )。
这样,即使被装饰物被边框装饰起来了,接口(API)也不会被隐藏起来。其他类依然可以调用getcolumns、getRows、getRowText 以及 show方法。这就是接口(API )的“透明性”。
在示例程序中,实例b4被装饰了多次,但是接口(API)却没有发生任何变化。
这得益于接口(API )的透明性,Decorator模式中也形成了类似于Composite模式中的递归结构。也就是说,装饰边框里面的“被装饰物”实际上又是别的物体的“装饰边框”。就像是剥洋葱时以为洋葱心要出来了,结果却发现还是皮。不过,Decorator模式虽然与Composite模式一样,都具有递归结构,但是它们的使用目的不同。Decorator模式的主要目的是通过添加装饰物来增加对象的功能。
3.2 在不改变被装饰物的前提下增加功能
在Decorator模式中,装饰边框与被装饰物具有相同的接口(API)。虽然接口(API )是相同的,但是越装饰,功能则越多。例如,用SideBorder装饰Display后,就可以在字符串的左右两侧加上装饰字符。如果再用FullBorder装饰,那么就可以在字符串的四周加上边框。此时,我们完全不需要对被装饰的类做任何修改。这样,我们就实现了不修改被装饰的类即可增加功能。
Decorator模式使用了委托。对“装饰边框”提出的要求(调用装饰边框的方法)会被转交(委托)给“被装饰物”去处理。以示例程序来说,就是SideBorder类的getColumns方法调用了display.getColumns ()。除此以外,getRows方法也调用了display.getRows()。
3.3 可以动态地增加功能
Decorator模式中用到了委托,它使类之间形成了弱关联关系。因此,不用改变框架代码,就可以生成一个与其他对象具有不同关系的新对象。
3.4 只需要一些装饰物即可添加许多功能
使用Decorator模式可以为程序添加许多功能。只要准备一些装饰边框(ConcreteDecorator角色),即使这些装饰边框都只具有非常简单的功能,也可以将它们自由组合成为新的对象。
这就像我们可以自由选择香草味冰激凌、巧克力冰激凌、草莓冰激凌、猕猴桃冰激凌等各种口味的冰激凌一样。如果冰激凌店要为顾客准备所有的冰激凌成品那真是太麻烦了。因此,冰激凌店只会准备各种香料,当顾客下单后只需要在冰激凌上加上各种香料就可以了。不管是香草味,还是咖啡朗姆和开心果的混合口味,亦或是香草味、草莓味和猕猴桃三重口味,顾客想吃什么口味都可以。Decorator模式就是可以应对这种多功能对象的需求的一种模式。
四、相关的设计模式
4.1 Adapter模式
Decorator模式可以在不改变被装饰物的接口(API)的前提下,为被装饰物添加边框(透明性)。
Adapter模式用于适配两个不同的接口(API )。
设计模式学习(三):Adapter适配器模式
4.2 Stragety模式
Decorator模式可以像改变被装饰物的边框或是为被装饰物添加多重边框那样,来增加类的功能。
Stragety模式通过整体地替换算法来改变类的功能。
设计模式学习(四):Strategy策略模式