PHP三層結構(下) PHP實現AOP第1/2頁

讓我們把注意力集中到中間服務層上來。中間服務層代碼比較簡單,只是調用數據訪問層代碼將留言保存到數據庫。如代碼1所示: 
複製代碼代碼如下:

// 代碼 1 
// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 

}; 

在看到留言板的演示之後,公司的產品部和市場部或許會提出各種各樣的想法和需求。比如他們希望在添加留言之前判斷用戶的權限!只有註冊用戶才能留言!我們需要修改代碼,如代碼2所示: 
複製代碼代碼如下:

// 代碼 2, 增加登錄驗證 
// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
if (!($userLogin)) { 
// 提示用戶登錄 

// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 

}; 

市場部又希望在添加留言之前,對留言內容進行檢查,如果留言中含有髒話就不保存。我們繼續修改代碼,如代碼3所示: 
複製代碼代碼如下:

// 代碼 3, 增加髒話過濾 
// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
if (!($userLogin)) { 
// 提示用戶登錄 

if (stristr($newLWord, "SB")) { 
// 含有髒話, 提示留言發送失敗 

// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 

}; 

產品部也提出了新需求,他們希望加入積分機制。具體來講就是在用戶每次留言成功以後給用戶+5分。我們繼續修改代碼,如代碼4所示: 
複製代碼代碼如下:

// 代碼 4, 加入留言積分機制 
// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
if (!($userLogin)) { 
// 提示用戶登錄 

if (stristr($newLWord, "SB")) { 
// 含有髒話, 提示留言發送失敗 

// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 
// 給用戶加分 
$score = getUserScore($userName); 
$score = $score + 5; 
saveUserScore($userName, $score); 

}; 

沒過多久,產品部又對需求進行細化,他們希望用戶積分每積累夠1000分以後,就給用戶升級。我們繼續修改代碼,如代碼5所示: 
複製代碼代碼如下:

// 代碼 5, 加入用戶升級規則 
// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
if (!($userLogin)) { 
// 提示用戶登錄 

if (stristr($newLWord, "fuck")) { 
// 含有髒話, 提示留言發送失敗 


// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 
// 給用戶加分 
$score = getUserScore($userName); 
$score = $score + 5; 
saveUserScore($userName, $score); 
// 給用戶升級 
if (($score % 1000) == 0) { 
$level = getUserLevel($userName); 
$level = $level + 1; 
saveUserLevel($userName, $level); 


}; 

隨着需求的增多,我們需要不斷的修改中間服務層代碼。但是你應該不難發現,需求越多中間服務層代碼也就越多越龐大!最後會導致即便我們使用三層結構的開發模式,也還是沒有有效的降低工程難度!另外就是應需求的變化而修改中間服務代碼以後,需要重新測試所有代碼,而不是有效的測試新增代碼…… 

其實讓我們仔細分析一下這個留言板代碼,我先要提出一個主業務邏輯和次業務邏輯的概念。無論怎樣,把留言內容存入到數據庫,這是業務邏輯的主幹!這個就是主業務邏輯!這部分沒有隨着需求的增加而修改。至於在存入數據庫之前要進行權限校驗,要進行內容檢查,存入數據庫之後要給用戶加分,然後給用戶升級,這些都是前序工作和掃尾工作,都是次業務邏輯!主業務邏輯幾乎是一成不變的,次業務邏輯變化卻非常頻繁。爲了提高代碼的可讀性和可維護性,我們可以考慮把這些次業務邏輯放到別的地方,儘量不要讓它們干擾主業務邏輯。主業務邏輯專心幹自己該乾的事情好了,至於別的任何事情,主業務邏輯一概都不聞不問!那麼我們的代碼就可以寫成這樣,如代碼6所示: 
複製代碼代碼如下:

// 代碼 6, 將主業務邏輯和次業務邏輯分開 
// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
// 添加留言前 
beforeAppend($newLWord); 
// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 
// 添加留言後 
behindAppend($newLWord); 

}; 

我們可以把權限判斷代碼和留言內容文本過濾代碼統統塞進beforeAppend函數,把用戶積分代碼塞進behindAppend函數,這樣就把次業務邏輯從主業務邏輯代碼中清理掉了。主業務邏輯知道有個“序曲”函數beforeAppend,有個“尾聲”函數behindAppend,但是在序曲和尾聲函數中具體都做了什麼事情,主業務邏輯並不知道,也不需要知道!當然實際編碼工作並不那麼簡單,我們還要兼顧產品部和市場部更多的需求變化,所以最好能實現一種插件方式來應對這種變化,但是僅僅依靠兩個函數beforeAppend和behindAppend是達不到這個目的~ 

想要實現插件方式,可以建立接口!使用接口的好處是可以將定義和實現隔離,另外就是實現多態。我們建立一個留言擴展接口ILWordExtension,該接口有兩個函數beforeAppend和behindAppend。權限校驗、內容檢查、加分這些功能可以看作是實現ILWordExtension接口的三個實現類,主業務邏輯就依次遍歷這三個實現類,來完成次業務邏輯。如圖1所示: 
CheckPowerExtension擴展類用作用戶權限校驗,CheckContentExtension擴展類用作留言內容檢查,AddScoreExtension擴展類用作給用戶加分和升級。示意代碼如代碼7所示: 

(圖1),加入擴展接口
複製代碼代碼如下:

// 代碼 7,加入擴展接口 
// 擴展接口 
interface ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord); 
// 添加留言後 
public function behindAppend($newLWord); 
}; 

// 檢查權限 
class CheckPowerExtension implements ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord) { 
// 在這裏判斷用戶權限 


// 添加留言後 
public function behindAppend($newLWord) { 

}; 

// 檢查留言文本 
class CheckContentExtension implements ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord) { 
if (stristr($newLWord, "SB")) { 
throw new Exception(); 



// 添加留言後 
public function behindAppend($newLWord) { 

}; 

// 用戶積分 
class AddScoreExtension implements ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord) { 


// 添加留言後 
public function behindAppend($newLWord) { 
// 在這裏給用戶積分 

}; 

// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
// 添加留言前 
$this->beforeAppend($newLWord); 

// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 

// 添加留言後 
$this->behindAppend($newLWord); 


// 添加留言前 
private function beforeAppend($newLWord) { 
// 獲取擴展數組 
$extArray = $this->getExtArray(); 

foreach ($extArray as $ext) { 
// 遍歷每一個擴展, 並調用其 beforeAppend 函數 
$ext->beforeAppend($newLWord); 



// 添加留言後 
private function behindAppend($newLWord) { 
// 獲取擴展數組 
$extArray = $this->getExtArray(); 

foreach ($extArray as $ext) { 
// 遍歷每一個擴展, 並調用其 behindAppend 函數 
$ext->behindAppend($newLWord); 



// 獲取擴展數組, 
// 該函數的返回值實際上是 ILWordExtension 接口數組 
private function getExtArray() { 
return array( 
// 檢查權限 
new CheckPowerExtension(), 
// 檢查內容 
new CheckContentExtension(), 
// 加分 
new AddScoreExtension(), 
); 

}; 

如果還有新需求,,我們只要再添加ILWordExtension 實現類並且把它註冊到getExtArray函數裏即可。程序從此有了條理,並且算是具備了可擴展性。 

不過先不要忙着高興,有個問題就在這個可擴展性裏。當新的需求被提出之後,我們可以再添加 ILWordExtension 實現類,這個的確正確。但是將這個新類註冊到getExtArray函數裏,等於說還是要修改主業務邏輯代碼。能不能不修改呢?每次有新的需求變化還是要告知主業務邏輯,這樣終歸不太好。最理想的情況是新的擴展代碼加入系統之後,主業務邏輯代碼不用修改,因爲主業務邏輯根本不知道有新擴展這回事!爲此我們還需要優化一下設計方案,如圖2所示:

(圖2),加入擴展家族類 

對於調用擴展的主程序(也就是中間服務類LWordServiceCore),只讓它知道有ILWordExtension(擴展)這件事就可以了,它不需要知道還有CheckPowerExtension(檢查權限擴展)、CheckContentExtension(檢查內容擴展)和AddScoreExtension(加分擴展)這三個類。對這三個類的調用過程被移動到LWordExtensionFamily (擴展家族類)裏去了。 

LWordExtensionFamily其實就是一個能存放多個ILWordExtension接口實例的容器類,從圖2中可以看出這個容器類不僅僅是實現了ILWordExtension接口,而且還聚合多個ILWordExtension接口的實例,所以它很特殊!對於LWordServiceCore類,這個類只知道ILWordExtension接口,但並不知道這個接口存在三個實現類。恰好LWordExtensionFamily類就實現了ILWordExtension接口,這很好的符合了中間服務類的要求,並且這個擴展家族類知道ILWordExtension存在三個實現類,並會一一調用它們, LWordExtensionFamily代碼大概如代碼8所示:

複製代碼代碼如下:

// 代碼 8, 擴展家族 
// 擴展家族 
class LWordExtensionFamily implements ILWordExtension { 
// 擴展數組 
private $_extensionArray = array(); 
// 添加擴展 
public function addExtension(ILWordExtension $extension) { 
$this->_extensionArray []= $extension; 

// 添加留言前 
public function beforeAppend($newLWord) { 
foreach ($this->_extensionArray as $extension) { 
$extension->beforeAppend($newLWord); 


// 添加留言後 
public function behindAppend($newLWord) { 
foreach ($this->_extensionArray as $extension) { 
$extension->behindAppend($newLWord); 




通過代碼8不難看出LWordExtensionFamily類雖然也實現了ILWordExtension接口,但是它並不做任何實質的操作,而是通過循環語句將調用過程一一傳遞下去。爲了平滑實現擴展到插入的方式,所以最好創建一個工廠類MyExtensionFactory。如代碼9所示: 
複製代碼代碼如下:

// 代碼 9 
// 自定義擴展工廠 
class MyExtensionFactory { 
// 創建留言擴展 
public static function createLWordExtension() { 
$lwef = new LWordExtensionFamily(); 
// 添加擴展 
$lwef->addExtension(new CheckPowerExtension()); 
$lwef->addExtension(new CheckContentExtension()); 
$lwef->addExtension(new AddScoreExtension()); 
return $lwef; 
     // 注意這裏返回的是擴展家族類對象, 
     // 擴展家族 LWordExtensionFamily 恰好也實現了接口 ILWordExtension, 
     // 所以這是符合業務邏輯的要求. 
     // 從此, 業務邏輯可以不關心具體的擴展對象, 只要知道擴展家族即可 



使用擴展工廠類的好處就是可以隨意的添加和移除擴展實例,這就很好的實現了可插入式編程。對於LWordServiceCore類只知道一個ILWordExtension接口,對於LWordExtensionFamily知道需要一一調用每個擴展,但是具體會有多少個擴展是通過MyExtensionFactory給出的。各負其責結構也很清晰。如果我們做一個假設,MyExtensionFactory類的createLWordExtension函數不是通過new關鍵字這樣的硬編碼方式來添加擴展列表,而是通過更巧妙的讀取配置文件的方式來得到擴展列表,那麼是不是更方便更靈活呢?不過這個就不再本文中討論了。 

中間服務層通過工廠類取得一個ILWordExtension接口的具體實例,然後調用其beforeAppend和behindAppend方法。當然中間服務並不知道工廠類返回的其實是一個含有多個ILWordExtension實例的容器(因爲這個容器也實現了ILWordExtension接口),所以中間服務也就不知道擴展是被一一調用的。完整代碼如代碼10所示: 
複製代碼代碼如下:

// 代碼 10, 完整代碼 
// 擴展接口 
interface ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord); 
// 添加留言後 
public function behindAppend($newLWord); 
}; 
// 檢查權限 
class CheckPowerExtension implements ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord) { 
// 在這裏判斷用戶權限 

// 添加留言後 
public function behindAppend($newLWord) { 

}; 
// 檢查留言文本 
class CheckContentExtension implements ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord) { 
if (stristr($newLWord, "fuck")) 
throw new Exception(); 

// 添加留言後 
public function behindAppend($newLWord) { 

}; 
// 用戶積分 
class AddScoreExtension implements ILWordExtension { 
// 添加留言前 
public function beforeAppend($newLWord) { 

// 添加留言後 
public function behindAppend($newLWord) { 
// 在這裏給用戶積分 

}; 
// 擴展家族 
class LWordExtensionFamily implements ILWordExtension { 
// 擴展數組 
private $_extensionArray = array(); 
// 添加擴展 
public function addExtension(ILWordExtension $extension) { 
$this->_extensionArray []= $extension; 

// 添加留言前 
public function beforeAppend($newLWord) { 
foreach ($this->_extensionArray as $extension) { 
$extension->beforeAppend($newLWord); 


// 添加留言後 
public function behindAppend($newLWord) { 
foreach ($this->_extensionArray as $extension) { 
$extension->behindAppend($newLWord); 



// 自定義擴展工廠 
class MyExtensionFactory { 
// 創建留言擴展 
public static function createLWordExtension() { 
$lwef = new LWordExtensionFamily(); 
// 添加擴展 
$lwef->addExtension(new CheckPowerExtension()); 
$lwef->addExtension(new CheckLWordExtension()); 
$lwef->addExtension(new AddScoreExtension()); 
return $lwef; 


// 中間服務層 
class LWordServiceCore implements ILWordService { 
// 添加留言 
public function append($newLWord) { 
// 獲取擴展 
$ext = MyExtensionFactory::createLWordExtension(); 
$ext->beforeAppend($newLWord); 
// 調用數據訪問層 
$dbTask = new LWordDBTask(); 
$dbTask->append($newLWord); 
$ext->behindAppend($newLWord); 

}; 

從代碼10中可以看出雖然CheckPowerExtension、CheckContentExtension、AddScoreExtension以及LWordExtensionFamily都實現了ILWordExtension接口,但是它們的beforeAppend和behindAppend函數過程卻完全不同!特別是LWordExtensionFamily擴展家族類,它並沒有實質的業務邏輯處理過程,而是將調用依次傳遞給每一個擴展。beforeAppend和behindAppend函數在具體類中的不同實現,這是面向對象程序設計中的很典型的特性:多態! 
將次業務邏輯分散到各個擴展中,這種做法已經非常近似AOP(Aspect OrientedProgramming,面向切面編程)的編程方式。權限校驗、內容檢查和積分可以看作是不同的切面,這些切面和主業務邏輯交叉在一起,但又不會影響到主業務邏……這樣做的好處就是擴展代碼不會干擾主業務邏輯,我們也可以針對某一個擴展進行編碼和單元測試,然後通過MyExtensionFactory工廠類把擴展插入到業務流程中。完整的執行過程如圖3所示: 

(圖3),執行流程 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章