AOP介紹
AOP(Aspect-Oriented Programming,面向方面編程),可以解決面向對象編程中的一些問題,是OOP的一種有益補充。面向對象編程中的繼承是一種從上而下的關系,不適合定義從左到右的橫向關系,如果繼承體系中的很多無關聯的對象都有一些公共行為,這些公共行為可能分散在不同的組件、不同的對象之中,通過繼承方式提取這些公共行為就不太合適了。使用AOP還有一種情況是為了提高程序的可維護性,AOP將程序的非核心邏輯都“橫切”出來,將非核心邏輯和核心邏輯分離,使我們能集中精力在核心邏輯上,例如圖1所示的這種情況。
圖1 AOP通過“橫切”分離關注點
在圖1中,每個業務流程都有日志和權限驗證的功能,還有可能增加新的功能,實際上我們只關心核心邏輯,其他的一些附加邏輯,如日志和權限,我們不需要關注,這時,就可以將日志和權限等非核心邏輯“橫切”出來,使核心邏輯盡可能保持簡潔和清晰,方便維護。這樣“橫切”的另外一個好處是,這些公共的非核心邏輯被提取到多個切面中了,使它們可以被其他組件或對象復用,消除了重復代碼。
AOP把軟件系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關系不大的部分是橫切關注點。橫切關注點的一個特點是,它們經常發生在核心關注點的多處,而各處都基本相似,比如權限認證、日志、事務處理。AOP 的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
實現AOP的一些方法
實現AOP的技術分為:靜態織入和動態織入。靜態織入一般采用抓們的語法創建“方面”,從而使編譯器可以在編譯期間織入有關“方面”的代碼,AspectC++就是采用的這種方式。這種方式還需要專門的編譯工具和語法,使用起來比較復雜。我將要介紹的AOP框架正是基於動態織入的輕量級AOP框架。動態織入一般采用動態代理的方式,在運行期對方法進行攔截,將切面動態織入到方法中,可以通過代理模式來實現。下面看看一個簡單的例子,使用代理模式實現方法的攔截,如代碼清單1所示。
代碼清單1代理模式攔截方法的實現
#include<memory> #include<string> #include<iostream> using namespace std; class IHello { public: IHello() { } virtual ~IHello() { } virtualvoid Output(const string& str) { } }; class Hello : public IHello { public: void Output(const string& str) override { cout <<str<< endl; } }; class HelloProxy : public IHello { public: HelloProxy(IHello* p) : m_ptr(p) { } ~HelloProxy() { delete m_ptr; m_ptr = nullptr; } void Output(const string& str) final { cout <<"Before real Output"<< endl; m_ptr->Output(str); cout <<"After real Output"<< endl; } private: IHello* m_ptr; }; void TestProxy() { std::shared_ptr<IHello> hello = std::make_shared<HelloProxy>(newHello()); hello->Output("It is a test"); }
測試代碼將輸出:
Before real Output It is a test Before real Output
可以看到我們通過HelloProxy代理對象實現了對Output方法的攔截,這里Hello::Output就是核心邏輯,HelloProxy實際上就是一個切面,我們可以把一些非核心邏輯放到里面,比如在核心邏輯之前的一些校驗,在核心邏輯執行之后的一些日志等。
雖然通過代理模式可以實現AOP,但是這種實現還存在一些不足之處:
1.不夠靈活,不能自由組合多個切面。代理對象是一個切面,這個切面依賴真實的對象,如果有多個切面,要靈活地組合多個切面就變得很困難。這一點可以通過裝飾模式來改進,雖然可以解決問題但還是顯得“笨重”。
2.耦合性較強,每個切面必須從基類繼承,並實現基類的接口。
我們希望能有一個耦合性低,又能靈活組合各種切面的動態織入的AOP框架。
- 需要的技術
要實現靈活組合各種切面,一個比較好的方法是將切面作為模板的參數,這個參數是可變的,支持1到N(N>0)切面,先執行核心邏輯之前的切面邏輯,執行完之后再執行核心邏輯,然后再執行核心邏輯之后的切面邏輯。這里,我們可以通過可變參數模板來支持切面的組合。AOP實現的關鍵是動態織入,實現技術就是攔截目標方法,只要攔截了目標方法,我們就可以在目標方法執行前后做一些非核心邏輯,通過繼承方式來實現攔截,需要派生基類並實現基類接口,這使程序的耦合性增加了。為了降低耦合性,這里通過模板來做解耦,即每個切面對象需要提供Before(Args…)或After(Args…)方法,用來處理核心邏輯執行前后的非核心邏輯。
支持任意類型和數量的切面組合,我們通過可變模版參數實現即可,關於可變模板參數的用法和使用技巧,讀者可以參考我在《程序員》2015年2月刊上的文章《泛化之美--C++11可變模版參數的妙用》。
為了實現切面的充分解耦合,我們的切面不必通過繼承方式實現,而且也不必要求切面必須具備Before和After方法,只要具備任意一個方法即可,給使用者提供最大的便利性和靈活性。實現這個功能稍微有點復雜,復雜的地方在於切面可能具有某個方法也可能不具有某個方法,具有就調用,不具有也不會出錯。問題的本質上是需要檢查類型是否具有某個方法,在C++中是無法在運行期做到這個事情的,因為C++像不托管語言c#或java那樣具備反射功能,然而,我們可以在編譯期檢查類型是否具有某個方法。下面來看看是如何實現在編譯期檢查某個類型的方法是否存在的。我們先定義一個檢查方法是否存在的元函數(關於元函的概念數讀者可以參考我在《程序員》2015年3月刊上的文章《C++11模版元編程》)。
template<typename T> struct has_member_foo { private: template<typename U> static auto Check(int) -> decltype(std::declval<U>().foo(), std::true_type()); template<typename U> static std::false_type Check(...); public: enum{ value = std::is_same<decltype(Check<T>(0)), std::true_type>::value }; };
這個has_member_foo的作用就是檢查類型是否存在非靜態成員foo函數,具體的實現思路是這樣的:定義一個兩個重載函數Check,利用C++的SIFNA(Substitution failure is not an erro--替換失敗不算錯)特性,由於模板實例化的過程中會優先選擇匹配程度最高的重載函數,在模板實例化的過程中檢查類型是否存在foo函數,如果存在則返回std::true_type,否則返回std::false_type,最后檢查Check函數的返回類型即可知道類型是否存在foo函數。需要注意的是這里用到了C++11中auto+decltype實現的返回類型后置特性,用這個特性的主要目的是我們無法直接得到Check的返回類型,我們需要借助模板參數T才能得到Check的返回類型(關於返回類型后置這個特性的詳細用法和應用場景介紹,讀者可以參考《深入應用C++11:代碼優化與工程級應用》一書第一章的內容)。我們來看一下實現檢查很關鍵的一行代碼:
template<typename U> static auto Check(int) -> decltype(std::declval<U>().foo(), std::true_type());
我們用到了std::declval<U>().foo(),std::declval不會受到類型U是否存在共有構造函數的影響,也不會實例化類型,它僅僅是為了配合decltype來獲取函數foo的返回類型,如果獲取成功則表明存在foo函數,否則就會替換失敗,而選擇默認的Check函數。decltype在這里還有另外一個妙用,它可以通過逗號表達式連續推斷多個函數的返回類型,假如我們不僅僅是要推斷類型是否存在foo函數,我們還希望推斷類型是否存在foo1函數,這時我們僅僅需要在加一個逗號表達式就行了:
template<typename U> static auto Check(int) -> decltype(std::declval<U>().foo(),std::declval<U>().foo1(), std::true_type());
這樣我們就可以同時判斷某個類型是否具有foo和foo1函數了,decltype在這里體現了良好的擴展性。
雖然我們通過has_member_foo可以檢查出來類型是否存在foo函數,但是還存在一個問題,即重載函數的問題,假設我們有多個重載的foo函數,這個has_member_foo就不一定能檢查出來,因為decltype(std::declval<U>().foo())僅僅檢查的是不含參數的foo函數,因此這個has_member_foo還不夠完美,我們還需要改進它,讓它能對所有的重載函數有效。借助可變模板參數就可以輕松解決這個問題了:
template<typename T, typename... Args> struct has_member_foo { private: template<typename U> static auto Check(int) -> decltype(std::declval<U>().foo(std::declval<Args>()...), std::true_type()); template<typename U> static std::false_type Check(...); public: enum{ value = std::is_same<decltype(Check<T>(0)), std::true_type>::value }; };
改進之后的has_member_foo就可以對所有版本的重載函數進行檢查了,具體用法是這樣的:
cout << has_member_foo<MyStruct>::value << endl; //判斷是否存在無參的foo函數 cout << has_member_foo<MyStruct, int>::value << endl;//判斷是否存在含int參數的foo函數
再借助std::enable_if在編譯期根據has_member_foo::value可以做一個分支選擇就可以解決前面提出的問題:如果類型具有某個方法就調用,不具有也不會出錯。這個問題解決之后我們就可以實現一個AOP框架了
- 完整的實現
下面AOP完整的實現,我們對has_member_foo通過宏做了一個擴展,讓它方便對所有的非成員函數進行檢查,然后再借助std::enable_if和變參實現對方法的攔截。
代碼清單2 AOP的實現
#define HAS_MEMBER(member)\ template<typename T, typename... Args>struct has_member_##member\ {\ private:\ template<typename U> static auto Check(int) -> decltype(std::declval<U>().member(std::declval<Args>()...), std::true_type()); \ template<typename U> static std::false_type Check(...);\ public:\ enum{value = std::is_same<decltype(Check<T>(0)), std::true_type>::value};\ };\ HAS_MEMBER(Foo) HAS_MEMBER(Before) HAS_MEMBER(After) #include <NonCopyable.hpp> template<typename Func, typename... Args> struct Aspect : NonCopyable { Aspect(Func&& f) : m_func(std::forward<Func>(f)) { } template<typename T> typename std::enable_if<has_member_Before<T, Args...>::value&&has_member_After<T, Args...>::value>::type Invoke(Args&&... args, T&& aspect) { aspect.Before(std::forward<Args>(args)...);//核心邏輯之前的切面邏輯 m_func(std::forward<Args>(args)...);//核心邏輯 aspect.After(std::forward<Args>(args)...);//核心邏輯之后的切面邏輯 } template<typename T> typename std::enable_if<has_member_Before<T, Args...>::value&&!has_member_After<T, Args...>::value>::type Invoke(Args&&... args, T&& aspect) { aspect.Before(std::forward<Args>(args)...);//核心邏輯之前的切面邏輯 m_func(std::forward<Args>(args)...);//核心邏輯 } template<typename T> typename std::enable_if<!has_member_Before<T, Args...>::value&&has_member_After<T, Args...>::value>::type Invoke(Args&&... args, T&& aspect) { m_func(std::forward<Args>(args)...);//核心邏輯 aspect.After(std::forward<Args>(args)...);//核心邏輯之后的切面邏輯 } template<typename Head, typename... Tail> void Invoke(Args&&... args, Head&&headAspect, Tail&&... tailAspect) { headAspect.Before(std::forward<Args>(args)...); Invoke(std::forward<Args>(args)..., std::forward<Tail>(tailAspect)...); headAspect.After(std::forward<Args>(args)...); } private: Func m_func; //被織入的函數 }; template<typenameT> using identity_t = T; //AOP的輔助函數,簡化調用 template<typename... AP, typename... Args, typename Func> void Invoke(Func&&f, Args&&... args) { Aspect<Func, Args...> asp(std::forward<Func>(f)); asp.Invoke(std::forward<Args>(args)..., identity_t<AP>()...); }
在上面的代碼中,“template<typename T> using identity_t = T;”是為了讓vs2013能正確識別出模板參數,因為各個編譯器對變參的實現是有差異的。在GCC下,“msp.Invoke(std::forward<Args>(args)..., AP()...);”是可以編譯通過的,但是在vs2013下就不能編譯通過,通過identity_t就能讓vs2013正確識別出模板參數類型。這里將Aspect從NonCopyable派生,使Aspect不可復制。關於NonCopyable的實現請讀者參考8.2節的內容。上面的代碼用到完美轉發和可變參數模板,關於它們的用法,讀者可以參考第2章和第3章內容。
實現思路很簡單,將需要動態織入的函數保存起來,然后根據參數化的切面來執行Before(Args…)處理核心邏輯之前的一些非核心邏輯,在核心邏輯執行完之后,再執行After(Args…)來處理核心邏輯之后的一些非核心邏輯。上面的代碼中的has_member_Before和has_member_After這兩個traits是為了讓使用者用起來更靈活,使用者可以自由的選擇Before和After,可以僅僅有Before或After,也可以二者都有。
需要注意的是切面中的約束,因為通過模板參數化切面,要求切面必須有Before或After函數,這兩個函數的入參必須和核心邏輯的函數入參保持一致,如果切面函數和核心邏輯函數入參不一致,則會報編譯錯誤。從另外一個角度來說,也可以通過這個約束在編譯期就檢查到某個切面是否正確。
下面看一個簡單的測試AOP的例子,這個例子中我們將記錄目標函數的執行時間並輸出日志,其中計時和日志都放到切面中。在執行函數之前輸出日志,在執行完成之后也輸出日志,並對執行的函數進行計時,如代碼清單3所示。
代碼清單3帶日志和計時切面的AOP
struct TimeElapsedAspect { void Before(int i) { m_lastTime = m_t.elapsed(); } void After(int i) { cout <<"time elapsed: "<< m_t.elapsed() - m_lastTime << endl; } private: double m_lastTime; Timer m_t; }; struct LoggingAspect { void Before(int i) { std::cout <<"entering"<< std::endl; } void After(int i) { std::cout <<"leaving"<< std::endl; } }; void foo(int a) { cout <<"real HT function: "<<a<< endl; } int main() { Invoke<LoggingAspect, TimeElapsedAspect>(&foo, 1); //織入方法 cout <<"-----------------------"<< endl; Invoke<TimeElapsedAspect, LoggingAspect>(&foo, 1); return 0; }
測試結果如圖3所示。
圖3 AOP切面組合的測試結果
從測試結果中看到,我們可以任意組合切面,非常靈活,也不要求切面必須從某個基類派生,只要求切面具有Before或After函數即可(這兩個函數的入參要和攔截的目標函數的入參相同)。
總結
輕量級的AOP可以方便的實現對核心函數的織入,還支持切面的組合,但也存在不足之處,比如不能像Java的AOP框架一樣支持通過配置文件去配置切面,仍然需要手動對核心函數配置切面,如果需要通過配置文件去配置切面可以考慮使用AspectC++。
備注:本文節選自《深入應用C++11:代碼優化與工程級應用》,這本書是為了和廣大讀者分享學習和應用C++11的經驗和樂趣,告訴讀者C++11的新特性是如何綜合運用於實際開發中的,具有實踐的指導作用。特此感謝機械工業出版社授權。
本文是我發表於《程序員》7月刊,轉載請注明出處。