装饰器模式(Decorator Pattern),又称为包装器模式(Wrapper Pattern),是一种结构型设计模式。它允许在不改变原有对象结构的基础上,动态地给对象添加一些新的职责(即增加其额外功能)。
一、核心思想
装饰器模式的核心思想是通过组合而非继承来实现功能的扩展。它提供了一种比使用子类更加灵活的替代方案,使得对象可以在运行时根据需要动态地添加或移除装饰器,从而实现功能的动态组合。
二、定义与结构
定义:装饰器模式动态地给一个对象添加一些额外的职责。就扩展功能而言,装饰器模式提供了一种比使用子类更加灵活的替代方案。
结构:
- 抽象组件(Component)角色:定义了一个对象接口,可以给这些对象动态地添加一些职责。
- 具体组件(Concrete Component)角色:实现了抽象组件接口,并定义了一个具体的对象,也可以给这个对象添加一些职责。
- 装饰器(Decorator)角色:持有一个组件(Component)对象的引用,并定义一个与抽象组件一致的接口。它本身是一个抽象类,通常不会直接实例化。
- 具体装饰器(Concrete Decorator)角色:负责给组件添加新的职责。
角色
在装饰器模式中,各个角色之间的关系和职责如下:
- 抽象构件(Component):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法。它使得客户端能够以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
- 具体构件(Concrete Component):它是抽象构件类的子类,用于定义具体的构建对象,实现了在抽象构件中声明的方法。装饰类可以给它增加额外的职责。
- 抽象装饰(Decorator):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护了一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
- 具体装饰(Concrete Decorator):它是抽象装饰类的子类,负责向构件添加新的职责。它定义了新的方法,并可以增加新的方法用于扩充对象的行为。
三、实现步骤与代码示例
以Java为例,展示装饰器模式的具体实现步骤和代码示例。
步骤:
- 定义抽象组件接口。
- 实现具体组件类。
- 定义装饰器抽象类,它实现了抽象组件接口并持有一个抽象组件对象的引用。
- 实现具体装饰器类,它继承了装饰器抽象类并添加了新的职责。
代码示例:
// 抽象组件接口
public interface Component {
void operation();
}
// 具体组件类
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("ConcreteComponent: Basic operation.");
}
}
// 装饰器抽象类
public abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
// 具体装饰器A
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
addedBehaviorA();
}
private void addedBehaviorA() {
System.out.println("ConcreteDecoratorA: Added behavior A.");
}
}
// 具体装饰器B
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
addedBehaviorB();
}
private void addedBehaviorB() {
System.out.println("ConcreteDecoratorB: Added behavior B.");
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
Component component = new ConcreteComponent();
Component decoratorA = new ConcreteDecoratorA(component);
Component decoratorB = new ConcreteDecoratorB(decoratorA);
decoratorB.operation();
}
}
输出:
ConcreteComponent: Basic operation.
ConcreteDecoratorA: Added behavior A.
ConcreteDecoratorB: Added behavior B.
在这个例子中,ConcreteComponent
执行了基础操作,之后通过 ConcreteDecoratorA
和 ConcreteDecoratorB
为该组件对象动态添加了新的行为。最终的输出显示了基础操作以及两个装饰器的新增行为。
四、常见技术框架应用
虽然装饰器模式在不同的技术框架中可能有不同的实现方式,但其核心思想是一致的。以下是一些在其他技术框架中应用装饰器模式应用:
1、以Java的IO流为例
- 在Java的
java.io
包中广泛使用了装饰器模式。 - 抽象构件是
InputStream
(字节输入流)和OutputStream
(字节输出流)等接口。 - 具体构件例如
FileInputStream
(从文件读取字节流)和FileOutputStream
(向文件写入字节流)。 - 抽象装饰类可以是
FilterInputStream
和FilterOutputStream
,它们都实现了对应的InputStream
和OutputStream
接口,并且包含一个对InputStream
或OutputStream
的引用。 - 具体装饰类有很多,比如
BufferedInputStream
(为输入流添加缓冲功能)和DataInputStream
(可以从输入流读取基本数据类型)。以下是简单的代码示例:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Main {
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("test.txt");
// 使用装饰器BufferedInputStream为输入流添加缓冲功能
InputStream bufferedInputStream = new BufferedInputStream(inputStream);
int data;
while ((data = bufferedInputStream.read())!= -1) {
System.out.print((char) data);
}
bufferedInputStream.close();
inputStream.close();
}
}
2、ES6 装饰器
在ES6(ECMAScript 2015)及之后的版本中,JavaScript引入了装饰器作为实验性特性。装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问器、属性或参数上。它们使用@expression
这种形式,在编译时执行,并且可以用来修改类的行为。
装饰器本质上是一个函数,它接收目标对象(类构造函数或其原型)、属性名(对于属性和方法),以及描述符(对于方法)。装饰器可以返回一个新的属性描述符来替换旧的描述符,或者返回一个全新的构造函数来替代旧的构造函数。
下面我们将通过几个例子来详细解释如何在JavaScript中使用装饰器模式。
类装饰器
类装饰器应用于类构造函数,通常用于观察、修改或替换类定义。
function sealed(constructor) {
console.log('Sealed decorator called');
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Person {
constructor(name) {
this.name = name;
}
}
在这个例子中,当定义Person
类时,sealed
装饰器会立即执行,并将Person
构造函数及其原型都密封起来,防止进一步扩展。
方法装饰器
方法装饰器用于修饰类的方法,可以改变方法的行为。
function enumerable(value) {
return function (target, propertyKey, descriptor) {
descriptor.enumerable = value;
return descriptor;
};
}
class Example {
@enumerable(false)
method() {}
}
这里我们定义了一个enumerable
装饰器,它可以控制类方法是否可以在遍历中出现。当我们在Example
类中使用这个装饰器时,我们可以设置method
方法是否可枚举。
属性装饰器
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
function format(formatString) {
return function(target, key) {
let _value = target[key];
const getter = () => _value;
const setter = (value) => {
console.log(`Setting ${key} to ${formatString.replace('{}', value)}`);
_value = value;
};
delete target[key];
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Greeting {
@format('Hello, {}!')
name = '';
}
const greeting = new Greeting();
greeting.name = 'Alice';
console.log(greeting.name); // Output: Hello, Alice!
在这个例子中,format
装饰器用来格式化name
属性的值,当设置name
属性时,它会输出一条格式化的消息。
注意事项
不同的框架(如TypeScript、Angular)对装饰器的支持程度也不同。在实际项目中使用装饰器之前,请确认你的开发环境支持这一特性。
五、应用场景
装饰器模式适用于以下场景:
- 当不能采用继承的方式扩展系统,或采用继承的方式扩展系统不利于系统维护时。
- 当对象的功能要求可以动态增加、撤销时。
- 需要灵活扩展对象的功能,而不想增加大量子类时。
具体应用场景包括:
- I/O流处理:如Java的I/O库中的
BufferedReader
、BufferedWriter
等类都是对基本的Reader
和Writer
进行装饰,增加了缓冲功能。 - GUI组件定制:如按钮、文本框等组件的定制。
- 报表生成:如分页、标题、水印等格式的动态添加。
- 权限控制:如为不同的资源或操作添加权限检查。
- 日志记录:如为不同的对象或方法添加日志记录功能。
六、优缺点
优点:
- 灵活性强:装饰器和被装饰类可以独立发展,互不耦合。可以通过组合多个装饰器来灵活地组合对象的行为。
- 扩展性好:提供了比继承更加灵活的扩展方式。不需要创建大量的子类,就可以给对象增加新的行为或功能。
- 透明性好:用户可以根据需要选择性地添加装饰器,而不需要了解这些装饰器是如何实现的。这使得代码更加清晰和简洁。
- 复用性高:装饰器可以被重复使用在多个对象上,提高了代码的复用性。
缺点:
- 复杂性增加:过多地使用装饰器可能会使代码变得复杂,特别是当装饰器链很长时,理解和维护起来会比较困难。
- 性能开销:在装饰器链中,每个方法调用都会经过多层装饰器的包装,这可能会导致性能上的开销。
总之,装饰器模式是一种非常有用的设计模式,它可以提高代码的可扩展性和灵活性。但在设计时需要慎重考虑是否使用该模式,并权衡其优缺点。