1. 程式人生 > >PHP三層結構(下)——PHP實現AOP

PHP三層結構(下)——PHP實現AOP

讓我們把注意力集中到中間服務層上來。中間服務層程式碼比較簡單,只是呼叫資料訪問層程式碼將留言儲存到資料庫。如程式碼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所示:

 

(圖1),加入擴充套件介面

 

CheckPowerExtension擴充套件類用作使用者許可權校驗,CheckContentExtension擴充套件類用作留言內容檢查,AddScoreExtension擴充套件類用作給使用者加分和升級。示意程式碼如程式碼7所示:

 

複製程式碼

 

// 程式碼 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),執行流程