Skip to content

Commit

Permalink
wip: ch decorator, part 1 & 2
Browse files Browse the repository at this point in the history
  • Loading branch information
younggglcy authored and xcatliu committed Aug 18, 2024
1 parent f6797e1 commit 1fc9e44
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 1 deletion.
171 changes: 171 additions & 0 deletions advanced/decorator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# 装饰器

写在前面:本章只介绍 TypeScript 5.0+ 的装饰器用法,对于 5.0 以下的版本,请参考 [TypeScript 官方文档](https://www.typescriptlang.org/docs/handbook/decorators.html)

## 什么是装饰器

首先,什么是装饰器呢?[维基百科](https://en.wikipedia.org/wiki/Decorator_pattern)是这么说的:

> In [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), the **decorator pattern** is a [design pattern](https://en.wikipedia.org/wiki/Design_pattern_(computer_science)) that allows behavior to be added to an individual [object](https://en.wikipedia.org/wiki/Object_(computer_science)), dynamically, without affecting the behavior of other instances of the same [class](https://en.wikipedia.org/wiki/Class_(computer_science)).
本人的蹩足翻译:在 OOP (面向对象编程)中,装饰器模式是一种允许动态地往一个对象上添加自定义行为,而又不影响该对象所属的类的其他实例的一种设计模式。

> 什么是 OOP 和类?[前面的章节](https://ts.xcatliu.com/advanced/class.html)做过介绍。
这句话未免过于拗口了,我们不妨换个角度去切入。

## 装饰器的使用场景

要知道,一切设计模式的诞生,都是为了解决某个问题。在 JavaScript 的世界中,装饰器通常出现于以下场景:

1. 提供一种易读且容易实现的方式,修改类或者类的方法,避免出现大量重复的代码。

下面以修改类的方法为例。

首先,假设我们有一个 `Animal` 类:

```ts
class Animal {
type: string
constructor(type: string) {
this.type = type
}

greet() {
console.log(`Hello, I'm a(n) ${this.type}!`)
}
}

const xcat = new Animal('cat')
xcat.greet() // Hello, I'm a(n) cat!
```

该类有一个 greet 方法,和调用方打招呼。

假如说,我还希望根据不同的 `type`,往 console 打印不同动物的叫声呢?

聪明的你或许想到了,这不就是**类的继承**吗!在子类的 `greet()` 方法中,实现不同的逻辑,再调用 `super.greet()` 即可。

```ts
class Xcat extends Animal {
constructor() {
super('cat')
}
greet() {
console.log('meow~ meow~')
super.greet()
}
}
const xcat = new Xcat()
xcat.greet() // meow~ meow~
// Hello, I'm a(n) cat!
```

用装饰器实现,也不妨为一种思路,比如在 `Animal` 类中,为 `greet()` 方法添加「打印不同动物叫声的」行为:

```ts
class Animal {
type: string
constructor(type: string) {
this.type = type
}
@yelling
greet() {
console.log(`Hello, I'm a(n) ${this.type}!`)
}
}

const typeToYellingMap = {
cat: 'meow~ meow~'
}

function yelling(originalMethod: any, context: ClassMethodDecoratorContext) {
return function(...args: any[]) {
console.log(typeToYellingMap[this.type])
originalMethod.call(this, ...args)
}
}

const xcat = new Animal('cat')
xcat.greet() // meow~ meow~
// Hello, I'm a(n) cat!
```

`Animal.greet()` 方法上出现的 `@yelling` ,就是 TypeScript 中装饰器的写法,即 @ + 函数名的组合。

上述示例对装饰器的应用属于**方法装饰器**,此类装饰器本身接收两个参数,一是被装饰的方法,二是方法装饰器的上下文。方法装饰器应返回一个函数,此函数在运行时真正被执行。在上述例子中,我们在装饰器返回的函数中做了两件事情:

1. 打印相应类别的动物的叫声。
2. 调用 `originalMethod.call(this, …args)` ,确保原方法(即装饰器所装饰的方法)能够正确地被执行。
2. 结合「**依赖注入**」这一设计模式,优化模块与 class 的依赖关系。

什么是依赖注入呢?引用同事 [zio](https://github.com/ziofat) 的原话:

> **依赖注入其实是将一个模块所依赖的部分作为参数传入,而不是由模块自己去构造。**

可见,依赖注入解决了实际工程项目中,类、模块间依赖关系层级复杂的问题,将构造单例的行为交由实现依赖注入的框架去处理。

举个例子:

```ts
@injectable
class Dog implements IAnimal {
sayHi() {
console.log('woof woof woof')
}
}

@injectable
class Cat implements IAnimal {
sayHi() {
console.log('meow meow meow')
}
}

class AnimalService {
constructor(
@inject dog: Dog
@inject cat: Cat
) {
this._dog = dog
this._cat = cat
}

sayHiByDog() {
this._dog.sayHi()
}

sayHiByCat() {
this._cat.sayHi()
}
}
```

在上述代码中,`@injectable` 将一个类标记为「可被注入的」,在面向业务的类(即 `AnimalService`)中,使用 `@inject` 注入此类的单例,实现了「依赖倒置」。注意到这里的 `implements IAnimal` 用法,也是实战中依赖注入运用的精妙之处 —— 关心接口,而非具体实现。

3. 实现「AOP」,即 Aspect-oriented programming,面向切面编程。

所谓的「切面」,可以理解成,在复杂的各个业务维度中,只关注一个维度的事务。

例如,使用装饰器,实现对类的某个方法的执行时间记录:

```ts
class MyService {
@recordExecution
myFn() {
// do something...
}
}
function recordExecution(originalMethod: any, context: ClassMethodDecoratorContext) {
return function(...args: any[]) {
console.time('mark execution')
originalMethod.call(this, ...args)
console.timeEnd('mark execution')
}
}
```

1 change: 0 additions & 1 deletion advanced/further-reading.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@
- [Symbols](http://www.typescriptlang.org/docs/handbook/symbols.html)[中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Symbols.html)):新原生类型,这是 [ES6 的知识](http://es6.ruanyifeng.com/#docs/symbol)
- [Iterators and Generators](http://www.typescriptlang.org/docs/handbook/iterators-and-generators.html)[中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Iterators%20and%20Generators.html)):迭代器,这是 [ES6 的知识](http://es6.ruanyifeng.com/#docs/iterator)
- [Namespaces](http://www.typescriptlang.org/docs/handbook/namespaces.html)[中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Namespaces.html)):避免全局污染,现在已被 [ES6 Module](http://es6.ruanyifeng.com/#docs/module) 替代
- [Decorators](http://www.typescriptlang.org/docs/handbook/decorators.html)[中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Decorators.html)):修饰器,这是 [ES7 的一个提案](http://es6.ruanyifeng.com/#docs/decorator)
- [Mixins](http://www.typescriptlang.org/docs/handbook/mixins.html)[中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Mixins.html)):一种编程模式,与 TypeScript 没有直接关系,可以参考 [ES6 中 Mixin 模式的实现](http://es6.ruanyifeng.com/#docs/class#Mixin模式的实现)
1 change: 1 addition & 0 deletions pagic.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default {
'advanced/class-and-interfaces.md',
'advanced/generics.md',
'advanced/declaration-merging.md',
'advanced/decorator.md',
'advanced/further-reading.md',
],
},
Expand Down

0 comments on commit 1fc9e44

Please sign in to comment.