有寫過 Java Spring Framework 應該對於 @Annotation 用法並不陌生, 其常常用來實現 Decorator Pattern, 只要在函數或屬性上標記就能把功能附上或注入值, 巧妙的讓程式碼變的更加簡潔, 之後有些套件也是使用 Annotation 方式處理 (例如:Lombok), 是一個非常好用的語法特性, 而這種語法在近幾年也開始有其他程式語言開始支援, 例如:Python, Dart…等, 這語法特性在 JS 也有提出 proposal, 而在 Typescript 上已經優先支援此語法並且使用在 Angular, Vue3, NestJS…等一些框架上, 本篇將介紹一些 Decorator 的實際情境實作範例。
語法特性文件
Javascript Decorator Proposal: https://github.com/tc39/proposal-decorators
英文:https://www.typescriptlang.org/docs/handbook/decorators.html
中文:https://typescript.bootcss.com/decorators.html
示範使用情境
使用 Decorator 注入 property 資料範例
往往我們都會在 .env 或是環境變數設定一些 key 或是設定, 下面的範例是使用 Decorator 注入的方式替換掉取 jwtKey 變數時的值
// 定義一個回傳 Property 的函數
function config (name: string) {
// 相關設定可能來自 .env 或是系統環境變數, 這邊使用一個 MAP 示範
const map = new Map()
map.set('jwt-key', 't37XJOl8Ayxd2y3JphVLBOCWbYqxWcTk')
return (target: any, key: string) => {
Object.defineProperty(target, key, {
configurable: false,
get: function (this: { [name: string]: any}) {
return map.get(name)
},
set: function (value) {
// do nothing. 不可更改
}
})
}
}
class Encrypter {
@config('jwt-key')
jwtKey = '';
}
const encrypter = new Encrypter();
console.log(encrypter.jwtKey); // 輸出 `t37XJOl8Ayxd2y3JphVLBOCWbYqxWcTk`
使用 Decorator 替換函數實作範例
下面的範例是抄至 Spring Data JPA 的使用情境, 使用 Decorator 中已定義的查詢功能進行查詢替換掉函數實際功能, 只需要在函數上定義 SQL 就可以查詢, 節省部分重複的程式碼
function query (sql: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.value = async function (...args: any[]): Promise<any[]> {
for (const i in args) {
sql = sql.replaceAll('$' + (Number(i) + 1), args[i])
}
// 執行查詢...
const rows = [] as any[]
let conn
try {
const conn = createDBConn()
const [rows] = await conn.execute(sql)
} finally {
if (conn !== undefined) {
conn.end()
}
}
return rows
}
}
}
class StudentRepository {
@query('SELECT * FROM Student WHERE Name = "$1"')
findByName (name: string): any { /* do nothing. 由 Decorator 實作 */ }
@query('SELECT * FROM Student WHERE ID = $1')
findByID (id: number): any { /* do nothing. 由 Decorator 實作 */ }
}
使用 Decorator 實現 AOP 方式的 Cache method data
有些時候為了節省效能會使用到 Cache, 而往往讀取與寫入 Cache 的程式碼片段會遍佈在不同程式中, 這時可以針對某些函數成功的回傳值進行 Cache
function cacheData () {
let cacheDatas = {} as { [key: string]: any; };
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalFunc = descriptor.value;
descriptor.value = function (...args: any[]): any {
// 建立 cache 所用的 key, 依據自己情況改變 key 組成方式
let key = originalFunc.toString() + '-' + JSON.stringify(args);
if (cacheDatas.hasOwnProperty(key)) {
return cacheDatas[key]
}
let returnVal = originalFunc(...args);
if (returnVal instanceof Promise) {
// 如果是 promise 成功才會 cache
returnVal.then((value) => {
cacheDatas[key] = Promise.resolve(value);
return value;
})
} else {
cacheDatas[key] = returnVal;
}
return returnVal;
}
}
}
class MyClass {
@cacheData()
async foo (name: string, id: number): Promise<number> {
return (new Date()).getTime();
}
}
async function main() {
let obj = new MyClass();
console.log('Bob', await obj.foo("Bob", 1));
console.log('Alice', await obj.foo("Alice", 2));
console.log('Jake', await obj.foo("Jake", 3));
console.log('Bob', await obj.foo("Bob", 1)); // 這邊會是使用 cache 的結果
console.log('Bob', await obj.foo("Bob", 3));
}
main();
指定函數忽略 Exception (Error) 並回傳預設值 (AOP 應用)
有時使用第三方或自己寫功能時, 會遇到當其失敗會丟出 Exception 情境, 而如果想要忽略掉此錯誤, 會需要寫一個 try catch 來包住該函數功能, 但是這樣在程式碼可能會變複雜, 且可能會有許多 try catch 產生, 此時可以用 Decorator 方式忽略掉該函數中可能丟出的錯誤, 以替代的值返回
function ignoreError (defaultVal: any) {
let resolvePrmise = async (promise: Promise<any>, defaultValue: any) => {
try {
return await promise
} catch (e) {
// do nothing
}
return defaultValue
}
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalFunc = descriptor.value;
descriptor.value = function (...args: any[]): any {
try {
let returnVal = originalFunc(...args);
if (!(returnVal instanceof Promise)) {
return returnVal
}
return resolvePrmise(returnVal, defaultVal)
} catch (e) {
// do nothing
}
return defaultVal;
}
}
}
class MyClass {
@ignoreError('default value')
async foo (name: string): Promise<number> {
// do something
}
@ignoreError('default value2')
async foo2 (name: string): Promise<number> {
// do something
}
}
async function main() {
let obj = new MyClass();
console.log('Bob', await obj.foo("Bob"));
console.log('Alice', await obj.foo2("Alice"));
}
這幾個一些小小的情境應用, 還能作非常多的用途 (例如:對函數賦予 Lock 功能, 資料庫連線管理… 等), 有許多的框架用來作了不少好用的功能, 相信學習此語法特性會對於開發會有很多幫助



