重用和一致性是程式設計中經久不衰的兩個課題。在最新的 ES Proposal 中,「decorators 語法」為此帶來了一定的便利,並且,很適合應用於大型的類庫中。
裝飾模式
提到 decorator 大家都不會陌生,即「裝飾模式」—— 我們可以在「不侵入原有程式碼」的情況下,為程式碼增加一些「額外的功能」。
所謂「額外的功能」一般都比較獨立,不和原有邏輯耦合,只是做一層包裝。你也可以把它看成「包裝模式」。形如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 舊方法 function func() {} // 包裝後的新方法 function funcWrapped() { // 有的沒的 doSomethingBefore(); // 舊方法的過程本身並不變化 func(); // 這啊那的 doSomethingAfter(); } |
這樣看來,有一些場景特別適用這個模式,比如:
- 記錄方法的開始執行和結束執行。
- 為運算過程提供額外的快取能力。
- 標記方法為 deprecated。
- 等等。
編寫一個裝飾器
如果有好多方法都想包上這種「額外的功能」,那麼我們不會一個個地去改寫,而是考慮抽出一個「裝飾器」—— 它能夠接受原方法,然後生成包裝後的方法。比如,我們想記錄所有方法的執行時間:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function performanceTimingDecorator(func) { // 返回包裝後的新方法 return function(...args) { const start = Date.now(); func(...args); const end = Date.now(); const t = end - start; console.log(`${func.name} performed ${t}ms.`); }; } function func() {} const funcWrapped = performanceTimingDecorator(func); // func performed 2ms. funcWrapped(); |
使用 ES decorators
如果一個系統內需要大量運用裝飾器,那麼上述的寫法可讀性還有待提高。ES decorators 解決了這個問題,這是一個新的語法(語法糖):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 定義 decorator function performanceTiming(...args) { // 返回包裝後的方法 return function(target, key, descriptor) { // ... }; } class MyClass { // 使用這種語法修飾方法 func @performanceTiming func() {} } |
新的 decorator 語法 @xxx
的形式非常類似 Java Annotation,不過後者作為靜態語言,其 Annotation 的實現機制以及使用場景和 ES decorators 都有區別,這是一個題外話。事實上,ES decorators 完全借鑑自 Python 的 decorators。
同時,聰明的你應該發現,相比手寫裝飾器,新的語法中其實「該寫的東西一個都沒少」。那這個 decorators 語法有什麼意義呢?
在我看來,這種語法糖對 decorators 的「定義」和「呼叫」都做了收斂,帶來了「形式美感」。說人話,可讀性更好。
- 在 decorators 定義時,約束了裝飾器的輸入(固定的幾個相關引數)和輸出(返回一個 function),使所有裝飾器風格得到收斂。
- 在 decorators 呼叫時,以無侵入的語法「修飾」類或方法,可維護性和可讀性都提升很多。
這兩個優勢,讓我想到 ES decorators 的一個重要使用場景,便是應用於構架一致性 API。
構架一致性 API
對於多人開發的大型類庫來說,「一致性」是很重要同時也很難執行的一個課題。這裡的「一致性」包括:
- 各模組提供一致的標準公用功能。
- 公用功能的實現和呼叫方式也保持一致。
- 整體 API 的風格一致。
其中 1、2 兩點可以通過引入 ES decorators 機制來更好地達到。
實踐演示
先封裝好部分 decorators(可參見 @ali/universal-decorator
這個包),這裡選取兩個裝飾器:
@deprecated
– 用於修飾類的方法,如果方法被呼叫,則在 console 中提示此方法已經過時,以便開發者轉而呼叫其他方法。@moduleLevel
– 這是 Rax 體系下模組類的一個靜態成員標準欄位,可取值為幾個有限的列舉,此裝飾器對此做了約束。
接下來具體地應用到庫中。
例如 @ali/universal-tracker
中,report()
方法已經遷移到了 @ali/universal-goldlog
,原方法已經廢棄,則可以寫作:
1 2 3 4 5 6 7 8 9 10 11 |
import {deprecated} from '@ali/universal-decorator'; class Tracker { @deprecated('This method is moved to universal-goldlog.', { url: 'http://web.npm.alibaba-inc.com/package/@ali/universal-goldlog' }) report() { // ... } } |
然後在呼叫 report()
後則會提示:
這樣,在相關的所有庫中都引入類似的裝飾器,從而保證 API 表達上的一致,並且這些公共邏輯遵循一致的實現。
另外還有一個例子,可以用來對類的欄位做約束。以大量基於 Rax 的頁面模組為例,這些模組 class 需要宣告一個靜態屬性 moduleLevel
是 app 級別還是 page 級別,以便於框架將其渲染到對應的容器中。但是靜態成員的賦值不夠清晰明朗,也不能對列舉值做約束。使用 decorators 來改寫則:
1 2 3 4 5 6 7 |
import {moduleLevel} from '@ali/universal-decorator'; @moduleLevel('page') class MyModule1 {} @moduleLevel('other') class MyModule2 {} |
moduleLevel 這個 decorator 將為類賦上一個名為 moduleLevel
的靜態成員,並且會對傳入值作判斷,如果入參不是 'page'
或 'app'
,則發出警告:
最後,由於使用了 ES decorators 語法的程式碼,類似於一種宣告式的標記,所以更利於我們對這些程式碼作靜態分析,比如進一步的提前校驗,或是條件編譯等等。這部分更多的想法和思路,有待發掘。
引用
題圖:一棵被裝飾得五光十色的聖誕樹。很多涉及到 decorator 的文章動不動就拿聖誕樹來舉例子,儼然 Christmas tree 是 decorate 的固定賓語。?