Preface
I keep seeing AOP mentioned in various places from time to time. I've checked wiki more than once, but each time I was intimidated by all those unfamiliar terms. This time I finally made up my mind to try AOP
In the end, I discovered that AOP is similar to design patterns, but different from Strategy Pattern, Template Method Pattern, Decorator Pattern. AOP's close relative is Proxy Pattern, which can also separate logic. The core is also interception and detail hiding
P.S. Bringing out so many terms wasn't intentional. Because during the process of understanding AOP, I did compare and think about these several patterns. Then, regarding design patterns, hmm, how to say it, are they useful? Not useful? Well
I. Glossary
-
Aspect
In AOP, represents "where to do and what to do as a collection"
Modularization of cross-cutting concerns, such as the logging component mentioned above. Can be considered a combination of advice, introduction, and pointcut. For example, logging, caching, transaction management
-
Join point
In AOP, represents "where to do"
Represents extension points where cross-cutting concerns need to be inserted into the program. Join points could be class initialization, method execution, method invocation, field access, or exception handling, etc. Represents a point during execution, such as method execution or property access
-
Advice
In AOP, represents "what to do"
Or called the behavior executed at a join point. Advice provides a means to extend existing behavior at join points selected by pointcuts in AOP. Includes before advice, after advice, around advice. Represents the action of an aspect at a specific join point
-
Pointcut
In AOP, represents "the collection of where to do"
A pattern that selects a set of related join points, i.e., can be considered a collection of join points. Spring supports perl5 regular expressions and AspectJ pointcut patterns, Spring uses AspectJ syntax by default. A regular expression used to match join points. Every advice has a related pointcut expression, executed at any join point that matches it. For example, the execution of a particular named method
-
Introduction
In AOP, represents "what to do (add what)"
Also called inter-type declaration, adds extra new fields or methods to existing classes
-
Weaving
Linking aspects with other application types or objects to create advised objects
Weaving is a process, the process of applying aspects to target objects to create AOP proxy objects. Weaving can be performed at compile time, class load time, or runtime
-
Target Object
In AOP, represents "to whom to do"
The object that needs cross-cutting concerns woven into it, i.e., the object selected by pointcuts, the object that needs to be advised, thus can also be called "advised object"
-
AOP Proxy
An object created by the AOP framework using the proxy pattern, thereby achieving insertion of advice at join points (i.e., applying aspects). That is, applying aspects to target objects through proxies
There are many terms, simple classification:
Abstract concepts: Aspect, Introduction, Weaving, Target Object, AOP Proxy
Concrete concepts: Join point, Advice, Pointcut
Relationship: Pointcut is a collection formed by join points, both represent target positions where logic needs to be inserted, Advice represents the specific action to be inserted
When using AOP, you need to focus on join points and pointcuts. The former is "where you want to insert logic", the latter is "in which area you want to insert logic (area is composed of positions)", then weave in and register advice, add before/after logic
Advice Types
-
Before advice
Advice executed before a certain join point, but this advice cannot prevent execution before the join point (unless it throws an exception)
-
After returning advice
Advice executed after a certain join point completes normally: for example, a method returns normally without throwing any exceptions
-
After throwing advice
Advice executed when a method exits by throwing an exception
-
After (finally) advice
Advice executed when a certain join point exits (whether returning normally or exiting by exception)
-
Around Advice
Advice around a join point, such as a method invocation. This is the most powerful type of advice. Around advice can complete custom behavior before and after method invocation. It is also responsible for choosing whether to continue executing the join point, or directly return their own return value, or throw an exception to end execution
Note the difference between AfterThrowing and AroundAdvice. After business logic throws an exception, the former will be triggered, but you can't get the exception object, you only know that the concerned method threw an exception, which is not very meaningful. The latter completely wraps the business logic, so you can catch exception information (let's not discuss async callback exceptions for now). Other types of Advice are literal, easy to understand
II. Function
AOP can encapsulate logic or responsibilities that are unrelated to business but are commonly called by business modules (such as transaction processing, log management, permission control, etc.), making it easier to reduce duplicate code in the system, reduce coupling between modules, and benefit future operability and maintainability
Experience an example, object-oriented code easily grows like this:
/**
* OO style
*/
class OOUser {
public function add($fields) {
// check auth
if (!$this->isGranted('ADD_USER')) {
throw new Exception("Access Denied");
}
// log
$this->log('creating user');
// create user
try {
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
$this->insertUser($user);
} catch(Exception $e) {
$this->log('user create error: ' + $e);
// handleError($e);
}
// log
$this->log('user created');
}
}
Caching, logging, exception handling, permission checking and other logic are scattered throughout the project code (not just the User class), unable to be separated. There are many problems:
-
Cannot be reused
-
Difficult to understand the original function of the class, logic is messy
-
Very easy to make mistakes, if you forget to write this boilerplate code
-
Violates DRY principle, each logic block is interspersed with these familiar codes
Especially when maintaining old projects, seeing blocks of familiar code is very uncomfortable. Want to change but can't extract it, or spent a lot of effort and only alleviated some surface symptoms (for example, considering other encapsulation methods, streamlined a few lines of business code)
AOP specifically solves this problem. It can horizontally weave into the interior of objects for internal surgery, strip away core business logic, and we can focus on the truly useful few lines of code
III. PHP AOP Example
Found a relatively good PHP AOP framework: Go! AOP
P.S. Because better AOP frameworks involve reflection and annotations, with my current PHP ability it's insufficient to complete, so I gave up the idea of manually implementing AOP mechanism
Consider the previous OO code, classify the logic blocks:
public function add() {
//=before advice
// check auth
if (!isGranted('ADD_USER')) {
throw new Exception("Access Denied");
}
//=before advice
// log
log('creating user');
//=business logic
// create user
try {
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
insertUser($user);
} catch(Exception $e) {
//=after throwing advice
log('user create error: ' + $e);
// handleError($e);
}
//=after advice
// log
log('user created');
}
Found that business logic is only a few lines, but it's deeply wrapped by other not very critical code. When updating and maintaining, you will face the problem of "modifying a small piece in a large patch of code", locate the key part and carefully modify it, then it's still very easy to make mistakes (especially exception handling)
Then extract business logic, the new User class looks like this:
/**
* AOP style
*/
class AOPUser {
public function add($fields) {
//=business logic
// create user
$user = array('id' => '003');
$user['name'] = 'user';
//...
// save
$this->insertUser($user);
// throw error
$this->badMethod();
throw new Exception("A Stange Error");
}
/**
* Insert user to database
*
* @Loggable
*
* @param Array $info Info
*/
public function insertUser($user) {
//...
$this->log('user inserted');
}
//...other dependent methods that cannot be shared
}
We separated the business logic. Shareable dependent methods (for example, log(), isGranted(), etc.) were extracted to become shared lib. Other dependent methods that cannot be shared still exist as class members. At this time, the User class has relatively single responsibility, discordant codes are all kicked out, logic is very clear
Next, need to assemble (similar to decorator pattern, but implementation differs significantly). AOP will help us dynamically assemble. We only need to declare associations, tell AOP where to install what (which is the meaning of the term "aspect")
/**
* User aspect
*/
class UserAspect implements Aspect {
/**
* Pointcut for add method
*
* @Pointcut("execution(public App\App\AOPUser->add(*))")
*/
protected function UserAdd() {}
// Weave when executing $aopuser->add()
/**
* Check auth before add user
*
* @param MethodInvocation $invocation Invocation
* @Before("$this->UserAdd")
*/
protected function checkAuthBeforeAdd(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// check auth
if (!$isGranted('ADD_USER', $user->caller)) {
throw new Exception("Access Denied");
}
}
/**
* Handle Error after throwing
*
* @param MethodInvocation $invocation Invocation
* @AfterThrowing("$this->UserAdd")
*/
protected function handleErrorAfterThrowing(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// =after throwing advice
$log('user create error, handle error here');
// handleError();
//!!! avoid reporting error
set_exception_handler(function($e) {
echo "!!!Global Exception Handler: " . $e->getMessage();
});
}
/**
* Log after add user
*
* @param MethodInvocation $invocation Invocation
* @After("$this->UserAdd")
*/
protected function logAfterAdd(MethodInvocation $invocation) {
/** @var $user \App\App\AOPUser */
$user = $invocation->getThis();
// log
$log('user created');
}
//...other Advice
}
Declare the connection between Advice and target object through annotations, tell AOP where to insert what logic, eliminate logic adhesion
Note the handleErrorAfterThrowing() method. To avoid global exception errors, we used set_exception_handler(). We do this because AfterThrowing advice is triggered when an exception occurs at the pointcut, but we can't get the exception object, nor can we swallow it. So we use global exception interception to swallow this exception
If you need to precisely handle exceptions in a certain process, you should use Around advice, completely wrap the target process, then try-catch, as follows:
/**
* Around advice to catch exception
* @param MethodInvocation $invocation Invocation
* @Around("execution(public App\App\AOPUser->badMethod(*))")
*/
protected function aroundBadMethod(MethodInvocation $invocation) {
try {
$invocation->proceed();
} catch (Exception $e) {
echo '!!!Around Advice Error Handler: ' . $e->getMessage() . "<br>\n";
}
}
P.S. Exceptions in async callbacks are not discussed here. PHP generally doesn't care about this situation. For JS, annotation-based AOP implementation is also not considered (should use higher-order functions, binding, etc. for logic injection). Talk about it later
P.S. GO! AOP is quite powerful, also provides ways to weave into system methods and utility functions, including parameter interception, property access interception, etc.
IV. Demo
Online Demo: http://www.ayqy.net/temp/aop/src/
Source code address: https://github.com/ayqy/aop
Note: Requires PHP5.5+, because GO! AOP framework uses class as identifier internally
V. Summary
AOP is a supplement to OOP, horizontally weaving into objects and injecting logic to ensure single responsibility of classes
More aptly, AOP is a design pattern. There are also more intense views:
AOP is a patch for OOP. Vertical OOP establishes object system, inheritance encapsulation polymorphism; horizontal AOP weaves in, vertical and horizontal combined, invincible in the world...
Not wrong, just there's controversy about the degree of intrusion. For example, if you want AOP to weave into the entire OO system, the degree of intrusion is bound to be very large (considering inheritance). I personally prefer solutions with less intrusion, flexible but inconvenient
How to say it, learning AOP counts as gaining a design thought, similar to design principles (review):
-
Encapsulate change (extract parts prone to change to reduce their impact on other parts)
-
Favor composition over inheritance (composition is more flexible than inheritance)
-
Program to interfaces, not implementations (using interfaces avoids direct dependency on concrete classes)
-
Strive for loosely coupled design between interacting objects (looser coupling means more flexibility)
-
Classes should be open for extension, closed for modification (open-close principle)
-
Depend on abstractions, not concrete classes (reduce direct dependency on concrete classes)
-
Only talk to friends (Law of Demeter)
-
Don't call me, I will call you back (Major principle of Android development)
-
Classes should have only one reason to change (Single Responsibility Principle)
-
Horizontal logic injection (AOP)
Having one more option when considering problems, that's all. When building large systems, AOP should be a necessary built-in feature. But in terms of application scenarios, AOP is not a master key. But AOP's thought (horizontal logic injection) applies to any scenario
Reference Materials
-
Weaving aspects in PHP with the help of Go! AOP library: GO! AOP author's PPT, suitable for beginners (perceptual cognition)
-
Introduction to aspect-oriented programming: Explanation of AOP terminology
-
My understanding of AOP: The latter half is quite good
No comments yet. Be the first to share your thoughts.