图文讲解 Decorator -- 装饰者模式

261 阅读6分钟

什么是装饰者模式

顾名思义,就是给某一个物体添加一些装饰物,让其变得更加丰富多彩。比如一块蛋糕,什么都不加,它就是一块平平无奇的蛋糕,如果加上一些草莓,它就变成一块可口的草莓蛋糕,再加上一些巧克力,抹上一点奶油,插上几根蜡烛,哎呦,美美的生日蛋糕就成型了。

当然,无论你是草莓蛋糕还是生日蛋糕,本质上仍旧是一块蛋糕,只不过加上这些装饰物之后,味道更好了,目的也更加明确了。

我们写代码跟做蛋糕也是一样的,首先定义一个蛋糕的对象,然后不断地添加功能,这个对象就更加纯粹了,我们就可以更有目的性的去调用它。

类似这样不断为对象添加装饰物的模式就是 Decorator 设计模式。文字表述可能不够具体,下面就用代码和类图来解析一下。

Decorator 模式中的角色

首先我们画一下 Decorator 模式的类图,对其有一个直观的了解。

image.png

    1. Component: 增加功能的核心角色,也就是我们上面所说的蛋糕,是整个设计模式的核心,定义了具体的 API 接口。
    1. ConcreteComponent:实现了 Component 角色所定义的接口 API 的具体蛋糕,接下来就要为这个具体的蛋糕来添加装饰物。
    1. Decorator: 装饰物,该角色具有与 Component 角色相同的 API,在它内部保存了被装饰对象 component。
    1. ConcreteDecorator: 具体的装饰物。

通过这个类图,我们要发现几个重点:

  • 装饰物与被装饰者具有一致性,也就是说这两者都是 Component 的子类,这就体现了它们之间的一致性,即使装饰之后,API 也不会隐藏起来,就保证了接口 API 的透明性。
  • 由于这种透明的结构,就会形成一个递归结构,也就是说,装饰物里面的被装饰物同时又是其他物体的装饰物。

下面我们来看一个实例程序

示例程序

image.png

首先我们将实例程序的类图仿照 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 的文章,可以先了解一下。

image.png Context 作为最基础的抽象类,其中定义了一些基础的接口 API,如图所示: image.png

看一下 Context 的继承关系图:

image.png

跟 Decorator 的 UML 图对比一下,是不是非常的相似呢?

结语

  • 在不改变被装饰物的前提下增加功能:在 Decorator 模式中,装饰者与被装饰者拥有相同的接口 API,随着装饰物的增多,功能也就越多,但是其本质并不会发生改变,蛋糕还是那个蛋糕。
  • 使用委托:使用委托,将类之间的关系变为弱关联,可以在不更改框架代码的前提下,就可以生成一个与其他对象具有不同关系的心对象。
  • 功能抽离:我们可以准备多种装饰者,将功能进行抽离,只需要对被装饰者添加不同的装饰物,就可以达到我们的目的,无需重新创建一个品类。
  • 很多小类:为了添加不同的功能,我们会创建很多装饰者,这也就会创建很多特别小的类,当然他们是具有不同的功能的。

装饰者模式有这么多的优点,你在等什么?还不快快拿来使用?