Alex Liang

使用 babel decorator plugin 實現 Aspect-Oriented Programming

實務上經常會遇到程式流程夾雜許多非核心,但又必須得做的行為,例如 logging, auit, notification 或 cache.

這篇文章介紹如何使用 babel decorator plugin 實現 Aspect Object Programming。

將共用的功能抽到 decorator,讓整個流程變的清楚好懂。

開頭提到的這些行為在不同實作間會一直重複發生。對於想了解流程的人來說,這些其實不是核心的部分,如下圖所示:

aspect-oriented-programming

note: 在 SRE 的世界同樣也會發生每個 container 需要加入非業務邏輯的功能,如提供 HTTPS。其解決方案是 https://tachingchen.com/tw/blog/desigining-distributed-systems-the-sidecar-pattern-concept/ 稱為 sidecar pattern

Babel Decorator Plugin

Install

在安裝 babel plugin 前,請先確保已安裝 @babel/core and @babel/preset-env

1
npm install --save-dev @babel/plugin-proposal-decorators @babel/eslint-parser

Usage

設定 babel config:

babel.config.js
1
2
3
4
5
6
7
8
module.exports = {
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }]
],
presets: [
["@babel/preset-env", { targets: { node: "current" } }]
]
};

如果專案有使用 ESLint,需要設定 parser 讓 ESLint 認得 decorator

.eslint.js
1
2
3
{
parser: "@babel/eslint-parser"
}

Example

我們以 log 為例,babel decorator 本筫上是一個 wrapper function。其 signature 為 (value, context)

value 就是包裝的對象,可以是 function 或 class,使用 apply 取得對象的結果。

如此一來,我們可以選擇在包裝對象的前後插入程式碼,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function log(value, context) {
return function(...args) { // args is arguments of wrapped method
console.log("Logged at: " + new Date().toLocaleString());
return value.apply(this, args); // value is wrapped method
}
}

class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

@log
getBio() {
return `${this.name} is a ${this.age} years old`;
}
}

這裡宣告 log decorator,能印出呼叫對象的時間點及 error handling。

另一個需要注意的是 context 參數,它是一個 object 具有包裝對象的資訊。我們可以到 decorator playground 試玩,可以發現 context 有以下 property:

  • kind
  • name
  • isStatic
  • isPrivate
  • getMetadata
  • setMetadata

常用的有 kindname,前者為包裝對象的類型,在這個範例為 method;後者為對象的名稱。

這些資訊能讓 decorator 做更有彈性的處理,例如針對 class 的 decorator。

需要注意的是,假如 decorator 對象是 static method,則包裝後的 method 也會是 static。

note: babel 最近 release Stage 3 decorators,其 context 有些許差異,使用上需多做留意。

我們也可以給予 decorator 參數,讓行為有更多變化,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const map = new Map();

function cached(key) => (value, context) => {
return function(...args) {
if (map.has(key)) return map.get(key);

const result = value.apply(this, args);
map.set(key, result);
return result;
}
}

class ShoppingCart {

@cached(KEY)
get amount() {}
}

Reference