面向方面編程(AOP)對於PHP來說是一個新的概念。現在PHP對於 AOP 並沒有官方支持,但有很多擴展和庫實現了這個特性。本課中,我們將使用 Go! PHP library 來學習 PHP 如何進行 AOP 開發,或者在需要的時候,可以回來看一眼。
Aspect-Oriented programming is like a new gadget for geeks.
面向方面編程的思想在二十世紀90年代中期,於施樂帕洛阿爾托研究中心(PARC)成型。同很多有趣的新技術一樣,由於缺少明確的定義,起初 AOP 備受爭議。因此相關小組決定將未完成的想法公之於眾,以便接受廣大社區的反饋。關鍵問題在於“關注點分離(Separation of Concerns)”的概念。AOP 是一種可以分離關注的可行系方案。
AOP 於90年代末趨於成熟,標識為施樂 AspectJ 的發布,IBM 緊隨其後,於2001年發布了 Hyper/J。現在,AOP是一種對於常用編程語言來說都是一種成熟的技術。
AOP 的核心就是“方面”,但在我們定義「方面『aspect』」之前,我們需要先討論兩個術語;「切點『 point-cut』 」和「通知『advise』」。切點代表我們代碼中的一個時間點,特指運行我們代碼的某個時間。在切點運行代碼被稱為通知,結合一個活多個切點及通知的即為方面。
通常,每個類都會有一個核心的行為或關注點,但有時,類可能存在次要的行為。例如,類可能會調用一個日志記錄器或是通知一個觀察員。因為類中的這些功能是次要的,其行為通常都是相同的。這種行為被稱為“交叉關注點”;使用 AOP 可以避免。
Chris Peters 已經討論過在PHP中實現 AOP 的Flow 框架。 Lithium 框架也提供了對AOP的實現。
另一個框架采用了不同的方法,創建了一個 C/C++ 編寫的PHP擴展,在PHP解釋器的層級上宣示著它的魔力。名為AOP PHP Extension,我會在後續文章中討論它。
但正如我之前所言,本文將檢閱Go! AOP-PHP 庫。
Go! 庫並未擴展;它完全由PHP編寫,並為PHP5.4或更高版本使用。作為一個純PHP庫,它部署簡易,即使是在不允許編譯安裝你自己的PHP擴展的受限及共享主機環境,也可以輕易安裝。
Composer 是安裝 PHP 包的首選方法。如果你沒有使用過 Composer,你可以在Go! GitHub repository下載。
首先,將下面幾行加入你的 composer.json 文件。
1 2 3 4 5{
"require"
: {
"lisachenko/go-aop-php"
:
"*"
}
}
之後,使用 Composer 安裝 go-aop-php。在終端中運行下面命令:
1 2$
cd
/your/project/folder
$ php composer.phar update lisachenko
/go-aop-php
Composer 將會在之後數秒中內安裝引用的包以及需求。如果成功,你將看到類似下面的輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13Loading composer repositories with package information
Updating dependencies
- Installing doctrine
/common
(2.3.0)
Downloading: 100%
- Installing andrewsville
/php-token-reflection
(1.3.1)
Downloading: 100%
- Installing lisachenko
/go-aop-php
(0.1.1)
Downloading: 100%
Writing lock
file
Generating autoload files
在安裝完成後,你可以在你的代碼目錄中發現名為 vendor 的文件夾。Go! 庫及其需求就安裝在這。
1 2 3 4 5 6 7 8 9 10 11$
ls
-l .
/vendor
total 20
drwxr-xr-x 3 csaba csaba 4096 Feb 2 12:16 andrewsville
-rw-r--r-- 1 csaba csaba 182 Feb 2 12:18 autoload.php
drwxr-xr-x 2 csaba csaba 4096 Feb 2 12:16 composer
drwxr-xr-x 3 csaba csaba 4096 Feb 2 12:16 doctrine
drwxr-xr-x 3 csaba csaba 4096 Feb 2 12:16 lisachenko
$
ls
-l .
/vendor/lisachenko/
total 4
drwxr-xr-x 5 csaba csaba 4096 Feb 2 12:16 go-aop-php
我們需要創建一個調用,介於路由/應用程序的入口點。自動裝彈機的然後自動包括類。開始吧!引用作為一個切面內核。
1 2 3 4 5 6 7 8 9 10 11 12 13 14use
Go\Core\AspectKernel;
use
Go\Core\AspectContainer;
class
ApplicationAspectKernel
extends
AspectKernel {
protected
function
configureAop(AspectContainer
$container
) {
}
protected
function
getApplicationLoaderPath() {
}
}
現在,AOP是一種在通用編程語言中相當成熟的技術。
例如,我創建了一個目錄,調用應用程序,然後添加一個類文件: ApplicationAspectKernel.php 。
我們開始切面擴展!AcpectKernel 類提供了基礎的方法用於完切面內核的工作。有兩個方法,我們必須知道:configureAop()用於注冊頁面特征,和 getApplicationLoaderPath() 返回自動加載程序的全路徑。
現在,一個簡單的建立一個空的 autoload.php 文件在你的程序目錄。和改變 getApplicationLoaderPath() 方法。如下:
1 2 3 4 5 6 7 8 9 10// [...]
class
ApplicationAspectKernel
extends
AspectKernel {
// [...]
protected
function
getApplicationLoaderPath() {
return
__DIR__ . DIRECTORY_SEPARATOR .
'autoload.php'
;
}
}
別擔心 autoload.php 就是這樣。我們將會填寫被省略的片段。
當我們第一次安裝 Go語言!和達到這一點我的過程中,我覺得需要運行一些代碼。所以開始構建一個小應用程序。
我們的「方面」為一個簡單的日志記錄器,但在繼續我們應用的主要部分之前,有些代碼需要看一下。
我們的小應用是一個電子經紀人,能夠購買和出售股票。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19class
Broker {
private
$name
;
private
$id
;
function
__construct(
$name
,
$id
) {
$this
->name =
$name
;
$this
->id =
$id
;
}
function
buy(
$symbol
,
$volume
,
$price
) {
return
$volume
*
$price
;
}
function
sell(
$symbol
,
$volume
,
$price
) {
return
$volume
*
$price
;
}
}
這些代碼非常簡單,Broker 類擁有兩個私有字段,儲存經紀人的名稱和 ID。
這個類同時提供了兩個方法,buy() 和 sell(),分別用於收購和出售股票。每個方法接受三個參數:股票標識、股票數量、每股價格。sell() 方法出售股票,並計算總收益。相應的,buy()方法購買股票並計算總支出。
通過PHPUnit 測試程序,我們可以很容易的考驗我們經紀人。在應用目錄內創建一個子目錄,名為 Test,並在其中添加 BrokerTest.php 文件。並添加下面的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15require_once
'../Broker.php'
;
class
BrokerTest
extends
PHPUnit_Framework_TestCase {
function
testBrokerCanBuyShares() {
$broker
=
new
Broker(
'John'
,
'1'
);
$this
->assertEquals(500,
$broker
->buy(
'GOOGL'
, 100, 5));
}
function
testBrokerCanSellShares() {
$broker
=
new
Broker(
'John'
,
'1'
);
$this
->assertEquals(500,
$broker
->sell(
'YAHOO'
, 50, 10));
}
}
這個檢驗程序檢查經紀人方法的返回值。我們可以運行這個檢查程序檢驗我們的代碼,至少是不是語法正確。
讓我們創建一個自動加載器,在應用需要的時候加載類。這是一個簡單的加載器,基於PSR-0 autoloader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19ini_set
(
'display_errors'
, true);
spl_autoload_register(
function
(
$originalClassName
) {
$className
= ltrim(
$originalClassName
,
'\\'
);
$fileName
=
''
;
$namespace
=
''
;
if
(
$lastNsPos
=
strripos
(
$className
,
'\\'
)) {
$namespace
=
substr
(
$className
, 0,
$lastNsPos
);
$className
=
substr
(
$className
,
$lastNsPos
+ 1);
$fileName
=
str_replace
(
'\\'
, DIRECTORY_SEPARATOR,
$namespace
) . DIRECTORY_SEPARATOR;
}
$fileName
.=
str_replace
(
'_'
, DIRECTORY_SEPARATOR,
$className
) .
'.php'
;
$resolvedFileName
= stream_resolve_include_path(
$fileName
);
if
(
$resolvedFileName
) {
require_once
$resolvedFileName
;
}
return
(bool)
$resolvedFileName
;
});
這就是我們 autoload.php 文件中的全部內容。現在,變更 BrokerTest.php, 改引用Broker.php 為引用自動加載器 。
1 2 3 4 5require_once
'../autoload.php'
;
class
BrokerTest
extends
PHPUnit_Framework_TestCase {
// [...]
}
運行 BrokerTest,驗證代碼運行情況。
我們最後的一件事是配置Go!.為此,我們需要連接所有的組件讓們能和諧工作。首先,創建一個php文件AspectKernelLoader.php,其代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14include
__DIR__ .
'/../vendor/lisachenko/go-aop-php/src/Go/Core/AspectKernel.php'
;
include
'ApplicationAspectKernel.php'
;
ApplicationAspectKernel::getInstance()->init(
array
(
'autoload'
=>
array
(
'Go'
=>
realpath
(__DIR__ .
'/../vendor/lisachenko/go-aop-php/src/'
),
'TokenReflection'
=>
realpath
(__DIR__ .
'/../vendor/andrewsville/php-token-reflection/'
),
'Doctrine\\Common'
=>
realpath
(__DIR__ .
'/../vendor/doctrine/common/lib/'
)
),
'appDir'
=> __DIR__ .
'/../Application'
,
'cacheDir'
=> null,
'includePaths'
=>
array
(),
'debug'
=> true
));
我們需要連接所有的組件讓們能和諧工作!
這個文件位於前端控制器和自動加載器之間。他使用AOP框架初始化並在需要時調用autoload.php
第一行,我明確地載入AspectKernel.php和ApplicationAspectKernel.php,因為,要記住,在這個點我們還沒有自動加載器。
接下來的代碼段,我們調用ApplicationAspectKernel對象init()方法,並且給他傳遞了一個數列參數:
為了最後實現各個不同部分的連接,找出你工程中autoload.php自動加載所有的引用並且用AspectKernelLoader.php替換他們。在我們簡單的例子中,僅僅test文件需要修改:
1 2 3 4 5 6 7require_once
'../AspectKernelLoader.php'
;
class
BrokerTest
extends
PHPUnit_Framework_TestCase {
// [...]
}
對大一點的工程,你會發現使用bootstrap.php作為單元測試但是非常有用;用require_once()做為autoload.php,或者我們的AspectKernelLoader.php應該在那載入。
創建BrokerAspect.php文件,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19use
Go\Aop\Aspect;
use
Go\Aop\Intercept\FieldAccess;
use
Go\Aop\Intercept\MethodInvocation;
use
Go\Lang\Annotation\After;
use
Go\Lang\Annotation\Before;
use
Go\Lang\Annotation\Around;
use
Go\Lang\Annotation\Pointcut;
use
Go\Lang\Annotation\DeclareParents;
class
BrokerAspect
implements
Aspect {
/**
* @param MethodInvocation $invocation Invocation
* @Before("execution(public Broker->*(*))") // This is our PointCut
*/
public
function
beforeMethodExecution(MethodInvocation
$invocation
) {
echo
"Entering method "
.
$invocation
->getMethod()->getName() .
"()\n"
;
}
}
我們在程序開始指定一些有對AOP框架有用的語句。接著,我們創建了自己的方面類叫BrokerAspect,用它實現Aspect。接著,我們指定了我們aspect的匹配邏輯。
1* @Before(
"execution(public Broker->*(*))"
)
[operation - execution/access]([method/attribute type -
public
/
protected
] [
class
]->[method/attribute]([params])
請注意匹配機制不可否認有點笨拙。你在規則的每一部分僅可以使用一個星號‘*‘。例如public Broker->匹配一個叫做Broker的類;public Bro*->匹配以Bro開頭的任何類;public *ker->匹配任何ker結尾的類。
public *rok*->將匹配不到任何東西;你不能在同一個匹配中使用超過一個的星號。
緊接著匹配程序的函數會在有時間發生時調用。在本例中的方法將會在每一個Broker公共方法調用之前執行。其參數$invocation(類型為MethodInvocation)子自動傳遞到我們的方法的。這個對象提供了多種方式獲取調用方法的信息。在第一個例子中,我們使用他獲取了方法的名字,並且輸出。
僅僅定義一個切面是不夠的;我們需要把它注冊到AOP架構裡。否則,它不會生效。編輯ApplicationAspectKernel.php同時在容器上的configureAop()方法裡調用registerAspect():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16use
Go\Core\AspectKernel;
use
Go\Core\AspectContainer;
class
ApplicationAspectKernel
extends
AspectKernel
{
protected
function
getApplicationLoaderPath()
{
return
__DIR__ . DIRECTORY_SEPARATOR .
'autoload.php'
;
}
protected
function
configureAop(AspectContainer
$container
)
{
$container
->registerAspect(
new
BrokerAspect());
}
}
運行測試和檢查輸出。你會看到類似下面的東西:
1 2 3 4 5 6 7 8 9PHPUnit 3.6.11 by Sebastian Bergmann.
.Entering method __construct()
Entering method buy()
.Entering method __construct()
Entering method sell()
Time: 0 seconds, Memory: 5.50Mb
OK (2 tests, 2 assertions)
就這樣我們已設法讓代碼無論什麼時候發生在broker上時都會執行。
讓我們加入另外的方法到BrokerAspect。
1 2 3 4 5 6 7 8 9 10 11 12 13 14// [...]
class
BrokerAspect
implements
Aspect {
// [...]
/**
* @param MethodInvocation $invocation Invocation
* @After("execution(public Broker->*(*))")
*/
public
function
afterMethodExecution(MethodInvocation
$invocation
) {
echo
"Finished executing method "
.
$invocation
->getMethod()->getName() .
"()\n"
;
echo
"with parameters: "
. implode(
', '
,
$invocation
->getArguments()) .
".\n\n"
;
}
}
這個方法在一個公共方法執行後運行(注意@After匹配器)。染污我們加入另外一行來輸出用來調用方法的參數。我們的測試現在輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21PHPUnit 3.6.11 by Sebastian Bergmann.
.Entering method __construct()
Finished executing method __construct()
with parameters: John, 1.
Entering method buy()
Finished executing method buy()
with parameters: GOOGL, 100, 5.
.Entering method __construct()
Finished executing method __construct()
with parameters: John, 1.
Entering method sell()
Finished executing method sell()
with parameters: YAHOO, 50, 10.
Time: 0 seconds, Memory: 5.50Mb
OK (2 tests, 2 assertions)
目前為止,我們學習了在一個方法執行的之前和之後,怎樣運行額外的代碼。當這個漂亮的實現後,如果我們無法看到方法返回了什麼的話,它還不是非常有用。我們給aspect增加另一個方法,修改現有的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32//[...]
class
BrokerAspect
implements
Aspect {
/**
* @param MethodInvocation $invocation Invocation
* @Before("execution(public Broker->*(*))")
*/
public
function
beforeMethodExecution(MethodInvocation
$invocation
) {
echo
"Entering method "
.
$invocation
->getMethod()->getName() .
"()\n"
;
echo
"with parameters: "
. implode(
', '
,
$invocation
->getArguments()) .
".\n"
;
}
/**
* @param MethodInvocation $invocation Invocation
* @After("execution(public Broker->*(*))")
*/
public
function
afterMethodExecution(MethodInvocation
$invocation
) {
echo
"Finished executing method "
.
$invocation
->getMethod()->getName() .
"()\n\n"
;
}
/**
* @param MethodInvocation $invocation Invocation
* @Around("execution(public Broker->*(*))")
*/
public
function
aroundMethodExecution(MethodInvocation
$invocation
) {
$returned
=
$invocation
->proceed();
echo
"method returned: "
.
$returned
.
"\n"
;
return
$returned
;
}
}
僅僅定義一個aspect是不夠的;我們需要將它注冊到AOP基礎設施。
這個新的代碼把參數信息移動到@Before方法。我們也增加了另一個特殊的@Around匹配器方法。這很整潔,因為原始的匹配方法調用被包裹於aroundMethodExecution()函數之內,有效的限制了原始的調用。在advise裡,我們要調用$invocation->proceed(),以便執行原始的調用。如果你不這麼做,原始的調用將不會發生。
這種包裝也允許我們操作返回值。advise返回的就是原始調用返回的。在我們的案例中,我們沒有修改任何東西,輸出應該看起來像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25PHPUnit 3.6.11 by Sebastian Bergmann.
.Entering method __construct()
with parameters: John, 1.
method returned:
Finished executing method __construct()
Entering method buy()
with parameters: GOOGL, 100, 5.
method returned: 500
Finished executing method buy()
.Entering method __construct()
with parameters: John, 1.
method returned:
Finished executing method __construct()
Entering method sell()
with parameters: YAHOO, 50, 10.
method returned: 500
Finished executing method sell()
Time: 0 seconds, Memory: 5.75Mb
OK (2 tests, 2 assertions)
我們增加一點變化,賦以一個具體的broker一個discount。返回到測試類,寫如下的測試:
1 2 3 4 5 6 7 8 9 10 11 12require_once
'../AspectKernelLoader.php'
;
class
BrokerTest
extends
PHPUnit_Framework_TestCase {
// [...]
function
testBrokerWithId2WillHaveADiscountOnBuyingShares() {
$broker
=
new
Broker(
'Finch'
,
'2'
);
$this
->assertEquals(80,
$broker
->buy(
'MS'
, 10, 10));
}
}
這會失敗:
1 2 3 4 5 6 7 8 9 10 11 12Time: 0 seconds, Memory: 6.00Mb
There was 1 failure:
1) BrokerTest::testBrokerWithId2WillHaveADiscountOnBuyingShares
Failed asserting that 100 matches expected 80.
/home/csaba/Personal/Programming/NetTuts/Aspect
Oriented Programming
in
PHP
/Source/Application/Test/BrokerTest
.php:19
/usr/bin/phpunit
:46
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
下一步,我們需要修改broker以便提供它的ID。只要像下面所示實現agetId()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17class
Broker {
private
$name
;
private
$id
;
function
__construct(
$name
,
$id
) {
$this
->name =
$name
;
$this
->id =
$id
;
}
function
getId() {
return
$this
->id;
}
// [...]
}
現在,修改aspect以調整具有ID值為2的broker的購買價格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18// [...]
class
BrokerAspect
implements
Aspect {
// [...]
/**
* @param MethodInvocation $invocation Invocation
* @Around("execution(public Broker->buy(*))")
*/
public
function
aroundMethodExecution(MethodInvocation
$invocation
) {
$returned
=
$invocation
->proceed();
$broker
=
$invocation
->getThis();
if
(
$broker
->getId() == 2)
return
$returned
* 0.80;
return
$returned
;
}
}
無需增加新的方法,只要修改aroundMethodExecution()函數。現在它正好匹配方法,稱作‘buy‘,並觸發了$invocation->getThis()。這有效的返回了原始的Broker對象,以便我們可以執行它的代碼。於是我們做到了!我們向broker要它的ID,如果ID等於2的話就提供一個折扣。測試現在通過了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37PHPUnit 3.6.11 by Sebastian Bergmann.
.Entering method __construct()
with parameters: John, 1.
Finished executing method __construct()
Entering method buy()
with parameters: GOOGL, 100, 5.
Entering method getId()
with parameters: .
Finished executing method getId()
Finished executing method buy()
.Entering method __construct()
with parameters: John, 1.
Finished executing method __construct()
Entering method sell()
with parameters: YAHOO, 50, 10.
Finished executing method sell()
.Entering method __construct()
with parameters: Finch, 2.
Finished executing method __construct()
Entering method buy()
with parameters: MS, 10, 10.
Entering method getId()
with parameters: .
Finished executing method getId()
Finished executing method buy()
Time: 0 seconds, Memory: 5.75Mb
OK (3 tests, 3 assertions)
我們現在可以在一個方法的開始和執行之後、繞過時,執行附加程序。但當方法拋出異常時又如何呢?
添加一個測試方法來購買大量微軟的股票:
1 2 3 4function
testBuyTooMuch() {
$broker
=
new
Broker(
'Finch'
,
'2'
);
$broker
->buy(
'MS'
, 10000, 8);
}
現在,創建一個異常類。我們需要它是因為內建的異常類不能被 Go!AOP 或 PHPUnit 捕捉.
1 2 3 4 5 6 7class
SpentTooMuchException
extends
Exception {
public
function
__construct(
$message
) {
parent::__construct(
$message
);
}
}
修改經紀人類,對大值拋出異常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14class
Broker {
// [...]
function
buy(
$symbol
,
$volume
,
$price
) {
$value
=
$volume
*
$price
;
if
(
$value
> 1000)
throw
new
SpentTooMuchException(sprintf(
'You are not allowed to spend that much (%s)'
,
$value
));
return
$value
;
}
// [...]
}
運行測試,確保它們產生失敗消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15Time: 0 seconds, Memory: 6.00Mb
There was 1 error:
1) BrokerTest::testBuyTooMuch
Exception: You are not allowed to spend that much (80000)
/home/csaba/Personal/Programming/NetTuts/Aspect
Oriented Programming
in
PHP
/Source/Application/Broker
.php:20
//
[...]
/home/csaba/Personal/Programming/NetTuts/Aspect
Oriented Programming
in
PHP
/Source/Application/Broker
.php:47
/home/csaba/Personal/Programming/NetTuts/Aspect
Oriented Programming
in
PHP
/Source/Application/Test/BrokerTest
.php:24
/usr/bin/phpunit
:46
FAILURES!
Tests: 4, Assertions: 3, Errors: 1.
現在,期待異常(在測試中),確保它們通過:
1 2 3 4 5 6 7 8 9 10 11 12 13class
BrokerTest
extends
PHPUnit_Framework_TestCase {
// [...]
/**
* @expectedException SpentTooMuchException
*/
function
testBuyTooMuch() {
$broker
=
new
Broker(
'Finch'
,
'2'
);
$broker
->buy(
'MS'
, 10000, 8);
}
}
在我們的“方面”中建立一個新方法來匹配@AfterThrowing,別忘記指定 Use Go\Lang\Annotation\AfterThrowing;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16// [...]
Use Go\Lang\Annotation\AfterThrowing;
class
BrokerAspect
implements
Aspect {
// [...]
/**
* @param MethodInvocation $invocation Invocation
* @AfterThrowing("execution(public Broker->buy(*))")
*/
public
function
afterExceptionMethodExecution(MethodInvocation
$invocation
) {
echo
'An exception has happened'
;
}
}
@AfterThrowing匹配器抑制拋出的異常,並允許你去采取自己的行動。在我們的代碼中,我們簡單的顯示一個信息,但你可以做任何你的應用程序需要的事情。
這就是為什麼我建議你小心使用“方面”。
面向方面編程就像給怪人們的新玩意兒;您可以立即看到其巨大的潛力。方面允許我們在我們的系統的不同部分引入額外的代碼,而無需修改原始代碼。當你需要實現一些通過緊耦合引用和方法調用會污染你的方法和類的模塊時,這會非常有用。
然而,這種靈活性,是有代價的:陰暗朦胧。有沒有辦法告訴如果一方面表的方法只是在尋找方法或類。例如,在我們的Broker類中執行方法時沒有跡象表明發生任何事情。這就是為什麼我建議你小心使用“方面”的原因。
我們使用“方面”來給一個特定的經紀人提供折扣是誤用的一個例子。不要在一個真實的項目中這樣做。經紀人的折扣與經紀人相關;所以,在Broker類中保持這個邏輯。“方面”應該只執行不直接關系到對象主要行為的任務。
樂在其中吧!
英文原文:Aspect-Oriented Programming in PHP with Go!