Node.js 使用 AOP (剖面導向程式設計)

什麼是 AOP?參考下面文章:

https://openhome.cc/Gossip/SpringGossip/AOPConcept.html

其中

Cross-cutting concerns若直接撰寫在負責某商務的物件之流程中,會使得維護程式的成本增高,例如若您今天要將物件中的記錄功能修改或是移除該服務,則必須修改所 有撰寫曾記錄服務的程式碼,然後重新編譯,另一方面,Cross-cutting concerns混雜於商務邏輯之中,使得商務物件本身的邏輯或程式的撰寫更為複雜。

現在為了要加入日誌(Logging)與安全(Security)檢查等服務,物件的程式碼中若被硬生生的寫入相關的 Logging、Security 程式片段,則可使用以下圖解表示出 Cross-cutting 與 Cross-cutting concerns 的概念:

Cross-cutting concerns 若直接撰寫在負責某商務的物件之流程中,會使得維護程式的成本增高,例如若您今天要將物件中的日誌功能修改或是移除該服務,則必須修改所 有撰寫曾日誌服務的程式碼,然後重新編譯,另一方面,Cross-cutting concerns 混雜於商務邏輯之中,使得商務物件本身的邏輯或程式的撰寫更為複雜。

細節就要再請大家直接透過上面文章進行了解

目前在 Node.js 找到比較多人用的 AOP 套件如下:

此文章將會用 meld 來做展示

假設我們要在訂單建立完成後要進行 email 的送出

// 建立訂單
const order = await OrderService.createOrder(transaction, data);  
// 將訂單資料送出 mail
await MessageService.sendMail(order);  

當我們需要把它改成使用 SMS 簡訊的形式,我們就需要改為

// 建立訂單
const order = await OrderService.createOrder(transaction, data);  
// 將訂單資料送出 SMS
await MessageService.sendSMS(order);  

只有一個地方還好,若有多個地方的話就需要改為用 if else 判斷,如

// 建立訂單
const order = await OrderService.createOrder(transaction, data);  
// 將訂單資料送出
if(config.sendEmail)  
  await MessageService.sendMail(order);
if(config.sendSMS)  
  await MessageService.sendSMS(order);

這樣是其中一個作法,我們可以換個方式,參考下面圖片

來源:http://www.slideshare.net/WidhianBramantya/icoict-new

其中 OrderService.createOrder(transaction, data);

我們可以根據需要將一些附加功能組合後成為實際上要用的函式。

除了一開始介紹的 send mail 的功能,也有像是 logging 或是權限控管,讓我們主要的程式專注在主要的商業邏輯,其他附加的功能可以根據需要掛載上去。

有了主要的概念後,我們可以使用 meld 來改寫目前的程式,在伺服器啟動時,我們可以呼叫下面函式:

var sendMailAround = async function(joinpoint) {  
  console.log("=== sendMailAround start ===");
  var result = await joinpoint.proceed();
  await MessageService.sendMail(result);
  console.log("=== sendMailAround success ===");
  return order;
}
global.OrderService.createOrder = meld.around(OrderService.createOrder, sendMailAround);  

其中 meld.around(OrderService.createOrder, sendMailAround);

將把 OrderService.createOrder 轉化為 joinpoint.proceed();

如此我們就可以在 sendMailAround 進行後續的處理,原本的邏輯就會被改為

var result = await joinpoint.proceed();  
await MessageService.sendMail(result);  

類似的處理方式可以改為

meld.around(OrderService.createOrder, sendSmsAround);  

如此就可以讓本來需要 if else 的處理,改為組合積木的方式來進行,若 log 或是 email 我們都不需要,主要的程式碼不需要做任何調整,只要把設定檔拿掉就可以讓主要流程繼續運作。

另外一個角度,若有跟類似需要送 mail 的函式,比如註冊成功,我們可以重覆使用 sendMailAround 如:

meld.around(AuthService.register, sendSmsAround);

重覆使用 sendSmsAround 這層附加功能,只要在設計上考慮到回傳格式的一致。另外一方面來說,若有考慮到這樣的應用情境,也可以讓程式碼更容易進行組合拆解。

實際使用如下

console.log("=== OrderService.createOrder start ===");  
const order = await OrderService.createOrder(transaction, data);  
console.log("=== OrderService.createOrder end ===");  

AOP 設置:

var sendMailAround = async function(joinpoint) {  
  console.log("=== sendMailAround start ===");
  var result = await joinpoint.proceed();
  await MessageService.sendMail(result);
  console.log("=== sendMailAround success ===");
  return order;
}
global.OrderService.createOrder = meld.around(OrderService.createOrder, sendMailAround);  

執行結果如下

=== OrderService.createOrder start ===
=== sendMailAround start ===
2017-02-10 15:20:35.40 <info> OrderService.js:17 (_callee$) 產生訂單編號: 20170210539400001  
=== sendMailAround success ===
=== OrderService.createOrder end ===

若我們不需要 send mail 我們只要把下面程式碼移除

//global.OrderService.createOrder = meld.around(OrderService.createOrder, sendMailAround);

實際測試就會變為

=== OrderService.createOrder start ===
2017-02-10 15:22:18.80 <info> OrderService.js:17 (_callee$) 產生訂單編號: 20170210879800001  
=== OrderService.createOrder end ===

可以很方便的進行替換或是移除。

結論

AOP 的概念,筆者一開始知道這樣的應用是從 JAVA Spring 的框架而來,在 JAVA 語言是一個很成熟的技術,唯 Node.js 還未有 Spring 這樣的公司在背後發展維護類似 Spring 這樣的框架,即使是上面星星數較多的專案,也已經 2 年多沒有在維護,著時可惜。

不過 AOP 這樣不同方向的開發方式是值得參考的,除了讓商業邏輯可以更乾淨之外,也可以用讓主要程式根據不同需求進行組合,原本寫程式為橫向一層一層堆疊,變得可以用縱向切入的方式替程式碼加上不同的處理,在程式架構上可以有更多選擇。

同樣的概念也一直不停在不同的語言實作,所以學習程式開發,還是要在一個語言學習夠久,相關特性也比較容易融會貫通,與大家分享!