结构型模式:装饰器模式
简介
在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰器模式来实现。
装饰器模式指:在不改变现有对象结构的情况下,动态的给对象增加一些职责(功能)的一种模式。
这种模式也是防止滥用继承,相对于桥接模式只能桥接两个维度,装饰器模式是可以对接多个维度的,因为他可以嵌套使用。
我们举个例子:
我有一个食物类:class 煎饼
我单点煎饼这个食物是可以的,我也可以加不同的配料:鸡蛋、火腿、豆皮
于是乎我们通过继承得到:
- class 鸡蛋煎饼 extends 煎饼
- class 火腿煎饼 extends 煎饼
- class 豆皮煎饼 extends 煎饼
这样写好像没有什么问题,但是,配料我能不能加两个,我甚至加三个,于是就有了:鸡蛋豆皮煎饼、火腿豆皮煎饼...等等,这个类的数量就会指数上升。
甚至于,我们能不能在鸡蛋豆皮煎饼这个类上再发展出新的子类:加辣、不辣、巨辣
你会发现很简单的需求,就使得类的暴增,这显然不符合我们需求。
解决办法和桥接一样,都是通过使用组合的方式来打断继承。
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰器模式的目标。
装饰器模式的角色
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
在简单的需求下,可能就只有一个具体的装饰,那么装饰和抽象就可以作为一个具体装饰来写,省去单独的抽象装饰。
代码实现
//煎饼接口
interface Pancakes {
name(): string;
}
//煎饼的实现
class Pancake implements Pancakes {
name(): string {
return "煎饼";
}
}
//抽象装饰器
class Decorator implements Pancakes {
pancake: Pancake; //聚合
constructor(pancake: Pancake) {
this.pancake = pancake;
}
name(): string {
return this.pancake.name();
}
}
//豆干火腿装饰器
class DGDecorator extends Decorator {
constructor(pancake: Pancake) {
super(pancake);
}
name(): string {
const oldName = super.name();
return `豆干火腿${oldName}`;
}
}
抽象装饰器的作用其实就是为了扩展子类的,如果没有那么多子类扩展,单写一个具体实现也行。
装饰器模式可以理解为类继承的另一种实现,它跟继承一样都会有抽象定义的那些属性方法,完全遵循开闭原则,对原对象修改是关闭的,装饰器来进行扩展。更加灵活。
typescript中的装饰器
由于我们js其实很少用类去编写代码,所以上述代码可能在实际使用中很少去使用,但是我们可以看看ts中装饰器。
由于js的函数存在函数提升,所以目前ts中的装饰器只能用在类上面,因为类是没有提升的,不能未声明前使用。
而js原生装饰器目前还在提案中,ts的装饰器也只是语法糖。
装饰器如果需要再ts中使用,需要配置一下ts.config.ts
{
"compilerOptions": {
"experimentalDecorators": true
}
}
原理
function getName(): string {
return "name";
}
//装饰器函数
function decorator(targetFn: Function) {
return function (...args: any[]) {
if (typeof targetFn !== "function") return;
console.log("装饰器自己的处理");
return targetFn.apply(this, args);
};
}
//调用
const newFn = decorator(getName);
console.log(newFn());
但是这么写的话,如果有6个装饰器,那你的函数就会嵌套多个,非常不利于阅读,所以ts它使用这种方式调用:
@decorator
function getName(): string {
return "name";
}
这是伪代码,因为ts的装饰器他是有自己的对应特殊处理的。
装饰器分类
装饰器分为三种:
- 类装饰器
- 成员装饰器
- 参数装饰器
顾名思义,类装饰器是用于装饰类的,成员装饰器用于装饰类里面的成员,参数则是类里面函数的参数。
类装饰器
function classDecorator(target: Object) {
console.log(target); //class A {}
}
@classDecorator
class A {
name() {
return "A";
}
}
类装饰器也是一个函数,ts只是加了特殊调用的方式@classDecorator
,改函数接受一个参数,这个参数就是被装饰的类本身。
注意:
- void: 当没有返回值的时候,整个类的行为不会被返回值所影响
- new Class: 返回一个新的类的时候,被装饰的类会被这个新的类直接替换(不建议)
成员装饰器
function memberDecorator(target: Object, key: string, descriptor: PropertyDescriptor) {
console.log(target);
console.log(key);
console.log(descriptor);
}
class A {
@memberDecorator
name() {
return "A";
}
}
成员装饰器接受三个参数:
- target:如果装饰器装饰的是静态成员,则target是类本身,如果装饰器装饰的是实例成员,比如上述的name函数,target则是该类的原型prototype对象。
- key:代表当前修饰的键值
- desdescriptor:该key的属性描述对象,可以设置是否允许读写这些
成员装饰器不能有返回值
参数装饰器
function parameterDecorator(target: Object, methodName: string, index: number) {
console.log(target);
console.log(methodName);
console.log(index);
}
class A {
name(@parameterDecorator name: string) {
return name;
}
}
参数装饰器接受三个参数:
- target:如果装饰器装饰的是静态成员方法的参数,则target是类本身,如果装饰器装饰的是实例成员方法的参数,比如上述的name函数,target则是该类的原型prototype对象。
- methodName:装饰的参数所在的方法名称,注意是名称,string的
- index:参数所在函数参数中的索引
参数装饰器不能有返回值
参数装饰器更多的可能是更高阶的用法,比如跟元信息进行配合使用,用来做一些数据校验什么的。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据