面向方面編程

 

1.引言

軟件開發的目標是要對世界的部分元素或者信息流建立模型,實現軟件系統的工程需要將系統分解成可以創建和管理的模塊。於是出現了以系統模塊化特性的面向對象程序設計技術。模塊化的面向對象編程極度極地提高了軟件系統的可讀性、複用性和可擴展性。向對象方法的焦點在於選擇對象作爲模塊的主要單元,並將對象與系統的所有行爲聯繫起來。對象成爲問題領域和計算過程的主要元素。但面向對象技術並沒有從本質上解決軟件系統的可複用性。創建軟件系統時,現實問題中存在着許多橫切關注點,比如安全性檢查、日誌記錄、性能監控,異常處理等,它們的實現代碼和其他業務邏輯代碼混雜在一起,並散落在軟件不同地方(直接把處理這些操作的代碼加入到每個模塊中),這無疑破壞了OOP的“單一職責”原則,模塊的可重用性會大大降低,這使得軟件系統的可維護性和複用性受到極大限制。這時候傳統的OOP設計往往採取的策略是加入相應的代理(Proxy)層來完成系統的功能要求,但這樣的處理明顯使系統整體增加了一個層次的劃分,複雜性也隨之增加,從而給人過於厚重的感覺。由此產生了面向方面編程(AOP)技術。這種編程模式抽取出散落在軟件系統各處的橫切關注點代碼,並模塊化,歸整到一起,這樣進一步提高軟件的可維護性、複用性和可擴展性。


2.AOP簡介

  AOP: Aspect Oriented Programming 面向切面編程。
  面向切面編程(也叫面向方面):Aspect Oriented Programming(AOP),是目前軟件開發中的一個熱點。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
  AOP是OOP的延續,是(Aspect Oriented Programming)的縮寫,意思是面向切面(方面)編程。
  主要的功能是:日誌記錄,性能統計,安全控制,事務處理,異常處理等等。
  主要的意圖是:將日誌記錄,性能統計,安全控制,事務處理,異常處理等代碼從業務邏輯代碼中劃分出來,通過對這些行爲的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改  變這些行爲的時候不影響業務邏輯的代碼。

  可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種技術。AOP實際是GoF設計模式的延續,設計模式孜孜不倦追求的是調用者和被調用者之間的解耦,AOP可以說也是這種目標的一種實現。

假設把應用程序想成一個立體結構的話,OOP的利刃是縱向切入系統,把系統劃分爲很多個模塊(如:用戶模塊,文章模塊等等),而AOP的利刃是橫向切入系統,提取各個模塊可能都要重複操作的部分(如:權限檢查,日誌記錄等等)。由此可見,AOP是OOP的一個有效補充。

注意:AOP不是一種技術,實際上是編程思想。凡是符合AOP思想的技術,都可以看成是AOP的實現。

3.什麼是方面編程

  在考慮對象及對象與其他對象的關係時,我們通常會想到繼承這個術語。例如,定義某一個抽象類 — Dog 類。在標識相似的一些類但每個類又有各自的獨特行爲時,通常使用繼承來擴展功能。舉例來說,如果標識了 Poodle,則可以說一個 Poodle 是一個 Dog,即 Poodle 繼承了 Dog。到此爲止都似乎不錯,但是如果定義另一個以後標識爲 Obedient Dog 的獨特行爲又會怎樣呢?當然,不是所有的 Dogs 都很馴服,所以 Dog 類不能包含 obedience 行爲。此外,如果要創建從 Dog 繼承的 Obedient Dog 類,那麼 Poodle 放在這個層次結構中的哪個位置合適呢?Poodle 是一個 Dog,但是 Poodle 不一定 obedient;那麼 Poodle 是繼承於 Dog 還是 Obedient Dog 呢?都不是,我們可以將馴服看作一個方面,將其應用到任何一類馴服的 Dog,我們反對以不恰當的方式強制將該行爲放在 Dog 層次結構中。


4.與OOP面向對象編程的區別

  AOP、OOP在字面上雖然非常類似,但卻是面向不同領域的兩種設計思想。OOP(面向對象編程)針對業務處理過程的實體及其屬性和行爲進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。
  而AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有着本質的差異。
  上面的陳述可能過於理論化,舉個簡單的例子,對於“僱員”這樣一個業務實體進行封裝,自然是OOP/OOD的任務,我們可以爲其建立一個“Employee”類,並將“僱員”相關的屬性和行爲封裝其中。而用AOP設計思想對“僱員”進行封裝將無從談起。
  同樣,對於“權限檢查”這一動作片斷進行劃分,則是AOP的目標領域。而通過OOD/OOP對一個動作進行封裝,則有點不倫不類。

  換而言之,OOD/OOP面向名詞領域,AOP面向動詞領域。


5.AOP 的基本概念

       在面向對象編程中,類,對象,封裝,繼承,多態等概念是描述面向對象思想主要術語。與此類似,在面向方面編程中,同樣存在着一些基本概念: 
       聯結點(JointPoint) :一個聯結程序執行過程中的一個特定點。典型的聯結點有:調用一個方法;方法執行這個過程本身;類初始化;對象初始化等。聯結點是 AOP 的核心概念之一,它用來定義在程序的哪裏通過 AOP 加入新的邏輯。 
        切入點(Pointcut) :一個切入點是用來定義某一個通知該何時執行的一組聯結點。通過定義切入點,我們可以精確地控制程序中什麼組件接到什麼通知。上面我們提到,一個典型的聯結點是方法調用,而一個典型的切入點就是對某一個類的所在方法調用的集合。通常我們會通過組建複雜的切入點來控制通知什麼時候被執行。 
        通知(Advice) :在某一個特定的聯結點處運行的代碼稱爲“通知”。通知有很多種,比如
在聯結點之前執行的前置通知(before advice)和在聯結點之後執行的後置通知(after advice) 。 
       方面(Aspect) :通知和切入點的組合叫做方面,所以,方面定義了一段程序中應該包括的邏輯,以及何時應該執行該邏輯。 
       織入(Weaving) :織入是將方面真正加入程序代碼的過程。對於靜態 AOP 方案而言,織入是在編譯時完成的,通常是在編譯過程中增加一個步驟。類似的,動態 AOP 方案則是在程序運行是動態織入的。 
       目標(Target) :如果一個對象的執行過程受到某一個 AOP 的修改,那麼它就叫一個目標對象。目標對象通常也稱爲被通知對象。 
       引入(Introduction) :   通過引入,可以在一個對象中加入新的方法或屬性,以改變它的結構,這樣即使該對象的類沒有實現某一個接口,也可以修改它,使之成爲該接口的一個實現。   

       靜態和動態:靜態 AOP 和動態 AOP 兩者之間的區別主要在於什麼時間織入,以及如何織入。最早的 AOP 實現大多都是靜態的。在靜態 AOP 中,織入是編譯過程的一個步驟。用Java 的術語說,靜態 AOP 通過直接對字節碼進行操作,包括修改代碼和擴展類,來完成織入過程。顯然,這種辦法生成的程序性能很好,因爲最後的結果就是普通的 Java 字節碼,在運行時不再需要特別的技巧來確定什麼時候應該執行通知。這種方法的缺點是,如果想對方面做什麼修改,即使只是加入一個新的聯結點,都必須重新編譯整個程序。AspectJ 是靜態 AOP 的一個典型例子。與靜態 AOP 不同,動態 AOP 中織入是在運行時動態完成的。織入具體是如何完成的,各個實現有所不同。Spring AOP 採取的方法是建立代理,然後代理在適當的時候執行通知。動態 AOP 的一個弱點就在於,其性能一般不如靜態 AOP。而動態AOP 的主要優點在於可以隨時修改程序的所有方面,而不需重新編譯目標。

5.1 橫切技術

        “橫切”是AOP的專有名詞。它是一種蘊含強大力量的相對簡單的設計和編程技術,尤其是用於建立鬆散耦合的、可擴展的企業系統時。橫切技術可以使得AOP在一個給定的編程模型中穿越既定的職責部分(比如日誌記錄和性能優化)的操作。
        如果不使用橫切技術,軟件開發是怎樣的情形呢?在傳統的程序中,由於橫切行爲的實現是分散的,開發人員很難對這些行爲進行邏輯上的實現或更改。例如,用於日誌記錄的代碼和主要用於其它職責的代碼纏繞在一起。根據所解決的問題的複雜程度和作用域的不同,所引起的混亂可大可小。更改一個應用程序的日誌記錄策略可能涉及數百次編輯——即使可行,這也是個令人頭疼的任務。
       在AOP中,我們將這些具有公共邏輯的,與其他模塊的核心邏輯糾纏在一起的行爲稱爲“橫切關注點(Crosscutting Concern)”,因爲它跨越了給定編程模型中的典型職責界限。

5.2 橫切關注點

        一個關注點(concern)就是一個特定的目的,一塊我們感興趣的區域,一段我們需要的邏輯行爲。從技術的角度來說,一個典型的軟件系統包含一些核心的關注點和系統級的關注點。舉個例子來說,一個信用卡處理系統的核心關注點是借貸/存入處理,而系統級的關注點則是日誌、事務完整性、授權、安全及性能問題等,許多關注點——即橫切關注點(crosscutting concerns)——會在多個模塊中出現。如果使用現有的編程方法,橫切關注點會橫越多個模塊,結果是使系統難以設計、理解、實現和演進。AOP能夠比上述方法更好地分離系統關注點,從而提供模塊化的橫切關注點。
       例如一個複雜的系統,它由許多關注點組合實現,如業務邏輯、性能,數據存儲、日誌和調度信息、授權、安全、線程、錯誤檢查等,還有開發過程中的關注點,如易懂、易維護、易追查、易擴展等,

       1 .由不同模塊實現的一批關注點組成一個系統,即把模塊作爲一批關注點來實現,如圖:

      

      通過對系統需求和實現的識別,我們可以將模塊中的這些關注點分爲:核心關注點和橫切關注點。對於核心關注點而言,通常來說,實現這些關注點的模塊是相互獨立的,他們分別完成了系統需要的商業邏輯,這些邏輯與具體的業務需求有關。而對於日誌、安全、持久化等關注點而言,他們卻是商業邏輯模塊所共同需要的,這些邏輯分佈於核心關注點的各處。在AOP中,諸如這些模塊,都稱爲橫切關注點。應用AOP的橫切技術,關鍵就是要實現對關注點的識別。

       2 .識別關注點

      如果將整個模塊比喻爲一個圓柱體,那麼關注點識別過程可以用三棱鏡法則來形容,穿越三棱鏡的光束(指需求),照射到圓柱體各處,獲得不同顏色的光束,最後識別出不同的關注點。

      1 ). 關注點識別:三棱鏡法則,如圖所示:

      

      上圖識別出來的關注點中,Business Logic屬於核心關注點,它會調用到Security,Logging,Persistence等橫切關注點。

  1. public class BusinessLogic {    
  2.        public void SomeOperation() {    
  3.           //驗證安全性;Securtity關注點;    
  4.           //執行前記錄日誌;Logging關注點;    
  5.           DoSomething();    
  6.           //保存邏輯運算後的數據;Persistence關注點;    
  7.           //執行結束記錄日誌;Logging關注點;    
  8.        }    
  9.    }    
         3. 將橫切關注點織入到核心關注點中

        AOP的目的,就是要將諸如Logging之類的橫切關注點從BusinessLogic類中分離出來。利用AOP技術,可以對相關的橫切關注點封裝,形成單獨的“aspect”。這就保證了橫切關注點的複用。由於BusinessLogic類中不再包含橫切關注點的邏輯代碼,爲達到調用橫切關注點的目的,可以利用橫切技術,截取BusinessLogic類中相關方法的消息,例如SomeOperation()方法,然後將這些“aspect”織入到該方法中。將橫切關注點織入到核心關注點中,如圖:

         

       通過利用AOP技術,改變了整個系統的設計方式。在分析系統需求之初,利用AOP的思想,分離出核心關注點和橫切關注點。在實現了諸如日誌、事務管理、權限控制等橫切關注點的通用邏輯後,開發人員就可以專注於核心關注點,將精力投入到解決企業的商業邏輯上來。同時,這些封裝好了的橫切關注點提供的功能,可以最大限度地複用於商業邏輯的各個部分,既不需要開發人員作特殊的編碼,也不會因爲修改橫切關注點的功能而影響具體的業務功能。

6.AOP 實踐    

6.1 JAVA實踐 

在 WEB 程序開發中,我們知道由於 HTTP 協議的無狀態性,我們通常需要把用戶的狀態信息保存在 Session 中。在一些應用場景中,需要用戶必須登錄,才能繼續操作。

傳統實現方法 :
爲此我們在進行每個業務操作之前,傳統的實現方法會加入以下的邏輯:

  1. protected void doPost(HttpServletRequest request, HttpServletResponse response)  throws   
  2. ServletException, IOException {   
  3.    
  4. HttpSession session = request.getSession();   
  5. if(session.getAttribute("user")==null){   
  6.     request. getRequestDispatcher("login.jsp").forward(req,resp);   
  7.     }   
  8.       
  9.    doSpecialBussinessLogic();   
  10. }  

以這種方法實現的邏輯,要求程序員在應該實現登錄檢查的地方,都按以上的方法進行。這必然引起了代碼的大量重複和混亂。在這裏登錄檢查邏輯是一個非主要邏輯,而我們的主邏輯是doSpecialBussinessLogic(),主要邏輯和非主要邏輯的混亂是傳統編程方法的一個主要侷限。

用 AOP技術實現:

AOP的出現,爲以上問題提供了一個很好的解決方案。下面是用Aspectj  完成的登錄檢查邏輯的實現:

  1.   public aspect LoginCheckAOP {   
  2.      
  3.     pointcut loginCheck(HttpServletRequest req, HttpServletResponse resp):  (execution(void   
  4. *..*Action.doPost(HttpServletRequest,   
  5. HttpServletResponse))) && args(req,resp);   
  6.     
  7.   public before(HttpServletRequest req, HttpServletResponse resp) : loginCheck (req,resp) {   
  8.  HttpSession session = request.getSession();   
  9. if(session.getAttribute("user")==null){   
  10.     request. getRequestDispatcher("login.jsp").forward(req,resp);   
  11.     }   
  12. }   
  13. }   

        我們定義了一個名字爲LoginCheckAOP的方面,Aspectj的編譯器通過名字匹配自動把登錄檢查邏輯的代碼插入到需要的地方。 使用 AOP 方法進行登錄檢查比在需要的地方人工的插入檢查代碼有以下幾條好處。 
•  只需要在一個(LoginCheckAOP 方面中)地方放置所有的需要用於檢查的功能代碼。  
•  插入和刪除檢查代碼是很容易的。可以輕易地重新實現不同的檢查方面,而不用對其它代碼進行修改。  
•  在任何需要的地方登錄檢查,即使增加了新方法或新類。這可以消除人爲的錯誤。同時知道所有登錄檢查代碼被刪除了,並且當我們從構建配置中刪除方面時不會忽略     任何東西。   
•  有一個可重複使用的方面,它可以被應用和升級。  

6.2 PHP實踐 

目前的PHP來說,還沒有一個完整的AOP內置實現,雖然出現了RunKit,但一直都以BETA的狀態呆在PECL項目裏,估計很長時間內不太可能成爲PHP的缺省設置。那是不是AOP在PHP裏就破滅了呢?當然不是,因爲我們有__get(),__set(),__call()等魔術方法,合理使用這些方法可以爲我們實現某種程度的“準AOP”能力,之所以說是準AOP,是因爲單單從實現上來看,稱其爲AOP有些牽強,但是從效果上來看,又部分實現了AOP的作用,雖然其實現方式並不完美,但對於一般的使用已經足夠了。

  1. <?php  
  2. /** 
  3.  * 應用程序中某個業務邏輯類 
  4.  * 
  5.  */  
  6. class Target  
  7. {  
  8.     public function foobar(){  
  9.         echo '業務邏輯<br />';  
  10.     }  
  11. }  
  12.   
  13. //業務邏輯類的包裝類  
  14. class AOP  
  15. {  
  16.     private $instance;  
  17.   
  18.     public function __construct($instance) {  
  19.         $this->instance = $instance;  
  20.     }  
  21.   
  22.     public function __call($method, $param) {  
  23.         if(! method_exists($this->instance, $method)) {  
  24.             throw new Exception("Call undefinded method ".get_class($this->instance)."::$method");  
  25.         }  
  26.           
  27.         //前增強  
  28.         $this->before();  
  29.         $callBack = array($this->instance, $method);  
  30.         $return = call_user_func_array($callBack, $param);  
  31.         $this->after();  
  32.         return $return;  
  33.     }  
  34.     /** 
  35.      * 前增強 
  36.      * 
  37.      */  
  38.     public function  before() {  
  39.         echo '權限檢查<br />';  
  40.     }  
  41.     /** 
  42.      * 後增強 
  43.      * 
  44.      */  
  45.     public function after() {  
  46.         echo '日誌記錄<br />';  
  47.     }  
  48. }  
  49.   
  50.   
  51. /** 
  52.  * 工廠方法 
  53.  * 
  54.  */  
  55. class Factory  
  56. {  
  57.     public function getTargetInstance(){  
  58.         return new AOP(new Target());  
  59.     }  
  60. }  
  61.   
  62. //客戶端調用演示  
  63. header("Content-Type: text/html; charset=utf8");  
  64. try {  
  65.     $obj = Factory::getTargetInstance();  
  66.     $obj->foobar();  
  67. catch(Exception $e) {  
  68.     echo 'Caught exception: ',  $e->getMessage();  
  69. }  

利用php5內置的魔術方法__call來實現AOP,唯一的缺點是要在AOP類裏面實例客戶端對象,返回的是被AOP包裝後的對象。因此像get_class會遇到麻煩。

比如說,客戶端通過getBizInstance()方法以爲得到的對象是Target,但實際上它得到的是一個Target的包裝對象AOP,這樣的話,如果客戶端進行一些諸如get_class()之類和對象類型相關的操作就會出錯,當然,大多數情況下,客戶端似乎不太會做類似的操作。

其實我們在代理模式也提到過,這其實就是一個動態代理模式。

我們用 runkit 擴展來實現方法調用攔截的例子:

  1. /** 
  2.  * 應用程序中某個業務邏輯類 
  3.  * 
  4.  */  
  5. class Target  
  6. {  
  7.     public function foobar(){  
  8.         echo '業務邏輯<br />';  
  9.     }  
  10. }  
  11. runkit_method_rename('Target''foobar''#foobar');  
  12. runkit_method_add('Target','add','$a,$b','  
  13.     echo "before call\n";  
  14.     $ret = $this->{"#foobar"}($a,$b);  
  15.     echo "after call\n";  
  16.     return $ret;  
  17. ');  
也有人用了繼承方式來實現:

  1. <?php  
  2.   
  3.   
  4. //業務邏輯類的包裝類  
  5. class AOP  
  6. {  
  7.     private $instance;  
  8.   
  9.     public function __construct() {  
  10.     }  
  11.   
  12.     public function __call($method$param) {  
  13.         if(strchr($method,'Aop_')){  
  14.             $method = str_replace('Aop_','',$method);  
  15.              if(! method_exists($this$method)) {  
  16.                 throw new Exception("Call undefinded method ".get_class($this)."::$method");  
  17.             }  
  18.         }  
  19.         //前增強  
  20.         $this->before();  
  21.         $callBack = array($this$method);  
  22.         $return = call_user_func_array($callBack$param);  
  23.         $this->after();  
  24.         return $return;  
  25.     }  
  26.     /** 
  27.      * 前增強 
  28.      * 
  29.      */  
  30.     public function  before() {  
  31.         echo '權限檢查<br />';  
  32.     }  
  33.     /** 
  34.      * 後增強 
  35.      * 
  36.      */  
  37.     public function after() {  
  38.         echo '日誌記錄<br />';  
  39.     }  
  40. }  
  41.   
  42.   
  43. /** 
  44.  * 應用程序中某個業務邏輯類 
  45.  * 
  46.  */  
  47. class Target extends AOP   
  48. {  
  49.     public function foobar(){  
  50.         echo '業務邏輯<br />';  
  51.     }  
  52. }  
  53.   
  54.   
  55. //客戶端調用演示  
  56. header("Content-Type: text/html; charset=utf8");  
  57. try {  
  58.     $obj = new  Target();  
  59.     $obj->Aop_foobar();  
  60. } catch(Exception $e) {  
  61.     echo 'Caught exception: ',  $e->getMessage();  
  62. }  
這個有明顯的缺點:

1)嚴格的限制,如需要增強的方法名需要加AOP前綴。
2)如果Target類已經繼承另外的父類,無法在繼承AOP類。
3) AOP的實現,一個很重要的前提就是不能對源代碼有很明顯的侵入,而這裏增強的目標對象要繼承AOP類,無疑侵入了對象.

     除了以上的實現方法,我們可以使用配置文件來配置把哪些關注點代碼增強到目標對象的切入點上。


7.結論

    面向方面編程是一個令軟件開發人員激動的新技術, 它被用來尋找軟件系統中新的模塊化特性。面向方面編程是作爲面向對象編程技術的一種補充而出現,它們之間並不存在競爭關係,實際上它們在軟件開發中相輔相成,互爲補充。面向方面編程作爲一種嶄新的編程技術,它具有十分光明的應用前景。 


發佈了9 篇原創文章 · 獲贊 17 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章