什么是装饰者模式
顾名思义,就是给某一个物体添加一些装饰物,让其变得更加丰富多彩。比如一块蛋糕,什么都不加,它就是一块平平无奇的蛋糕,如果加上一些草莓,它就变成一块可口的草莓蛋糕,再加上一些巧克力,抹上一点奶油,插上几根蜡烛,哎呦,美美的生日蛋糕就成型了。
当然,无论你是草莓蛋糕还是生日蛋糕,本质上仍旧是一块蛋糕,只不过加上这些装饰物之后,味道更好了,目的也更加明确了。
我们写代码跟做蛋糕也是一样的,首先定义一个蛋糕的对象,然后不断地添加功能,这个对象就更加纯粹了,我们就可以更有目的性的去调用它。
类似这样不断为对象添加装饰物的模式就是 Decorator 设计模式。文字表述可能不够具体,下面就用代码和类图来解析一下。
Decorator 模式中的角色
首先我们画一下 Decorator 模式的类图,对其有一个直观的了解。
-
- Component: 增加功能的核心角色,也就是我们上面所说的蛋糕,是整个设计模式的核心,定义了具体的 API 接口。
-
- ConcreteComponent:实现了 Component 角色所定义的接口 API 的具体蛋糕,接下来就要为这个具体的蛋糕来添加装饰物。
-
- Decorator: 装饰物,该角色具有与 Component 角色相同的 API,在它内部保存了被装饰对象 component。
-
- ConcreteDecorator: 具体的装饰物。
通过这个类图,我们要发现几个重点:
- 装饰物与被装饰者具有一致性,也就是说这两者都是 Component 的子类,这就体现了它们之间的一致性,即使装饰之后,API 也不会隐藏起来,就保证了接口 API 的透明性。
- 由于这种透明的结构,就会形成一个递归结构,也就是说,装饰物里面的被装饰物同时又是其他物体的装饰物。
下面我们来看一个实例程序
示例程序
首先我们将实例程序的类图仿照 Decorator 的类图画出来,然后再一一解析各个类的作用
- Display:可以显示多行字符串的抽象类,getColumns 方法和 getRows 方法分别用于获取横向字符数和纵向字符数,getRowText 方法用于获取指定某一行的字符串,这些都是抽象方法,需要子类实现。show 方法显示所有行的字符串,在 show 方法内部会调用 getRows 方法获取行数,getRowText 方法获取该行需要显示的字符串,最后通过循环语句显示所有的字符串信息。
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));
}
}
}
- StringDisplay:Display 的子类,用于显示单行字符串的类,也是接下来的被装饰者。
public class StringDisplay extends Display {
private String str;
public StringDisplay(String str) {
this.str = str;
}
@Override
public int getColumns() {
return str.getBytes().length;
}
@Override
public int getRows() {
return 1;
}
@Override
public String getRowText(int row) {
if (row == 0) {
return str;
}
return null;
}
}
- Border:装饰边框的抽象类,虽然这是边框类,但它也是 Display 的子类,通过继承,装饰边框与被装饰者具有相同的方法。我们在创建 Border 时,会将一个 Display 类型的字段传入,传入的字段就表示被装饰者。
public abstract class Border extends Display{
protected Display display;
// 通过构造函数传入 被装饰者
protected Border(Display display) {
this.display = display;
}
}
- SideBorder:具体的装饰者,Border 的子类,用指定的字符装饰字符串的左右两侧,可以给字符两侧添加 “|”。
public class SideBorder extends Border{
// 两边的边框
public char borderChar;
protected SideBorder(Display display, char c) {
super(display);
borderChar = c;
}
// 字符数 = 字符串长度 + 左右两侧的边框
@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;
}
}
- FullBorder:具体装饰者2号,具体实现同 SideBorder,不过功能有所不同,添加的是上下边框。
public class FullBorder extends Border{
protected 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(), '-') + "+";
}
// 其他边框
return "|" + display.getRowText(row-1) + "|";
}
// 构造边框
public String makeLine(int count,char c){
StringBuffer sBuffer = new StringBuffer();
for(int i = 0;i<count;i++){
sBuffer.append(c);
}
return sBuffer.toString();
}
}
- Main: 我们使用测试类来试验一下
public class Main {
public static void main(String[] args) {
Display b1 = new StringDisplay("hello 你好");
Display b2 = new SideBorder(b1, '|');
Display b3 = new FullBorder(b2);
b1.show();
b2.show();
b3.show();
System.out.println("====================================");
Display b4 = new SideBorder(
new FullBorder(
new SideBorder(
new FullBorder(
new StringDisplay("hello world")), '|')
), '/');
b4.show();
}
}
看一下打印情况
hello 你好
|hello 你好|
+------------+
||hello 你好||
+------------+
====================================
/+---------------+/
/||+-----------+||/
/|||hello world|||/
/||+-----------+||/
/+---------------+/
测试类中,我们通过定义 StringDisplay 被装饰者,然后不断为其添加边框,上下边框和所有边框,最终得到以上的打印。是不是非常方便呢?为了便于您的理解,最好还是要自己实现一遍才能加深理解。
IO 包与 Decorator
我曾经写过一篇关于 io 的文章:一文了解IO框架 ,有兴趣的可以看看。
其实 java 的 io 包中就使用了装饰者模式,想一想我们如果去读一个文件? 我们可以像这样生成一个读取文件的实例:
Reader reader = new FileReader("path");
也可以像这样,读取文件时将文件添加到缓冲区:
Reader reader2 = new BufferedReader(new FileReader("path"));
或者像这样,管理行号:
Reader reader3 = new LineNumberReader(new BufferedReader(new FileReader("path")))
无论是 LineNumberReader 还是 BufferedReader 的构造函数,都会接收一个 Reader 类的实例,所以我们才可以想上面那样进行组合。
不同的 Reader 类,就像不同的装饰者一样,给最原始的 FileReader 类添加不同的功能添加剂,让其有更丰富的功能让我们使用,但是其本质仍然是一个 Reader 类。这就是装饰者的一个典型实现。
Context 与 Decorator
Android 开发者对于 Context 一定非常的熟悉, Android上下文Context的使用说明书 这里是一篇关于 Context 的文章,可以先了解一下。
Context 作为最基础的抽象类,其中定义了一些基础的接口 API,如图所示:
看一下 Context 的继承关系图:
跟 Decorator 的 UML 图对比一下,是不是非常的相似呢?
结语
- 在不改变被装饰物的前提下增加功能:在 Decorator 模式中,装饰者与被装饰者拥有相同的接口 API,随着装饰物的增多,功能也就越多,但是其本质并不会发生改变,蛋糕还是那个蛋糕。
- 使用委托:使用委托,将类之间的关系变为弱关联,可以在不更改框架代码的前提下,就可以生成一个与其他对象具有不同关系的心对象。
- 功能抽离:我们可以准备多种装饰者,将功能进行抽离,只需要对被装饰者添加不同的装饰物,就可以达到我们的目的,无需重新创建一个品类。
- 很多小类:为了添加不同的功能,我们会创建很多装饰者,这也就会创建很多特别小的类,当然他们是具有不同的功能的。
装饰者模式有这么多的优点,你在等什么?还不快快拿来使用?