定义
装饰器模式(Decorator Pattern)属于结构型设计模式。将新的行为以创建类的方式去对原始对象进行包装,在实现同一接口并且不修改原有结构的前提下,达到扩展新行为的目的。简而言之,装饰器模式的核心展现出一种组合的思想,希望一切新的行为都是“即插即拔”的状态。
对于那些基于类的面向对象编程语言做开发时,装饰器模式也深深被成熟运用。装饰器模式同样也是JS的大势所趋,从NextJs语法大量使用的注解,再到TypeScript的写法,再到ECMA未来可能将装饰器模式纳入标准,无时无刻不体现出了AOP(面向切面编程)思想的伟大。
特点
- 解耦性:装饰器模式作为结构型的设计模式,更加体现了组合优于继承的思想。新增功能在不修改原有代码的基础上保持了动态性,不影响原有代码,保证了代码的完整性。
- 灵活性:装饰器模式可以达到“即插即拔”的状态,在面对大型的项目时能够更灵活去处理复杂业务。
- 动态性:可以很方便去添加移除一些功能,还不需要修改原有代码。
场景
适用于给对象附加能力时使用,如Vue的指令设计,就很好体现了装饰器模式的作用;Java的注解也是对某个类进行能力的扩充。
举例实现
这里我们举个例子,有一款“一刀999”的类传奇游戏有一个装备系统,我们将其拆解一下,得到一个本体和无数的插件。如果我要给一个用户一把“屠龙刀”,我正常继承原始对象是可以的。但如果我的需求是:我要给“划雨悦潭之赋”这个游戏玩家一把“屠龙刀”,一双“黄金之翼”,一枚“麻痹戒指”,那么使用传统的继承就不合适了。
这种场景下,我们可以使用装饰器模式,利用组合的思想去设计代码,如下图所示。 首先我们定义一个IBody的接口。创建一个Body的本体类去实现IBody接口,创建一个基础的装饰器类Decorator也去实现IBody接口,然后每个具体的装备类再去继承基础装饰类。
在客户端,我们创建一个人物本体对象,创建“翅膀”的对象将本体对象传入,此时本体对象去操作对应的获得装备逻辑;再创建“武器”的对象将得到的创建“翅膀”的对象传入以此类推,最后将得到的对象调用give方法给予特定的目标;就像“叠房子”一样一层一层落。
在具体装饰类中,我们部分重写了give方法,在处理自己的逻辑之前先调用父类的give方法,形成一个链条。具体代码如下:
// 人物本体接口 interface IBody { give(roleName: string): void; } // 基础人物本体类 class Body implements IBody { private name: string; constructor(name: string) { this.name = name; } give(roleName: string): void { console.log(`给角色"${roleName}"提供道具`); } } // 基础装饰器 定义整个装饰器的结构 子类可以做扩展 class Decorator implements IBody { // 基础的装饰器类是不能够单独使用的 private body: IBody; // 初始化时需传入一个IBody类型的实例 constructor(body: IBody) { this.body = body; } give(roleName: string): void { this.body.give(roleName); } } // 具体装饰类 翅膀 class WingDecorator extends Decorator{ private wingName: string; constructor(body: IBody, wingName: string) { super(body); this.wingName = wingName; } // 重写父类的give方法 give(roleName: string): void { // 调用父类的give方法 super.give(roleName); // 给人物赠送翅膀 console.log(`系统赠送[翅膀]"${this.wingName}"给"${roleName}"`); } } // 具体装饰类 翅膀 class WeaponDecorator extends Decorator{ private weaponName: string; constructor(body: IBody, weaponName: string) { super(body); this.weaponName = weaponName; } // 重写父类的give方法 give(roleName: string): void { // 调用父类的give方法 super.give(roleName); // 给人物赠送武器 console.log(`系统赠送[武器]"${this.weaponName}"给"${roleName}"`); } } // 具体装饰类 戒指 class RingDecorator extends Decorator{ private ringName: string; constructor(body: IBody, ringName: string) { super(body); this.ringName = ringName; } // 重写父类的give方法 give(roleName: string): void { // 调用父类的give方法 super.give(roleName); // 给人物赠送戒指 console.log(`系统赠送[戒指]"${this.ringName}"给"${roleName}"`); } } const body = new Body(''); const wingBody = new WingDecorator(body, '黄金之翼'); const wingWeaponBody = new WeaponDecorator(wingBody, '屠龙刀'); const wingWeaponRingBody = new RingDecorator(wingWeaponBody, '麻痹戒指'); wingWeaponRingBody.give('划雨悦潭之赋');
最终运行代码,可以得到如下的结果。我只give了一次,为什么会有4条输出呢?
在每一个new之后都打印一下实例,可以看到最终到wingWeaponRingBody时,已经形成了“麻痹戒指-屠龙刀-黄金之翼-”的链条。当调用give方法时,先会调用父类的give方法,而父类的give方法执行的是this.body.give(),也就是说会一层一层执行下去,从而达到组合的效果;装饰器类作为工厂方法被调用,返回的是一个装饰器类。调用逻辑如下图。
装饰器方式
TS中的装饰器模式还。需要将具体的装饰器类改写成装饰器方法,用JS原型的特性形成原型链调用give方法;在调用时使用@符号,大大提高了客户端调用代码的便捷性。
// 人物身体接口 interface IBody { give(message: string): void; } // 基础人物身体类 class Body implements IBody { private name: string; constructor(name: string) { this.name = name; } give(roleName: string): void { console.log(`给角色"${roleName}"提供道具`); } } // 基础装饰器 定义整个装饰器的结构 子类可以做扩展 class Decorator implements IBody { // 基础的装饰器类是不能够单独使用的 private body: IBody; // 初始化时需传入一个IBody类型的实例 constructor(body: IBody) { this.body = body; } give(roleName: string): void { this.body.give(roleName); } } // TS装饰器的写法 翅膀 export function WingDecorator(wingName: string): ClassDecorator { return function(constructor: Function) { const send = constructor.prototype.give; // 先拿到原型上的give方法 // 然后再扩展自己的逻辑 constructor.prototype.give = function(roleName: string) { // 扩展之前先执行一遍give方法 send.apply(this, arguments); // 扩展自己的逻辑 console.log(`系统赠送[翅膀]"${wingName}"给"${roleName}"`); } }; } // TS装饰器的写法 武器 export function WeaponDecorator(weaponName: string): ClassDecorator { return function(constructor: Function) { const send = constructor.prototype.give; // 先拿到原型上的give方法 // 然后再扩展自己的逻辑 constructor.prototype.give = function(roleName: string) { // 扩展之前先执行一遍give方法 send.apply(this, arguments); // 扩展自己的逻辑 console.log(`系统赠送[武器]"${weaponName}"给"${roleName}"`); } }; } // TS装饰器的写法 戒指 export function RingDecorator(ringName: string): ClassDecorator { return function(constructor: Function) { const send = constructor.prototype.give; // 先拿到原型上的give方法 // 然后再扩展自己的逻辑 constructor.prototype.give = function(roleName: string) { // 扩展之前先执行一遍give方法 send.apply(this, arguments); // 扩展自己的逻辑 console.log(`系统赠送[戒指]"${ringName}"给"${roleName}"`); } }; } // 客户端使用 @WingDecorator('黄金之翼') @WeaponDecorator('屠龙刀') @RingDecorator('麻痹戒指') class Client extends Body{ constructor(name: string) { super(name); } } const client = new Client(''); client.give('划雨悦潭之赋');
TS装饰器不生效问题
TS中装饰器还是实验性语法,所以直接使用@符号是不生效的。需要在配置文件中,显式将experimentalDecorators设为true。
{ "compilerOptions": { "experimentalDecorators": true, } }
总结
装饰器模式很好诠释了“组合”的思想,能够保证设计原则的同时也大大提高了客户端使用的便捷性,但也不要过度装饰和包装,可能导致太多层反而增加代码复杂度。前端必须掌握的设计模式系列持续更新,如果对您有帮助希望多多点赞哦!