PHP三層結構

來源:互聯網
上載者:User
PHP三層結構(上)??簡單三層結構

我們以一個簡單的留言板代碼為例,先來看一個最簡單的三層結構代碼,如代碼1所示:

// 代碼 1
// 外觀層類
class LWordHomePage {
// 添加留言
public function append($newLWord) {
// 調用中間服務層
$serv = new LWordServiceCore();
$serv->append($newLWord);
}
};

// 中間服務層
class LWordServiceCore {
// 添加留言
public function append($newLWord) {
// 調用資料訪問層
$dbTask = new LWordDBTask();
$dbTask->append($newLWord);
}
};

// 資料訪問層
class LWordDBTask {
// 添加留言
public function append($newLWord) {
// 資料層代碼(省略)
}
};

執行時序圖,1所示:

(圖1),簡單三層結構時序圖

從代碼和時序圖中可以直觀看出三層結構的調用順序。但實際開發中這種簡單三層結構並不能滿足需求!我們先從外觀層與中間服務層這兩層的代碼開始討論。在外觀層LWordHomePage類中直接使用new關鍵字建立並調用中間服務類LWordServiceCore屬於一種硬式編碼方式。在實際項目開發過程中,外觀層和中間服務層可能會由不同的人員來開發,即一個功能模組由多個人共同完成。而外觀層LWordHomePage類的開發進度是不可能等到LWordServiceCore類完全開發完成之後才開始(換句話來說就是,外觀層不可能等到中間服務層完全開發完成之後才開始),這樣的協作效率非常低!為了使項目可以由多人同時開發,所以我們要切割代碼設計。我們可以組織一個臨時的中間服務類來滿足外觀層的開發進度。等到中間服務層完全開發完成之後,替換一下就可以了……2所示:

(圖2),外觀層在不同的服務間切換

顯然,要實現這樣的需求,在外觀層中直接使用new關鍵字建立和調用LWordServiceCore類是非常不靈活的!這很難做到靈活的隨意的切換!!我們可以建立TempService類充當中間服務層的臨時實現。我們還需要分析一下TempService和LWordServiceCore這兩個類,它們都具備相同的append函數用於添加留言,只不過一個是臨時的而另外一個是真實的。既然TempService和LWordServiceCore這兩個類都有公用函數,那麼就應該可以有一個公用的父類。考慮到對這個公用的上級類沒有別的成員和屬性,所以將這個公用的上級類定義為介面,即ILWordService!UML類圖3所示:


(圖3)定義和實現ILWordService介面

在LWordHomePage類中並不直接建立TempService或者LWordServiceCore類對象,建立過程會交給一個工廠類MyServiceFactory(簡單原廠模式)。這樣一來,外觀層中的LWordHomePage類只需要知道ILWordService介面即可,外觀層代碼並不關心具體的中間服務代碼是什麼,這樣就極好的實現了外觀層與具體服務代碼的分離。

這相當於什麼呢?就好比兩個硬體工程師,一個是製造電腦顯卡的,一個是製造電腦主板的。製造顯卡的工程師可以把顯卡插到到一塊測試電路中,來測試顯卡是否可以正常工作?同樣,製造主板的工程師也可以把主板插入到另一塊測試電路中,來測試主板是否可以正常工作?等到這兩位工程師都各自完成工作之後,將他倆的工作成果對接在一起就可以了。這是一種並行開發方式,幾乎可以省掉一半的時間。從軟體工程的角度來講,我們在設計介面代碼的時候也應該考慮是否需要支援多人同時開發,從而提高生產效率。

依照UML類圖(3所示),我們修改PHP代碼如代碼2所示:

// 代碼2, 通過工廠建立留言服務並調用
// 外觀層類
class LWordHomePage {
// 添加留言
public function append($newLWord) {
// 調用中間服務
$serv = MyServiceFactory::create();
// 注意此時是操作 ILWordService 介面, 而非 LWordService 類
$serv->append($newLWord);
}
};

// 留言服務介面
interface ILWordService {
public function append($newLWord);
};

// 服務工廠類
class MyServiceFactory {
// 建立留言服務
public static function create() {
if (1) {
// 返回中間服務層
return new LWordServiceCore();
} else {
// 返回臨時實現
return new TempService();
}
}
}

// 臨時服務類
class TempService implements ILWordService {
// 添加留言
public function append($newLWord) {
// 臨時代碼(省略)
}
};

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

// 資料訪問層
class LWordDBTask {
// 添加留言
public function append($newLWord) {
// 資料層代碼(省略)
}
};

時序圖4所示:

(圖4)通過工廠類建立留言服務

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

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.