開發應用程式某功能時,需撰寫程式碼定義執行流程,隨著後續需求的增加,原執行流程中會參與更多程式碼,用來定義實現新需求的流程,有時新定義的程式碼與主要執行流程一致,有時這些程式碼看來就是硬生生切入主要流程,這類直接切入主流程的程式碼越多,主流程的意圖就會越模糊,類似邏輯若隨處散落在應用程式的各個主要流程之中,也會令應用程式難以維護。

以攔截過濾器抽取橫向流程
假設你完成Web應用程式某個請求回應功能,現在打算了解請求與回應間的時間差,因而在原始碼中增加效能量測的程式碼;接著想要記錄瀏覽器發送的請求資訊,因此又安插了日誌輸出的程式碼;後來打算限制來自某些網域的請求,再度加入安全檢查的程式碼;你可能有多個頁面,且每個功能流程都為了這些需求,而安插了類似的程式碼,然而有著相似邏輯的程式碼散落在各處,將來要拿掉功能或修改時,就得找出這些程式碼修改。

效能量測、日誌輸出、安全檢查等流程,與應用程式完成請求回應功能的原有流程無關,應以攔截器(Interceptor)模式的概念實現為獨立元件,必要時透過設定或標註方式參與原有流程,避免模糊主要流程之意圖,抽離橫切流程也讓它們得以重用。

以Java EE來說,常見攔截過濾器(Intercepting Filter)模式之實現,像是Servlet API的Filter元件,就可用來實現方才談到的效能量測、日誌輸出、安全檢查等流程,在必要時,使用部署描述檔(Deployment descriptor)或以標註方式告知容器,讓Filter元件定義的程式碼能參與請求回應的流程。

而Web容器實現攔截過濾器的方式,是於執行Servlet的service方法前,檢查有無設置Filter元件清單,有的話,就逐一執行Filter元件定義之流程。

類似地,在Web框架中,也可見到類似的實現,像是Struts 2或是Spring MVC,都是於執行Action或Controller元件前,檢查有無設置Interceptor或HandlerInterceptor清單,有的話,就逐一執行各自定義之流程,看來就像切入了請求回應的流程之中。由於容器或框架實現了攔截過濾流程,開發者得以將橫切流程定義為可重用的元件,而後續的Servlet、Action或Controller元件不用作任何修改,想了解主要請求回應處理流程,從中一目瞭然。

更彈性的切面導向設計
《Clean Code》書中說:「將所有關注的事(Concerns)分離開來,是軟體技巧中,最古老、最重要的設計技巧之一。」有些關注與程式主流程一致,易識別與分離為獨立的程式庫或框架,有些則是對主要流程橫切的關注(Cross-cutting concerns),易破碎地出現在各主要流程,面對這些,以設計獨立可重用的切面(Aspect)模組為目標,就是切面導向程式設計(Aspect-oriented programming,AOP)。

若以重用橫切關注的角度來看,攔截過濾器可算是AOP的簡單實現,效能量測、日誌輸出、安全檢查等,這類定義在攔截過濾器元件之流程,稱為Advice(建議),也就是建議參與主流程之流程,只不過Advice能參與主要流程的Join point(參與點)較單純或固定,通常是特定對象的特定方法之前後,像Servlet API中的Filter元件,Join point是Servlet物件的service方法前後,Struts 2的Interceptor,參與點是Action物件的execute或指定方法之前後。

在更複雜的情境,會希望能在更多Join point進行流程建議,因此須能定義Pointcut(參與點集合),像是運用表達式來描述Join point的物件型態、方法名稱、參數模式、傳回型態等,當程式執行至滿足Pointcut描述條件時,就會執行Advice的流程,AOP稱這過程為:將Advice流程織入(Weave)主流程。

在這一連串描述之後可以看到,為了更廣泛描述與定義那些橫切主流程的關注,AOP中充斥著切面、Advice、Join point、Pointcut、織入等名詞,讓人不容易理解其意義(而且有些在中文上找不出適切的譯名),甚至過去還曾有過AOP將取代OOP(Object-oriented programming)的謬論。

AOP簡單來說,就是要實現關注分離(Separation of concerns),但對象是對主流程橫切的關注,識別出來才是最主要精神,後續再了解採用的工具,如何支援切面、Advice、Join point、Pointcut。

語言動態性影響橫向關注抽離

雖然以橫切關注的分離來說,攔截過濾器也算是AOP的簡單實現,不過談到AOP這個名詞,多數人傾向於聯想到動態改變物件的行為,當然這實現上依使用的語言、框架與技術而有所不同,然而基本上是代理(Proxy)模式進一步的擴充實現。

對於Python、Ruby這類動態定型語言來說,由於變數本身沒有型態,操作時僅要求實際物件擁有對應行為或協定,因而實現代理機制時,本身就比較簡單。例如,想在執行物件execute方法前,增加日誌行為,Python的話,可以如下定義代理物件:

class LoggingProxy:
def __init__(self, target):
self.target = target
def execute(self):
# do logging
self.target.execute()

之後以LogginProxy(target)建立實例,並執行其execute方法,就會看到增加了日誌行為,如果使用Java這類靜態定型語言,就麻煩多了,代理物件與目標物件還得實現相同介面。不過就算使用動態定型語言,逐一為各物件定義代理物件,也很煩人。

若語言本身就擁有執行時期改變物件結構與行為的能力,實現此需求就簡單許多,例如Python中函式是物件,可直接以新函式置換原函式,或是在Ruby中可搭配alias_method、define_method及開放類別等方式修改物件行為,在這類語言中,識別、分離並實現橫切關注,相對來說是稀鬆平常。

如果是Java這類動態性低的語言,就得運用程式碼生成、反射(Reflection)等機制與來自動產生代理物件,例如修改原始碼或位元組碼、使用動態代理程式庫來動態產生代理物件等。

然而程式實作上都有一定的難度,因而在Java這類語言中,識別出橫切關注後,通常得借助一些工具、程式庫或框架,來完成橫切關注之分離與實現,並配合Pointcut表達式,告知工具、程式庫或框架,在哪些時機點將Advice織入主要流程。

過去這類工具不普及,實現橫切關注並非尋常工作,因而這類工具普及並帶來各式術語時,Java這類語言使用者感覺到AOP帶來許多新奇觀念。

保持適當的關注分離
若發現應用程式起始,經常進行物件間相依關係的建立流程,之後才是執行商務流程,此時,相依關係的建立流程有著類似模式,因而從流程中分離出來,成為可重用的依賴注入框架,這就可讓焦點更集中在後續商務流程。在發現Web應用程式請求回應之間,有著進行請求處理、轉發與頁面呈現的類似流程,將這個流程抽離出來成為可重用的框架,就可將焦點更集中在各式商務相關元件的實作。

AOP概念想做的其實也類似,將關注的事抽離出來以便重用,只不過這個該抽離出來的關注,不若前段描述會與主要流程有一致方向,而是橫切入主要流程,實際上與攔截過濾器該做的事相似。

攔截過濾器的應用範圍之外,就算你用的是動態性不高的語言,現在也有合適工具輔助,因而不用太在意看似抽象複雜的名詞,重點是保持適當的關注分離,剩下的就是熟悉並善用你選擇的工具。

專欄作者

熱門新聞

Advertisement