Skip to content

装饰器

✍️ w 🕒 2024-06-20 14:17:34(2 days ago) 🔗 A.前端知识整理

装饰器是一种在不修改现有代码的前提下,动态地为对象增加功能的设计模式。它通常用于编程语言中来扩展函数、方法或类的行为。装饰器可以看作是某种包装,使原本的功能可以额外运行一些代码。

装饰器(Decorator)其实是面向对象中的概念,在一些纯粹的面向对象的类型语言中早就有装饰器的内容了,在java中叫注解,在C#中叫特征

TS/JS 装饰的种类

装饰器并不是Typescript新引出的概念,是JavaScript本身就支持的内容。2024年,也才刚刚进展到第3阶段不久,现在ts5 自动支持 第三提案,但是现在大部分的 前端库依旧还是 js 的第二提案

  • JS 配置 babel 开启装饰器提案
  • TS 配置 tsconfig.json experimentalDecorators:true 开启装饰器

解决的问题

还是要提到Metadata(元数据),简单的说,就是为真正的数据提供额外的信息。例如,如果一个变量表示一个数组,那么数组的length就是其元数据。相似的,数组中的每个元素是数据的话,那么数据类型就是其元数据。更加宽泛的讲,元数据不仅仅是编程中的概念,它能够帮助我们更快的实现一些事情

装饰器就是可以装饰器,能够带来额外的信息量,可以达到分离关注点的目的。

  • 信息书写位置的问题
  • 重复代码的问题

上述两个问题产生的根源:某些信息,在定义时,能够附加的信息量有限。

装饰器的作用:为某些属性、类、参数、方法提供元数据信息(metadata)

下看看通过其他方式达到装饰器思路的方法

装饰器模式

装饰器模式是一种设计模式,通过这中设计模式来看装饰器所带来的好处,我们在开发过程中想给对象动态地增加职责的并且还能在不改变对象自身的基础上,在程序运行期间给对象 动态地添加职责。

现在想象一下有一个功能需要扩展,但是我们设想的是不影响原有的设计功能,因为对原因代码的改动带来的风险都是未知的,如果是面向对象的思路最简的方案 继承方式 但这种方式所带来的问题

  • 如果直接修改这个类,会违反开闭原则(OCP:对扩展开放,对修改关闭)。装饰器模式提供了一种不修改原始类而增加功能的方法。
  • 如果我们通过继承来增加功能,每增加一个功能就需要增加一个子类,功能越多,类的数量就会急剧增加,导致类爆炸问题。

如果利用装饰器 即用即付 的方式也是这类问题解决方案

  • 需要扩展一个类的功能,或给一个类添加附加职责,但不希望通过继承来实现。
  • 动态地为一个对象添加功能,这些功能可以在运行时根据需要进行添加或撤销。
  • 需要对一些基本功能进行扩展,并且可以组合这些功能来产生更复杂的行为。

装饰器模式思路解决问题

现在我们有一个简单的逻辑推导,我们有一个 飞机模型 它可以射出子弹。但是在开发初期我们没有想过随着业务的扩展我们可能需要飞机发射导弹、原子弹等其他功能。为了保持代码的灵活性和可维护性,可能不同开发开始出现了不同思路解决

js
class Plane {
    fire() {
        console.log('发射普通子弹');
    }
}

const plane = new Plane();
plane.fire();

// 输出:发射普通子弹

不恰当的方式一 -- 直接修改原始类

js
class Plane {
    fire() {
        console.log('发射普通子弹');
        this.fireMissiles();
        this.fireAtoms();
    }

    fireMissiles() {
        console.log('发射导弹');
    }

    fireAtoms() {
        console.log('发射原子弹');
    }
}

const plane = new Plane();
plane.fire();

// 输出:发射普通子弹、发射导弹、发射原子弹

这类产生的问题

  • 违反单一职责原则:一个类承担了太多职责,导致代码难以维护。 因为站在其他角度来看飞机并不是所有的都可以进行原子弹的发射
  • 不符合开闭原则:每次增加新功能都需要修改原始类,容易引入新的错误

不恰当的方式二 -- 使用全局变量或标志位

js
class Plane {
    fire() {
        console.log('发射普通子弹');
        if (Plane.enableMissiles) {
            console.log('发射导弹');
        }
        if (Plane.enableAtoms) {
            console.log('发射原子弹');
        }
    }
}

Plane.enableMissiles = true;
Plane.enableAtoms = true;

const plane = new Plane();
plane.fire();

// 输出:发射普通子弹、发射导弹、发射原子弹
  • 全局变量污染:容易造成全局变量污染,难以调试和维护。
  • 不灵活:功能的启用和禁用通过全局变量控制,难以在不同实例之间进行控制。(当然你可以采用不使用静态方法的形式去在类中定义改变但依旧违反了违反单一职责原则)

不恰当的方式三 -- 链式调用硬编码

链式调用的这种形式本质上已经比上面的思路更好 更加符合最开始说的到装饰器的思路通过组合附加在不影响原有类的情况下进行编程开发

js
class Plane {
    fire() {
        console.log('发射普通子弹');
    }
}

class PlaneWithMissiles {
    constructor(plane) {
        this.plane = plane;
    }

    fire() {
        this.plane.fire();
        console.log('发射导弹');
    }
}

class PlaneWithMissilesAndAtoms {
    constructor(plane) {
        this.plane = plane;
    }

    fire() {
        this.plane.fire();
        console.log('发射原子弹');
    }
}

const plane = new Plane();
const planeWithMissiles = new PlaneWithMissiles(plane);
const planeWithMissilesAndAtoms = new PlaneWithMissilesAndAtoms(planeWithMissiles);
planeWithMissilesAndAtoms.fire();

// 输出:发射普通子弹、发射导弹、发射原子弹
  • 功能组合固定,难以动态调整
  • 每增加一种组合功能就需要创建新的类,类数量迅速增加(这里指的是特定形式的类编写,只有飞机能发射导弹,但如果坦克也能发射呢)

使用装饰器模式的实现解决问题

通过装饰器模式,我们将功能扩展和组合的过程简化了,使得代码更加灵活和易于维护。

js
class Plane {
    fire() {
        console.log('发射普通子弹');
    }
}

class MissileDecorator {
    constructor(plane) {
        this.plane = plane;
    }

    fire() {
        this.plane.fire();
        console.log('发射导弹');
    }
}

class AtomDecorator {
    constructor(plane) {
        this.plane = plane;
    }

    fire() {
        this.plane.fire();
        console.log('发射原子弹');
    }
}

class LaserDecorator {
    constructor(plane) {
        this.plane = plane;
    }

    fire() {
        this.plane.fire();
        console.log('发射激光');
    }
}

let plane = new Plane();

// 添加发射导弹功能
plane = new MissileDecorator(plane);
plane.fire();

// 输出:发射普通子弹、发射导弹

// 添加发射原子弹功能
plane = new AtomDecorator(plane);
plane.fire();

// 输出:发射普通子弹、发射导弹、发射原子弹

通过以上步骤,我们展示了如何使用装饰器模式来动态地扩展飞机模型的功能。装饰器模式使得我们可以在不修改现有类的情况下,灵活地添加新功能,符合开闭原则,提高了代码的可维护性和扩展性。

如果未来有更多的功能需求,只需要创建新的装饰器类,并按照上述方式应用即可。这样的方法不仅保持了代码的清晰和简洁,还提供了极大的灵活性。

在来一个装饰器的对比案例

  1. SimpleMessage 类实现了基础的消息功能,它有一个 getContent 方法返回原始消息内容。
  2. MessageDecorator 类是一个抽象的装饰器类,它持有一个消息对象,并通过调用这个对象的 getContent 方法来实现装饰功能。
  3. EncryptedMessageDecoratorCompressedMessageDecorator 类分别实现了加密和压缩功能,它们在调用基础消息对象的 getContent 方法后,分别对结果进行加密和压缩处理。
  4. 在使用时,可以将装饰器类组合起来,实现多重装饰功能,如先压缩后加密。

首先,定义一个基础的消息类:

javascript
class Message {
  getContent() {
    throw new Error("This method should be overridden");
  }
}

class SimpleMessage extends Message {
  constructor(content) {
    super();
    this.content = content;
  }

  getContent() {
    return this.content;
  }
}

接下来,定义一个装饰器基类和具体的装饰器类:

javascript
class MessageDecorator extends Message {
  constructor(message) {
    super();
    this.message = message;
  }

  getContent() {
    return this.message.getContent();
  }
}

class EncryptedMessageDecorator extends MessageDecorator {
  getContent() {
    // 简单的加密逻辑(反转字符串)
    return this.message.getContent().split('').reverse().join('');
  }
}

class CompressedMessageDecorator extends MessageDecorator {
  getContent() {
    // 简单的压缩逻辑(去掉空格)
    return this.message.getContent().replace(/\s+/g, '');
  }
}

最后,使用这些装饰器类来装饰我们的消息对象:

javascript
// 创建一个简单的消息对象
let message = new SimpleMessage("Hello World");
console.log("Original Message:", message.getContent());

// 使用加密装饰器
let encryptedMessage = new EncryptedMessageDecorator(message);
console.log("Encrypted Message:", encryptedMessage.getContent());

// 使用压缩装饰器
let compressedMessage = new CompressedMessageDecorator(message);
console.log("Compressed Message:", compressedMessage.getContent());

// 组合使用加密和压缩装饰器
let encryptedCompressedMessage = new EncryptedMessageDecorator(compressedMessage);
console.log("Encrypted and Compressed Message:", encryptedCompressedMessage.getContent());

执行上述代码,你将看到以下输出:

plaintext
Original Message: Hello World
Encrypted Message: dlroW olleH
Compressed Message: HelloWorld
Encrypted and Compressed Message: dlroWolleH

通过这种方式,我们可以在不修改现有类的基础上,动态地为对象添加新的功能,体现了装饰器模式的灵活性和可扩展性。

  • js 函数式编程思路
js
// 基础消息类
class TextMessage {
  constructor(message) {
    this.message = message;
  }

  getText() {
    return this.message;
  }
}

// 高阶函数 - HTML装饰器
function HtmlDecoratedClass(BaseClass) {
  return class extends BaseClass {
    getText() {
      const originalText = super.getText();
      return `<p>${originalText}</p>`;
    }
  };
}

// 高阶函数 - 加密装饰器
function EncryptDecoratedClass(BaseClass) {
  return class extends BaseClass {
    getText() {
      const originalText = super.getText();
      // 这里应该是你的加密逻辑
      return this.encrypt(originalText);
    }
    encrypt(msg) {
      // 简单处理加密
      return msg.split("").reverse().join("");
    }
  };
}

// 使用装饰器
let DecoratedClass = HtmlDecoratedClass(TextMessage);
DecoratedClass = EncryptDecoratedClass(DecoratedClass);

const messageInstance = new DecoratedClass("Hello World");
console.log(messageInstance.getText()); // 输出被 HTML 格式化并加密的文本

装饰器案例js动态语言独有的

利用JavaScript的动态特性,可以在运行时修改对象的方法,达到扩展功能的目的。

js
let plane = {
    fire: function() {
        console.log('发射普通子弹');
    }
};

let missileDecorator = function() {
    console.log('发射导弹');
};

let atomDecorator = function() {
    console.log('发射原子弹');
};

let originalFire = plane.fire;
plane.fire = function() {
    originalFire();
    missileDecorator();
};

let decoratedFire = plane.fire;
plane.fire = function() {
    decoratedFire();
    atomDecorator();
};

plane.fire();

// 输出:发射普通子弹、发射导弹、发射原子弹

切面编程 AOP

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过将跨切面关注点(cross-cutting concerns)与业务逻辑分离来提高代码的模块化。AOP 的主要目的是将那些与业务逻辑无关的功能(例如日志记录、安全、事务管理等)从核心业务逻辑中分离出来,从而使核心逻辑更加简洁和清晰。

  1. 切面(Aspect):模块化的关注点,一个关注点可以是日志、安全、事务等。
  2. 连接点(Join Point):程序执行过程中的特定点,如方法调用、方法执行等。
  3. 切入点(Pointcut):定义一个或多个连接点,表示在何处插入切面的逻辑。
  4. 通知(Advice):在切入点执行的代码,可以在方法执行前(前置通知),方法执行后(后置通知)等。
  5. 织入(Weaving):将切面应用到目标对象以创建代理对象的过程。

AOP 和装饰器模式的关系

AOP 和装饰器模式在某种程度上有相似之处,尤其是在增强对象功能方面。两者都可以在不修改对象本身的情况下,动态地为对象添加新的行为。但是,它们的关注点和实现方式有所不同:

  1. 关注点

    • AOP:主要关注跨切面关注点的分离,如日志、安全等,强调的是切面和业务逻辑的分离。
    • 装饰器模式:主要用于动态地为对象添加职责,强调的是功能的增强和扩展。
  2. 实现方式

    • AOP:通常通过框架(如 Spring AOP)实现,使用代理对象,在运行时通过定义切入点和通知来增强对象功能。
    • 装饰器模式:通过创建装饰器类,将对象包裹起来,并在装饰器类中扩展或增强对象的功能。

JavaScript 中的 AOP 实现

在 JavaScript 中,我们可以通过函数重写的方式实现 AOP。以下是一个简单的 AOP 示例,展示如何在方法执行前后添加额外的行为:

javascript
Function.prototype.before = function(beforeFn) {
    const originalFn = this;
    return function(...args) {
        beforeFn.apply(this, args);
        return originalFn.apply(this, args);
    };
};

Function.prototype.after = function(afterFn) {
    const originalFn = this;
    return function(...args) {
        const result = originalFn.apply(this, args);
        afterFn.apply(this, args);
        return result;
    };
};

// 示例函数
const greet = function(name) {
    console.log(`Hello, ${name}`);
};

// 添加前置通知
const greetWithLogging = greet.before(function(name) {
    console.log(`Logging: About to greet ${name}`);
});

// 添加后置通知
const greetWithLoggingAndFarewell = greetWithLogging.after(function(name) {
    console.log(`Goodbye, ${name}`);
});

// 执行带有AOP通知的函数
greetWithLoggingAndFarewell('Alice');

// 输出:
// Logging: About to greet Alice
// Hello, Alice
// Goodbye, Alice

对比总结

  • 关注点分离

    • AOP:专注于将非业务逻辑(如日志、事务)从业务逻辑中分离出来。
    • 装饰器模式:专注于动态地为对象添加功能和职责。
  • 应用场景

    • AOP:适用于需要在多个类或方法中添加相同的非功能性行为(如日志、安全)的场景。
    • 装饰器模式:适用于需要在运行时动态扩展对象功能的场景。
  • 实现复杂度

    • AOP:通常需要使用框架或库来实现,具有较高的学习成本和实现复杂度。
    • 装饰器模式:相对简单,通过创建装饰器类即可实现。

通过理解 AOP 和装饰器模式的区别和联系,可以更好地选择合适的方式来实现代码的增强和扩展,提高代码的模块化和可维护性。

语言层面装饰器

上面提过元数据,如果我们能将属性或者类的一些数据进行绑定成为元数据,在使用的时候获取这些元数据 举个例子我们经常做的表单验证

js
class User { 
  // 注意:严格检查(strict)不赋初始值会报错
  // 演示可以设置 strictPropertyInitialization: false
  loginId: string; // 必须是3-5个字符
  loginPwd: string; // 必须是6-12个字符
  age: number; // 必须是0-100之间的数字
  gender: "男" | "女";

  validate() { 
    // 对账号进行验证
    // 对密码进行验证
    // 对年龄进行验证
    // ...
  }
}

当我们写这个类的属性的时候,对这个属性应该是最了解的。如果我们能在写属性的时候,就直接可以定义这些验证是最舒服的。因此能不能将这些校验规则绑定在属性上形成元数据

伪代码思路

js
@xxxx
class User {
    @require
    @range(3,5)
    @description("账号")
    loginid: string; //描述是:账号,验证规则:1.必填,2.必须是3-5个字符


    loginpwd: string; //必须是6-12个字符
    age: number; //必须是0-100之间的数字
    gender: "男" | "女";
}

class Article {
    title: string; //长度必须是4-20个字符
}

/**
 * 统一的验证函数
 * @param obj 
 */
function validate(obj: object) {
    for (const key in obj) {
        const val = (obj as any)[key];
        //缺少该属性的验证规则
    }
}

Released under the MIT License.