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