C + + common components - lightweight AOP Library

C + + common components - lightweight AOP Library

1.AOP introduction

AOP (aspect oriented programming) can solve some problems in object-oriented programming, which is a useful supplement to OOP. Inheritance in object-oriented programming is a top-down relationship, which is not suitable to define the horizontal relationship from left to right. If many unrelated objects in the inheritance system have some public behaviors, these public behaviors may be scattered in different components and different objects, and it is not suitable to extract these public behaviors by inheritance. There is also a case where AOP is used to improve the maintainability of programs. AOP "crosscuts" the non core logic of programs and separates the non core logic from the core logic, so that we can focus on the core logic,

In the figure above, each business process has the function of log and permission verification, and it is possible to add new functions. In fact, we only care about the core logic, and some other additional logic, such as log and permission, do not need to be concerned. At this time, we can "crosscut" the non core logic such as log and permission, so as to keep the core logic as simple and clear as possible and easy to maintain. Another benefit of such "crosscutting" is that these common non core logic is extracted into multiple aspects, so that they can be reused by other components or objects, eliminating duplicate code.

AOP divides the software system into two parts: core concerns and crosscutting concerns. The main process of business processing is the core concern, and the little related part is the crosscutting concern. One of the characteristics of crosscutting concerns is that they often occur in many places of core concerns, and they are basically similar everywhere, such as permission authentication, log, transaction processing. The role of AOP is to separate various concerns in the system and separate the core concerns from the crosscutting concerns.

2. Simple implementation of AOP - through agent mode

The technology of AOP is divided into static weaving and dynamic weaving. Static weaving generally uses special syntax to create "aspects", so that the compiler can weave "aspects" of the code during compilation, AspectC + + is used in this way. This method also needs special compilation tools and syntax, which is more complex to use. The AOP framework introduced in section 10.3 is just a lightweight AOP framework based on dynamic weaving. Dynamic weaving generally adopts the way of dynamic agent, intercepts the method in the running period, and weaves the cut into the method dynamically, which can be realized through the agent mode. Let's take a simple example. Use the proxy mode to implement the interception of methods, as shown in the following code:

#include<memory>
#include<string>
#include<iostream>
using namespace std;
class IHello{
public:
    IHello(){}
    virtual ~IHello(){}
    virtual void 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>(new Hello());
    hello->Output("It is a test");
}

int main() {
    TestProxy();
    return 0;
}

The output results are as follows:

Before real Output
It is a test
Before real Output

The Output method is intercepted through the HelloProxy proxy object. Here, Hello:: Output is the core logic. HelloProxy is actually a facet. We can put some non core logic into it, such as some checks before the core logic and some logs after the core logic is executed.

Although AOP can be implemented by proxy mode, there are still some shortcomings in this implementation:

·Not flexible enough to combine multiple sections freely. The proxy object is a facet, which depends on the real object. If there are multiple facets, it is difficult to combine them flexibly. This point can be improved through decoration mode, which can solve the problem but still appears "bulky".

·Coupling is strong, each facet must inherit from the base class and implement the interface of the base class.

We hope to have an AOP framework with low coupling and flexible combination of various sections.

3. Implementation of lightweight AOP framework - through template

To realize flexible combination of various aspects, a better method is to take the aspect as the parameter of the template. This parameter is variable and supports 1 to N (N > 0) aspects. First, the aspect logic Before the core logic is executed, then the core logic After the core logic is executed. Here, the combination of facets can be supported through variable parameter templates. The key of AOP implementation is dynamic weaving. The implementation technology is to intercept the target method. As long as the target method is intercepted, we can do some non core logic Before and After the target method is executed. To achieve interception through inheritance, we need to derive the base class and implement the base class interface, which increases the coupling of the program. In order to reduce the coupling, the template is used to make the constraint, that is, each tangent object must have Before (Args...) Or After (Args...) Method to deal with the non core logic Before and After the core logic is executed. The following describes how to realize the AOP framework that can flexibly combine various aspects of dynamic weaving, as shown in the following code:

_Pragma("once")

#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(Before)
HAS_MEMBER(After)

class NonCopyable{
public:
    NonCopyable(const NonCopyable&) = delete; // deleted
    NonCopyable& operator = (const NonCopyable&) = delete; // deleted
    NonCopyable() = default;   // available
};

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)...);//Section logic before core logic
        m_func(std::forward<Args>(args)...);//Core logic
        aspect.After(std::forward<Args>(args)...);//Section logic after core logic
    }

    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)...);//Section logic before core logic
        m_func(std::forward<Args>(args)...);//Core logic
    }

    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)...);//Core logic
        aspect.After(std::forward<Args>(args)...);//Section logic after core logic
    }

    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; //Woven function
};

//Auxiliary function of AOP, simplifying call
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)..., AP()...);
}
  • main.cpp
#include <iostream>
#include <functional>
#include "Aspect.hpp"

using namespace std;

struct AA{
    void Before(int i){
        cout <<"Before from AA"<<i<< endl;
    }
    void After(int i){
        cout <<"After from AA"<<i<< endl;
    }
};
struct BB{
    void Before(int i){
        cout <<"Before from BB"<<i<< endl;
    }
    void After(int i){
        cout <<"After from BB"<<i<< endl;
    }
};
struct CC{
    void Before(){
        cout <<"Before from CC"<< endl;
    }
    void After(){
        cout <<"After from CC"<< endl;
    }
};
struct DD{
    void Before(){
        cout <<"Before from DD"<< endl;
    }
    void After(){
        cout <<"After from DD"<< endl;
    }
};
void GT(){
    cout <<"real GT function"<< endl;
}
void HT(int a){
    cout <<"real HT function: "<<a<< endl;
}
void TestAop1(){
    // Weaving ordinary function
    std::function<void(int)> f = std::bind(&HT, std::placeholders::_1);
    Invoke<AA, BB>(std::function<void(int)>(std::bind(&HT, std::placeholders::_1)), 1);
    // Combined two sections AA BB
    Invoke<AA, BB>(f, 1);

    // Weaving ordinary function
    Invoke<CC, DD>(&GT);
    Invoke<AA, BB>(&HT, 1);

    // Weaving lambda expression
    Invoke<AA, BB>([](int i){}, 1);
    Invoke<CC, DD>([]{});
}

/*------------------------------------*/
struct TimeElapsedAspect{
	void Before(int i){
        cout <<"time start"<< endl;
	}
	void After(int i){
		cout <<"time end"<< endl;
	}
};
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;
}

void TestAop2() {
    Invoke<LoggingAspect, TimeElapsedAspect>(&foo, 1); //Weaving method
    cout <<"-----------------------"<< endl;
    Invoke<TimeElapsedAspect, LoggingAspect>(&foo, 1);
}

int main() {
    TestAop1();
    TestAop2();
    return 0;
}

The implementation idea is very simple. Save the functions that need to be woven dynamically, and then execute before (Args...) according to the parameterized section Some non core logic before processing core logic. After the core logic is executed, execute after (Args...) To deal with some non core logic after the core logic. In the above code, the two traits of has? Member? Before and has? Member? After are used to make users more flexible. Users are free to choose between before and after. They can have either before or after or both.

It is necessary to pay attention to the constraints in the facet, because through the parameterization of the template, the facet must have a Before or After function, and the input parameters of these two functions must be consistent with those of the core logic. If the input parameters of the facet function and the core logic function are inconsistent, a compilation error will be reported. From another point of view, you can also check whether a certain tangent is correct at compile time through this constraint.

It can be seen from the test results that we can combine facets arbitrarily, which is very flexible and does not require that the facet must derive from a certain base class, only that the facet has a Before or After function (the input parameters of these two functions are the same as those of the intercepted target function).

147 original articles published, praised 15, visited 150000+
Private letter follow

Tags: Programming github Lambda

Posted on Wed, 11 Mar 2020 07:49:49 -0400 by SpectralDesign.Net