有寫過 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 功能, 資料庫連線管理… 等), 有許多的框架用來作了不少好用的功能, 相信學習此語法特性會對於開發會有很多幫助