简介
目标: 为对象动态添加功能;
定义:动态地给一个对象添加额外的职责。就增加功能来说,装饰模式比生成子类更为灵活
而继承实际上是不灵活的复用方式。
具体步骤: 为与原使用被装饰对象的代码无缝结合,定义抽象类实现与被装饰对象相同接口,在具体实现类中转调被装饰对象,于转调前后添加新功能,实现为被装饰对象增功能,思路与“对象组合”类似。
本质
功能细化,动态组合
适用场景
- 需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责
- 不适合适用子类来进行扩展的时候
入门案例一:打印带有装饰边框的 Hello World!
输出示例
+--------------+
|Hello, world. |
+--------------+
设计类
| 类名 | 简介 |
|---|---|
| Display | 用于显式字符串的抽象类 |
| StringDisplay | 显式单行字符串的类 |
| Border | 显式装饰边框的抽象类 |
| SideBorder | 只显式左右边框的类 |
| FullBorder | 显式上下左右边框的类 |
| Main | 测试程序 |
类图设计
Display 类
可以显式多行字符串的抽象类
getColumns(): 抽象方法,获取横向字符数getRows(): 抽象方法,获取纵向行数getRowText(): 抽象方法,获取指定的某一行字符串show(): 显式所有行的字符串的方法,内部调用 getRows 获取行数和 getRowText 获取该行需要显示的字符串,然后通过 for 循环语句将所有的字符串显示出来。
public abstract class Display {
public abstract int getColumns(); // 获取列数
public abstract int getRows(); // 获取行数
public abstract String getRowText(int row); //获取第 row 行的字符串
//输出显示
public final void show() {
for (int i = 0; i < getRows(); i++) {
System.out.println(getRowText(i));
}
}
}
StringDisplay 类
用于显示单行字符串的类。它为 Display 的子类,需要实现 Display 类中声明的所有抽象方法的重任。
其中:
string字段中保存的是要显示的字符串。- 由于
StringDisplay类只显示一行字符串。所以getColumns()直接返回string.getBytes().length的值。 getRows方法则返回固定值1getRowText仅当获取第 0 行的内容的时候才会返回string字段
public class StringDisplay extends Display {
private String string;
public StringDisplay(String string) {
this.string = string;
}
@Override
public int getColumns() {
return this.string.getBytes().length;
}
@Override
public int getRows() {
return 1;
}
@Override
public String getRowText(int row) {
if (row == 0) {
return string;
} else {
return null;
}
}
}
Border 类
装饰边框的抽象类;但是它也是 Display 的子类。
因此,通过继承;装饰边框和被装饰物都具有了相同的方法。装饰边框 Border 与被装饰物 Display 具有相同的方法也就意味着它们具有一致性
- Display 类型的 display 字段表示被装饰物
- display 可能使 StringDiplay 实例又或者是 Border
public abstract class Border extends Display {
protected Display display;
protected Border(Display display) {
this.display = display;
}
}
SideBorder 类
一个具体装饰边框,是 Border 类的子类。SideBorder 类用指定的字符 borderchar装饰左右两侧;它实现了父类中声明的所有抽象方法。起到了装饰作用
- getColumns: 获取横向字符数的方法。在被装饰物的字符数的基础上,再加上两侧边框的字符数即可。需要调用 display.getColumns() 即可得到被装饰物的字符数。
- getRows: 因为 SideBorder 类并不会在字符串的上下两侧添加字符,因此 getRows 方法直接返回 display.getRows() 即可
- getRowText: 获取参数指定的那一行字符串。即
bordeChar + display.getRowText() + borderChar即可
public class SideBorder extends Border {
private char borderChar;
protected SideBorder(Display display, char c) {
super(display);
this.borderChar = c;
}
// 获取列数,原字符 + 左右边框字符
@Override
public int getColumns() {
return display.getColumns() + 2;
}
@Override
public int getRows() {
return display.getRows();
}
// 获取当前行的内容
@Override
public String getRowText(int row) {
return borderChar + display.getRowText(row) + borderChar;
}
}
FullBorder 类
与 SideBorder 类一样,也是 Border 类的子类。作用区别:
- SideBorder 类会在字符串的左右两侧加上装饰边框,
- FullBorder 类则会在字符串的上下左右都加上装饰边框。
但是 SideBorder 可以指定边框,而 FullBorder 内的边框是固定的
新增 makeLine() 方法用于连续显示某一字段
public class FullBorder extends Border {
protected FullBorder(Display display) {
super(display);
}
@Override
public int getColumns() { // 列数(字符数) 为被装饰物加上两侧边框字符数
return this.display.getColumns() + 2;
}
@Override
public int getRows() { // 行数为内容行 + 上下边框
return this.display.getRows() + 2;
}
@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) + "|";
}
}
private String makeLine(char c, int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(c);
}
return sb.toString();
}
}
Main 类
测试程序行为的类。生成 4 个实例,即 b1 ~ b4。作用:
- b1: 将 "Hello, world." 不加装饰地直接显示出来
- b2: 将 b1 的两侧加上装饰边框 '#'
- b3: 将 b2 的上下左右加上装饰边框
- b4: 为 "你好 世界" 加上多重边框
public class Main {
public static void main(String[] args) {
StringDisplay b1 = new StringDisplay("Hello, world."); // 指定输出内容
SideBorder b2 = new SideBorder(b1, ' '); // 添加左右边框
FullBorder b3 = new FullBorder(b2); // 输出携带上下边框 + 左右边框的
b1.show();
b2.show();
b3.show();
/*
输出:
Hello, world.
Hello, world.
+---------------+
| Hello, world. |
+---------------+
*/
// 嵌套实例
SideBorder b4 = new SideBorder(
new FullBorder(
new FullBorder(
new SideBorder(
new FullBorder(
new StringDisplay("Hello! Jools Wakooo!!!")),
'*')
)
), '/');
b4.show();
/*
输出:
/+----------------------------+/
/|+--------------------------+|/
/||*+----------------------+*||/
/||*|Hello! Jools Wakooo!!!|*||/
/||*+----------------------+*||/
/|+--------------------------+|/
/+----------------------------+/
*/
}
}
其中 b1, b2, b3 的关系
Decorator 模式中的核心组件
UML
核心组件
| 角色 | 简介 |
|---|---|
| Component | 增加功能的核心角色, 由示例中的 Display 扮演 |
| ConcreteComponent | 实现了 Component 角色所定义的接口,如 **StringDisplay ** |
| Decorator | 具有与 Component 角色相同的接口,内部保存了被装饰对象 —— Component 角色,知道自己要装饰的对象,示例中由 Border 角色充当 |
| ConcreteDecorator | 具体的 Decorator 角色,由示例中的 SideBorder 和 FullBorder 充当 |
接口 API 的透明性
⌈装饰边框⌋ 与 ⌈被装饰物⌋ 需要具有 一致性。比如示例中,⌈装饰边框⌋ Border 类是表示 ⌈被装饰物⌋ Display 类的子类。Border 类及其子类与表示 ⌈被装饰物⌋ 的 Display 类具有相同的接口接口不会被隐藏起来。其他类依然可以调用如 getColumns 、getRows 以及 show 方法。
不改变被装饰物的前提下增加功能
⌈装饰边框⌋ 与 ⌈被装饰物⌋ 具有相同的接口(抽象类),但是虽然接口相同,装饰越多,功能则越多。使用了委托,对 ⌈装饰边框⌋ 提出的要求会被转交给 ⌈被装饰物⌋ 去处理。
Java.io 包与 Decorator 模式
读取文件实例
Reader reader = new FileReader("xxxx.txt")
读取文件的时候将内容加载入缓冲区
Reader reader = new BuffredReader(
new FileReader("default.txt")
);
添加行号管理
Reader reader = new LineNumberReader(
new BuffredReader(
new FileReader("default.txt")
)
);
并且无论是 LineNumberReader 类的构造函数还是 BufferedReader 类的构造函数。都可以接收 Reader 类(的子类)的实例作为参数。可以自由组合
比如:只管理行号而不进行缓存处理
Reader reader = new LineNumberReader(
new FileReader("default.txt")
);
也可以不从文件中读取数据,而是从网络中读取数据
Reader reader = new LineNumberReader(
new BufferedReader(
new InputStreamReader(
socket.getInputStream();
)
)
);
这里使用 InputStreamReader 类既接收 getInputStream 方法返回的 InputStream类的实例作为构造函数的参数,也提供了 Reader 类的接口 API
什么是一致性?
继承 —— 父类与子类的一致性
class Parent {
...
void parentMethod() {
...
}
}
class Child extends Parent {
...
void childMethod() {
...
}
}
子类实例支持直接调用父类方法
Parent obj = new Child();
obj.parentMethod();
也就是说像操作 Parent 类的实例一样操作 Child 实例
但是如果反过来,需要先进行类型转换
Parent obj = new Child();
((Child) obj).childMetod();
委托 —— 自己和被委托对象的一致性
比如
class Rose {
Violet obj = ....;
void method() {
obj.method();
}
}
Class Violet {
void method() {
...
}
}
两者具有相同的 method 方法。而 Rose 将 method 方法的处理委托给 Violet。
或者基于抽象类或者接口表明其为共通关系
abstract class Flower {
abstract void method();
}
class Rose extends Flower {
Violet obj = ...;
void method() {
obj.method();
}
}
class Violet extends Flower {
void method() {
....
}
}
接口实现同理
入门案例一:练习
[练习] - 扩展入门案例一,添加 UpDownBorder类,支持为字符串装饰上下两条边框
输出示例
- 当设置上界字符为
-的时候
Hello,world.
-----------
Hello,world.
-----------
- 叠加装饰器
Hello!! World!!!
----------------
Hello!! World!!!
----------------
*----------------*
*Hello!! World!!!*
*----------------*
实现新的具体装饰器 ConcreteDecorator - UpDownBorder
public class UpDownBorder extends Border {
private char upDownChar;
private String bound;
public UpDownBorder(Display display, char upDownChar) {
super(display);
this.upDownChar = upDownChar;
this.bound = makeBorder();
}
@Override
public int getColumns() {
return display.getColumns(); //每一行字符数不变
}
@Override
public int getRows() {
return display.getRows() + 2; //新增上下边界
}
//重构当前行内容,添加上下边界
@Override
public String getRowText(int row) {
if (row == 0) { // 下边框
return bound;
} else if (row == display.getRows() + 1) { // 上边框
return bound;
} else { // 其他边框
return display.getRowText(row - 1);
}
}
// 构建上下边界
private String makeBorder() {
int cols = getColumns();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cols; i++) {
sb.append(upDownChar);
}
return sb.toString();
}
}
测试
@Test
public void showUpDownBound() {
StringDisplay b1 = new StringDisplay("Hello!! World!!!"); // 基础
UpDownBorder b2 = new UpDownBorder(b1, '-'); // 添加上下边框
SideBorder b3 = new SideBorder(b2, '*'); // 添加侧边框
FullBorder b4 = new FullBorder(b3); // 添加全边框
b1.show();
System.out.println();
b2.show();
System.out.println();
b3.show();
System.out.println();
b4.show();
SideBorder b5 = new SideBorder(b3, '|'); // 新增侧边框
UpDownBorder b6 = new UpDownBorder(b5, '/'); // 新增上下边框
System.out.println();
b5.show();
System.out.println();
b6.show();
/*
输出:
Hello!! World!!!
----------------
Hello!! World!!!
----------------
*----------------*
*Hello!! World!!!*
*----------------*
+------------------+
|*----------------*|
|*Hello!! World!!!*|
|*----------------*|
+------------------+
|*----------------*|
|*Hello!! World!!!*|
|*----------------*|
////////////////////
|*----------------*|
|*Hello!! World!!!*|
|*----------------*|
////////////////////
*/
}
[练习] - 新增可以显示多行字符串 MultiStringDisplay 类,在 Decorator 模式中扮演 ConcreteComponent 角色
示例、
MultiStringDisplay md = new MultiStringDisplay();
md.add("早上好。");
md.add("下午好。");
md.add("晚安,明天见。");
md.show();
/*
输出结果:
早上好。
下午好。
晚安,明天见。
*/
Display d1 = new SideBorder(md, '#');
d1.show();
/*
输出结果:
#早上好。 #
#下午好。 #
#晚安,明天见。#
*/
Display d2 = new FullBorder(md);
d2.show();
/*
输出结果:
+-------------+
|早上好。 |
|下午好。 |
|晚安,明天见。 |
+-------------+
*/
代码实现
public class MultiStringDisplay extends Display {
private List<String> strs;
public MultiStringDisplay() {
this.strs = new ArrayList<>();
}
public void add(String s) {
this.strs.add(s);
}
@Override
public int getColumns() {
int maxSize = Integer.MIN_VALUE;
for (String str : strs) {
maxSize = Math.max(str.length(), maxSize);
}
return maxSize;
}
@Override
public int getRows() {
return this.strs.size();
}
@Override
public String getRowText(int row) {
int size = getColumns();
String str = strs.get(row);
return str + " ".repeat(Math.max(0, size - str.length())); //
}
}
测试
@Test
public void testMultiStrs() {
MultiStringDisplay md = new MultiStringDisplay();
md.add("Good Morning");
md.add("Good Afternoon");
md.add("See you soon!!!");
md.show();
System.out.println();
SideBorder b1 = new SideBorder(md, '*');
b1.show();
System.out.println();
FullBorder b2 = new FullBorder(b1);
b2.show();
/*
输出:
Good Morning
Good Afternoon
See you soon!!!
*Good Morning *
*Good Afternoon *
*See you soon!!!*
+-----------------+
|*Good Morning *|
|*Good Afternoon *|
|*See you soon!!!*|
+-----------------+
*/
}
入门案例二:饮料添加配料
题目描述
不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格
下图表示在深度烘焙咖啡(DarkRoast)饮料上新增添加摩卡(Mocha)配料,之后又添加了鲜奶油(Whip)配料。
深度烘焙咖啡被摩卡包裹,摩卡又被鲜奶油包裹。
它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。
设计
Component - Beverage
public interface Beverage {
double cost();
}
ConcreteComponent - DarkRoast
public class DarkRoast implements Beverage{
@Override
public double cost() {
return 1;
}
}
Decorator - CondimentDecorator
public abstract class CondimentDecorator implements Beverage {
protected Beverage beverage;
}
ConcreteDecorator - Milke + Mocha
public class Milk extends CondimentDecorator{
public Milk(Beverage beverage) {
this.beverage = beverage;
}
@Override
public double cost() {
return 1 + beverage.cost();
}
}
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public double cost() {
return 1 + beverage.cost();
}
}
测试
public class Client {
public static void main(String[] args) {
DarkRoast beverage = new DarkRoast();
System.out.println(beverage.cost());
Beverage b2 = new Milk(beverage);
System.out.println(b2.cost());
Beverage b3 = new Mocha(new Milk(beverage));
System.out.println(b3.cost());
/*
输出:
1.0
2.0
3.0
*/
}
}
装饰模式的优缺点
优点
- 比继承更加灵活: 继承是静态的,所有子类共享相同的功能。而装饰模式通过将功能分离到各个装饰器中,并在运行时动态组合这些装饰器,从而实现灵活的功能组合。最终的功能由运行时动态组合的装饰器决定。
- 更加容易复用功能: 把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器仅实现一个功能。
缺点
- 产生很多细粒度对象: 为与原使用被装饰对象的代码无缝结合,定义抽象类实现与被装饰对象相同接口,在具体实现类中转调被装饰对象,于转调前后添加新功能,实现为被装饰对象增功能,思路与“对象组合”类似。
巩固案例 - 咖啡加糖
原题来源
题目描述
小明喜欢品尝不同口味的咖啡,他发现每种咖啡都可以加入不同的调料,比如牛奶、糖和巧克力。他决定使用装饰者模式制作自己喜欢的咖啡。
请设计一个简单的咖啡制作系统,使用装饰者模式为咖啡添加不同的调料。系统支持两种咖啡类型:黑咖啡(Black Coffee)和拿铁(Latte)。
输入输出示例
UML设计
实现 Component (Coffee 接口)
// Component
interface Coffee {
void brew();
}
实现 ConcreteComponent
// ConcreteComponent 01
class BlackCoffee implements Coffee {
@Override
public void brew() {
System.out.println("Brewing Black Coffee");
}
}
// ConcreteComponent 02
class Latte implements Coffee {
@Override
public void brew() {
System.out.println("Brewing Latte");
}
}
实现 Decorator 抽象类
- 注意:持有的 Coffee 字段可以用
protected修饰简化代码; 支持子类直接访问
abstract class CondimentDecorator implements Coffee {
// 对子类可见
protected Coffee coffee;
public CondimentDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
实现 ConcreteDecorator
class Milk extends CondimentDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
@Override
public void brew(){
super.coffee.brew();
System.out.println("Adding Milk");
}
}
class Sugar extends CondimentDecorator {
public Sugar(Coffee coffee) {
super(coffee);
}
@Override
public void brew() {
super.coffee.brew();
System.out.println("Adding Sugar");
}
}
执行结果
参考
- 《图解设计模式》 之 装饰器模式
- pdai.tech/md/dev-spec…
- www.jianshu.com/p/9a7f5d81c…
- 卡码网 - 设计模式