第222期 / April 5, 2016

研發新視界

分享到臉書!分享到維特!分享到噗浪!分享到Google+!分享到微博!轉寄友人友善列印

觀點導向程式設計(AOP)與代理機制實作

作者/黃瀚明

[發表日期:2016/4/6]

前言

想像一個情境:你撰寫了一個飛機起飛前的檢查程式,用於在飛機起飛前執行相關的檢查動作,如飛機是否在指定的跑道準備起飛、跑道是否淨空等,可能像這樣:


圖一


在requestTakeOff方法裡面,第14行先記錄了”收到起飛前跑道淨空的要求”,然後第15行才是真正檢查飛機是否在23號跑道,最後在回傳結果前再記錄一次”已檢查跑道狀況”。這樣看起來似乎很完美,但這樣的設計卻將不屬於商業邏輯的”記錄”動作也混雜進了真正的商業邏輯,尤其”記錄”動作可能不僅限於起飛時發生,飛機降落時應該也會需要記錄,如此一來當程式規模逐漸擴大,維護或修改這些真正的商業邏輯將會變得異常的困難。日後若要修改這個”記錄”的方式(例如將System.out.println改為Logger)會非常麻煩,而且修改原有程式碼也違反了OCP(Open-Closed Principle)原則1。

要避免這樣的情況,我們必須將”記錄”這件事從真正的商業邏輯中抽離,將這種分散在各商業邏輯中的共同功能提取出來,並模組化以使程式形成低耦合力、高內聚力,同時提高程式可讀性及再利用性。而這樣的需求,就促進了AOP (Aspect Oriented Programming)的產生。

觀點導向程式設計(AOP)

AOP可以翻譯為”面向問題的程式設計”、”觀點導向程式設計”或”剖面導向程式設計”,主要應用在日誌記錄、性能統計、安全控制、事務處理及異常處理等功能,其目的是要將這些功能從商業邏輯中抽取出來,如前段飛機起飛的程式碼中提到的”記錄”功能即可套用AOP的概念,將之從商業邏輯中(檢查跑道)提取出來。

在AOP中,這些與商業邏輯無關,卻被插入商業邏輯的系統層面功能被稱為Cross-cutting concerns,這些Cross-cutting concerns橫切入主要的商業邏輯,使得商業邏輯模組變得更加複雜,AOP將Cross-cutting concerns提取出來,除了簡化商業邏輯模組外,也讓這些被提出來的Cross-cutting concerns模組化。因此,Cross-cutting concerns可以很容易的被應用到各種商業邏輯上,同時,想修改Cross-cutting concerns的功能時,也無須更動到原有的商業邏輯。應用AOP,使我們的程式能夠做到”對擴展開放、對修改關閉”,符合了OCP原則。下圖描繪了Cross-cutting concerns的概念。


圖二


使用JAVA實作AOP

一、靜態代理

在JAVA中,實作AOP的方法是使用設計模式中的代理模式(Proxy Pattern),代理模式提供一個中介的類別,透過代理類別替原始物件做事。換句話說,當我們需要做某件事時,不直接存取原始物件,而是透過代理的管道來處理,以前述飛機起飛程式碼為例,轉換成代理模式後以UML示意如下:


圖三


轉換成代理模式後,新增一個TakeOffProxy代理類別,此代理類別與原本的TakeOff類別實作同一個介面ITakeOff,而代理類別中的requestTakeOff方法實際上則是呼叫被代理類別TakeOff的requestTakeOff方法,由被代理類別來執行真正的商業邏輯。因此,我們可以在TakeOffProxy的requestTakeOff方法中,加上需要的Cross-cutting concerns。AirportDemo則是針對介面ITakeOff進行操作,當然其操作的實體是TakeOffProxy。這樣說可能很抽象,讓我們來看看程式碼:


圖四


無論是代理類別(TakeOffProxy)或是被代理類別(TakeOff)都必須實作一個共同的介面,這個介面定義了一個方法讓代理類別與被代理類別實作,在此我們將這個介面命名為ITakeOff。因為定義了ITakeOff這個介面,我們要呼叫requestTakeOff這個方法時,可針對介面操作,無須知道實作介面的類別究竟為何,也不用了解這個方法究竟是如何實作的,在需要修改方法裡面的內容時,也不需要更改呼叫方式。如果還是很模糊,看完所有程式碼後再回來看這段文字會更了解。我們接著看代理類別TakeOffProxy的程式碼:


圖五


TakeOffProxy在建構子內綁定了真正執行requestTakeOff的ITakeOff型態物件,同時,TakeOffProxy在requestTakeOff內將執行requestTakeOff的工作交給了這個物件。當然,這裡我們可以再次發現,因為是針對介面操作,所以TakeOffProxy也無須知道實際實作內容為何,低耦合的結果就是我們可以隨意修改requestTakeOff實際的工作內容,不用擔心其他模組會被影響。最後,TakeOffProxy把真正的商業邏輯交給TakeOff做,自己只負責日誌記錄這件事,日後想修改”日誌記錄”的實作時,只要在TakeOffProxy內修改就好。例如這裡把日誌記錄拉出來變成一個方法,同時把System.out.println改成了Logger。


圖六


被代理類別TakeOff這邊,由於日誌記錄被拉到TakeOffProxy內,TakeOff只需專責處理requestTakeOff真正的工作即可,其程式碼不但簡化,也單純化,同樣的要修改實作方法時,也無須擔心影響其他模組。看完上面兩個類別,我們可以發現我們已經將Cross-cutting Concerns(即日誌記錄)抽離出來,TakeOffProxy專責處理日誌處理的工作,而TakeOff只要負責真正的商業邏輯即可。最後我們來看一下怎麼使用這些類別:


圖七


使用這些類別的方法非常簡單,在第15行我們針對介面ITakeOff進行操作,在第14行指定了代理及被代理類別。換個方式說,我們在14行指定了Cross-cutting Concerns為TakeOffProxy(處理日誌記錄),而商業邏輯是由TakeOff類別來處理。像上面這樣的代理模式實作方式,我們稱作靜態代理(Static Proxy),但這樣的實作方式有其缺點,當商業邏輯數量龐大時,我們必須撰寫非常多的代理類別,因為不同的商業邏輯,其呼叫的方法可能不同。如飛機起飛,呼叫的是requestTakeOff,但飛機降落我們可能呼叫的是requestLanding,而且傳入的參數可能不同,這個時候我們可能還是會需要建立多個不同的類別來處理一樣的事務(日誌記錄)。要解決這樣的問題,我們必須利用JAVA提供的反射機制。

二、動態代理

利用JAVA反射機制實作的代理模式,我們稱作動態代理(Dynamic Proxy),動態代理的概念與靜態代理相同,只是實作方式不同。我們直接來看程式碼:


圖八


我們必須定義一個介面讓被代理類別實作,在介面內指定需實作的方法,不過與靜態代理有些不同,代理類別無須實作這個介面,實作動態代理時,代理類別甚至不用了解它應該實作哪些介面,只要負責他自己的事務即可(在此例中就是日誌處理)。接著我們看看實作動態代理時,代理類別應該怎麼撰寫:


圖九


要實作動態代理,我們必須建立一個Handler,且此Handler必須實作java.lang.reflect.InvocationHandler這個介面,並實作出該介面定義的invoke方法,可以看到除了這個方法外,其他的部分和靜態代理時幾乎相同,除了第19行的物件由原本的ITakeOff型別變成了Object型別。Object型別是所有物件的父類別,所以可以接收任何型別的實體,也因此LogHandler不用關心他究竟"橫切"入了哪種商業邏輯,只要專心在處理日誌記錄即可。

LogHandler在invoke方法內仍然是將實際的商業邏輯委派給被代理類別,只不過這次LogHandler不知道被代理類別是誰(因為被綁定成Object類別了),甚至連實際商業邏輯的方法名稱都不知道,只透過method.invoke來呼叫真正的商業邏輯,由此達成了LogHandler與商業邏輯的完全脫鉤。


圖十


在動態代理中,被代理類別與靜態代理中完全一樣,只要專責處理商業邏輯即可。接著我們看看動態代理的使用方法:


圖十一


從第16行我們可以看到LogHandler的建構子傳入的參數是TakeOff實體,表示實際執行商業邏輯的類別是TakeOff類別。第17行則是利用了JAVA的反射機制動態建立一個代理類別,然後在第19行透過這個代理類別執行商業邏輯。前面提及動態代理的好處是我們遇到不同的商業邏輯時,無須撰寫新的代理類別,如上述例子中,當我們想要把”起飛”改成”降落”時,只要改變AirportDemo中的呼叫方式,將new TakeOff()改成new Landing()、TakeOff.class…改為Landing.class…即可改變商業邏輯(如landing.requestLanding()),完全無需修改LogHandler。

當然,靜態代理有其缺點,動態代理也不盡然完美,由於動態代理的代理類別是由JAVA動態在執行期(runtime)產生,也間接影響了程式的執行效能,這是JAVA動態代理美中不足的地方。

三、動態代理綁定多Cross-cutting Concerns

眼尖的你或許發現了,前面的例子中,無論是靜態代理或動態代理,其Cross-cutting Concern都只有一個(日誌處理),而且這兩種實作中,代理類別或Handler的建構子都只接收一種被代理類別。換句話說,目前看到的例子都只有一個Cross-cutting Concern。但一般的系統通常不會只有一種Cross-cutting Concern,除了日誌記錄外,可能還需要權限稽核、例外處理等Cross-cutting Concerns。如此例中,飛機起飛前除了檢查跑道,可能還需要檢查這台飛機是否已核准飛行,這麼一來勢必要建立並綁定多個被代理類別或是Handler了,那麼要如何解決這個問題呢?

其實我們只要將動態代理稍微修改一下就可以達到目的:將LogHandler改為一個Generic的Handler,並在此Handler裡面呼叫其他的Handler,就可綁定多個Handler了。讓我們來看看程式碼:


圖十二



圖十三


由於我們的目的是綁定多個Cross-cutting Concerns,不更動商業邏輯,因此商業邏輯和其實作的介面無須更改,需要較多修改的部分,是前面提及的一般化Handler-GneHandler,我們需要在這個Handler裡呼叫其他的Handler,而為了方便呼叫,我們定義一個共同的介面-AOPHandler:


圖十四


這個介面定義了兩個方法:beforeInvoke和afterInvoke,分別讓實作AOPHandler的Handler定義商業邏輯執行前和執行後需要做的事。我們先來看看呼叫這些Handler的GneHandler,會更明白為何需要如此定義AOPHandler:


圖十五


GneHandler是由前述例子的LogHandler改寫而來,其實他的角色和LogHandler一模一樣,只是在這裡我們把"日誌記錄"這件事改成了"呼叫其他的Handler"。可以看到在真正的商業邏輯執行前,依序呼叫了各Handler的beforeInvoke,而商業邏輯執行後則呼叫了afterInvoke。由此,我們便可達成綁定多個Handler的目的。我們接著看看GneHandler的使用方法:


圖十六


因為是從動態代理改寫而來,架構基本上和動態代理一樣,這裡我們要看的是20到24行,加入了不同的Handler到handlers ArrayList,並傳入GneHandler的建構子,讓GneHandler能夠依序觸發這些Handler。當然,這些Handler處理不同的事務,修改這些Handler也不會影響到其他的Handler。


圖十七


結語

AOP只是一個概念,無論是靜態代理、動態代理都只是AOP的實作,只要符合AOP的概念即可。除了本文提及的靜態代理與動態代理,還有其他實作AOP的框架(Framework),目前在JAVA領域中最知名的AOP框架應屬AspectJ,有興趣的話可以到eclipse.org/aspectj了解。最後,雖然AOP帶來許多好處,但不可諱言的,實作AOP也讓我們的程式變得稍微複雜,因此不可過度使用AOP,且最好要搭配詳盡的說明文件,以便使新進員工快速了解程式架構。

(本文轉載自RUN!PC)

附錄

1 OCP原則由Bertrand Meyer提出,其談到”Software entities should be open for extension but closed for modification”,也就是”對擴展開放、對修改關閉”, 如果某個模組的功能是可擴展(新增)的,則此模組是對擴展開放的。另外,如果某個模組被其他模組呼叫,且不允許修改的話,則此模組是對修改關閉的。換句話說,”對擴展開放、對修改關閉”其意義就是軟體應該透過擴展功能來解決需求的變化,而不是修改原有的程式碼來適應變化。(本文作者現服務於凌群電腦,並由凌群電腦投稿RUN!PC刊載)

參考資料

1. http://projects.eclipse.org/projects/tools.aspectj
2. http://blog.chinaunix.net/uid-29140694-id-3983948.html
3. http://www.cnblogs.com/ywqu/archive/2010/01/22/1653875.html
4. http://www.dotblogs.com.tw/alonstar/archive/2011/05/13/25025.aspx
5. http://baike.baidu.com/view/866233.htm
6. http://www.cnblogs.com/flyoung2008/p/3251148.html
7. http://fecbob.pixnet.net/blog/post/38055029-java%E5%8B%95%E6%85%8B%E4%BB%A3%E7%90%86%E5%85%A7%E9%83%A8%E5%AF%A6%E7%8F%BE
8. http://fanli7.net/a/JAVAbiancheng/JAVAzonghe/20140228/473997.html
9. http://sea-taiwan.blogspot.tw/2009/03/ocp.html
10. http://openhome.cc/Gossip/SpringGossip/DynamicProxy.html
11. http://openhome.cc/Gossip/SpringGossip/FromProxyToAOP.html
12. http://openhome.cc/Gossip/SpringGossip/AOPConcept.html
13. http://docs.jboss.org/aop/1.1/aspect-framework/userguide/en/html/what.html
14. http://www.javaworld.com/article/2073918/core-java/i-want-my-aop---part-1.html
15. http://www.javacodegeeks.com/2012/06/simple-introduction-to-aop.html
16. http://blog.csdn.net/hguisu/article/details/7586704
17. http://www.ibm.com/developerworks/rational/library/mar06/pollice/
18. http://itpx.eol.cn/shiti_jc_2246/20110831/t20110831_677639.shtml
19. http://big5.webasp.net/article/15/14040.htm
20. http://yizhenn.iteye.com/blog/2089237
21. Guidelines for Aspect-Oriented Design, Christina von Flach G. Chavez, Carlos J.P