_我們能夠有意識地推斷我們想要在哪一條想法的溪流中遨游,然而此后與那些想法的接觸會潛在地塑造我們的習慣和信仰。 -- 《智慧社會》_
##1.27.1 寫在前面的話
此篇章有點長,但我認為是值得一讀的。因為這里我將逐步講述如何在已有的基礎上演變擴展出更高層次的代碼結構和系統架構,而不致于因目前頻繁的需求變更而導致代碼凌亂不堪。更為重要的是,你將能從中發現,如何在一個框架中持續演變,最終體驗浮現式設計的樂趣。如果你的項目亦能如此,我相信你會找到編程如同搭建積木般輕便明了的感覺。
##1.27.2 更富表現力的Model層
接口不盡相同,主要區別在于領域業務數據的處理。而數據的來源則更為廣泛,可能是來自數據庫,可能來自第三方平臺接口,可能存放于內存。所以,PhalApi這里的Model層,則是 **廣義上的數據源層** ,用于獲取原始的業務數據,而不管來自何方,何種存儲媒介。這也是為什么我們沒有將Model層打造成活動紀錄或者數據映射器的原因。當然,如果你確實需要,也可以自行調整。
如果數據來源于數據庫,我們則需要考慮到數據庫服務器的感受,保證不會有過載的請求而導致它罷工。對此,我們可以結合緩存來進行性能優化。
如,一般地:
```javascript
// 版本1:簡單的獲取
$model = new Model_User();
$rs = $model->getByUserId($userId);
```
這種是沒有緩存的情況,當發現有性能問題并且可以通過緩存來解決時,我們可以在調用時簡單引入緩存:
```javascript
// 版本2:使用單點緩存/多級緩存 (應該移至Model層中)
$key = 'userbaseinfo_' . $userId;
$rs = DI()->cache->get($key);
if ($rs === NULL) {
$rs = $model->getByUserId($userId);
DI()->cache->set($key, $rs, 600);
}
```
但不建議在領域Domain層中引入緩存,因為會導致混淆和不便進行測試。更好是將緩存的處理移至Model,保持數據獲取的透明性:
```javascript
class Model_User extends PhalApi_Model_NotORM {
public function getByUserIdWithCache($userId) {
$key = 'userbaseinfo_' . $userId;
$rs = DI()->cache->get($key);
if ($rs === NULL) {
$rs = $this->getByUserId($userId);
DI()->cache->set($key, $rs, 600);
}
return $rs;
}
```
對應地,外部的調用調整成:
```javascript
// 版本2:使用單點緩存/多級緩存 (應該移至Model層中)
$model = new Model_User();
$rs = $model->getByUserIdWithCache($userId);
```
至此,Model層對于上層如Domain來說,負責獲取源數據,而不管此數據來自于數據庫,還是遠程接口,抑或是緩存包裝下的數據。這正是我們使用數組在Model層和Domain層通訊的原因,因為數組更加通用,不需要額外添加實體。
##1.27.3 重量級數據獲取的應對方案
縱使更富表現力的Model很好地封裝了源數據的獲取,但是仍然會遇到一些尷尬的問題。特別地,當我們大量地進行緩存讀取判斷時,會出現很多重復的代碼,這樣既不雅觀也難以管理,甚至會出現一些簡單的人為編寫錯誤而導致的BUG。另外,當我們需要進行預覽、調試或測試時,我們是不希望看到緩存的,即我們能夠手工指定是否需要緩存。
這里再稍微簡單回顧總結一下我們現在的問題:我們希望通過緩存策略來優化Model層的源數據獲取,特別當源數據獲取的成本非常大時。但我們又希望我們可以輕易控制何時需要緩存,何時不需要,并且希望原有的代碼能在OCP的原則下不需要修改,但又能很好地傳遞源數據獲取的復雜參數。歸納一下,則可分為三點:緩存的控制、源數據的獲取、復雜參數的傳遞。
###(1)緩存的控制
不管是單點緩存,還是多級緩存,都希望使用原有已經注冊的cache組件服務。所以,應該使用委托。委托的另一個好處在于使用外部依賴注入可以獲得更好的測試性。
###(2)源數據的獲取
源數據的獲取,作為源數據獲取的主要過程和主要實現,需要進行緩存的控制(可細分為:是否允許讀緩存、和是否允許寫緩存)、 獲取緩存的key值和有效時間,以及最終原始數據的獲取。明顯,這里應該使用模板方法,然后提供鉤子函數給具體子類。
這里,我們提供了Model代理抽象類PhalApi_ModelProxy。
之所以使用代理模式,是因為實際上并不一定會真正調用到最終源數據的獲取,因為往往源數據的獲取成本非常高,故而我們希望通過緩存來攔截數據的獲取。
由于Model代理被上層的Domain領域層調用,但又依賴于下層Model層獲得原始數據,所以處于Domain和Model之間。為了保持良好的項目代碼層級,如果需要創建PhalApi_ModelProxy子類,建議新建一個ModelProxy目錄。
如對用戶基本信息的獲取,我們添加了一個代理:
```javascript
class ModelProxy_UserBaseInfo extends PhalApi_ModelProxy {
protected function doGetData($query) {
$model = new Model_User();
return $model->getByUserId($query->id);
}
protected function getKey($query) {
return 'userbaseinfo_' . $query->id;
}
protected function getExpire($query) {
return 600;
}
}
```
其中,doGetData($query)方法由具體子類實現,委托給Model_User的實例進行源數據獲取。另外,實現鉤子函數以返回緩存唯一key,和緩存的有效時間。
這里只是作為簡單的示例,更好的建議是應該將緩存的時間納入配置中管理,如 **配置四個緩存級別:低(5 min)、中(10 min)、高(30 min)、超(1 h)** ,然后根據不同的業務數據使用不同的緩存級別。這樣,即便于團隊交流,也便于緩存時間的統一調整。
###(3)復雜參數的傳遞
敏銳的讀者會發現,上面有一個$query查詢對象,這就是我們即將談到的復雜參數的傳遞。
$query是查詢對象PhalApi_ModelQuery的實例。我們強烈建議此類實例應當被作為 **值對象** 對待。雖然我們出于便利將此類對象設計成了結構化的使用。但你可以輕松通過new PhalApi_ModelQuery($query->toArray())來拷貝一個新的查詢對象。
此查詢對象,目前包括了四個成員變量:是否讀緩存、 是否寫緩存、主鍵id、時間戳。
很多時候,這四個基本的變量是滿足不了各項目的實際需求的,因此你可以定義你的查詢子類, 以支持豐富的數據獲取。如調用優酷平臺接口獲取用戶最近上傳發布的視頻時,需要用戶昵稱、獲取的數量、排序種類等。
###(4)最終的調用
在完成了上面的工作后,讓我們看下最終呈現的效果:
```javascript
// 版本3:緩存 + 代理
$query = new PhalApi_ModelQuery();
$query->id = $userId;
$modelProxy = new ModelProxy_UserBaseInfo();
$rs = $modelProxy->getData($query);
```
在領域層中,我們切換到了Model代理獲取數據,而不再是原來的Model直接獲取。其中新增的是代理具體類 ModelProxy_UserBaseInfo,和可選的查詢類。
###(5)UML靜態圖
至此,我們很好地在源數據的獲取基礎上,統一結合緩存策略。你會發現: **緩存節點可變、具體的源數據可變、復雜的查詢亦可變** 。

將此圖簡化一下,可得到:

這樣的設計是合理的,因為緩存節點我們希望能在項目內共享,而不管是哪塊的業務數據;對于具體的源數據獲取明顯也是不盡相同,所以也需要各自實現,同時對于同一類業務數據(如用戶基本信息)則使用一樣的緩存有效時間和指定格式的緩存key(通常結合不同的id組成唯一key);最后在前面的緩存共享和同類數據的基礎上,還需要支持不同數據的具體獲取,因此需要查詢對象。也就是說,你可以在不同的層級不同的范疇內進行自由的控制和定制。
如果退回到最初的版本,我們可以對比發現,Model Proxy就是Domain和Model間的橋梁,即:中間層。因為每次直接通過Model獲取源數據的成本較大,我們可以通過Model Proxy模型代理來緩存獲取的數據來減輕服務器的壓力。

##1.27.4 細粒度和可測試性
這無疑是細粒度的劃分,但對于支撐復雜的領域業務卻發揮著重要的作用。一來是如此清楚明了,二來則是帶來了可測試性。
正如前面提及到的,我們在預覽、調試、單元測試或者后臺計劃任務時,不希望有緩存的干擾。在細粒度劃分的基礎上,可輕松用以下方法實現而不必擔心會破壞代碼的簡潔性。
###(1)取消緩存的方法1: 外部注入模擬緩存
在構造Model代理時,默認情況下使用了DI()->cache作為緩存,當需要進行單元測試時,我們可以兩種途徑在外部注入模擬的緩存而達到測試的目的:替換全局的DI()->cache,或單次構造注入。對于計劃任務則可以在統一的后臺任務啟動文件將DI()->cache設置成空對象。
###(2)取消緩存的方法2: 查詢中的緩存控制
在項目層次,我們可以統一構造自己的查詢基類,以實現對緩存的控制。
如:
```javascript
class Common_ModelQuery extends PhalApi_ModelQuery {
public function __construct($queryArr = array()) {
parent::__construct($queryArr);
if (DI()->debug) {
$this->readCache = FALSE;
$this->writeCache = FALSE;
}
}
}
```
至于DI()->debug的設置,則可以在入口文件中根據約定的接口參數設定,簡單地如:
```javascript
if (isset($_GET['debug']) && $_GET['debug'] == 1) {
DI()->debug = true;
}
```
這樣便可以獲得了接口預覽和調試的能力。
##1.27.5 何時使用此方案?
可以看到,此方案是在緩存策略(包括單點緩存、低高速緩存、多級緩存)和廣義Model層基礎上擴展的,以便應對重量級的業務數據獲取。此方案有一定的優勢,但作為代價則是額外的代碼編寫以及層級復雜性。并且,我們還沒談及到數據變更時的處理。
所以,請在確切需要統一封裝高成本的數據獲取時,才使用此方案。
##1.27.6 擴展:多接口參數傳遞的優雅處理方案
當接口的查詢參數過多時,我們需要手工重復地將接口參數從Api層傳遞到Domain層,再通過Query對象傳遞到Model層,這中間任何一個環節的缺失或遺漏都會造成一個BUG。
為此,項目可以考慮使用一種更為優雅的方案來進行整合,并實現自動化參數獲取,但又保留接口原來的參數驗證。
假設,我們需要以下多個接口參數:
```
function getRules() {
return array(
'getList' => array(
'keyword' => array(...),
'filed' => array(...),
'page' => array(...),
'perpage' => array(...),
'order' => array(...),
),
);
}
```
為避免出現以下這樣的手工調用(而且也不符合值對象的特征):
```
$query = new Query_Demo();
$query->keyword = $this->keyword;
$query->filed = $this->filed;
$query->page = $this->page;
$query->perpage = $this->perpage;
$query->order = $this->order;
$domain = new Domain_Demo();
$list = $domain->getList($query);
```
我們首先需要提取出一個層超類:
```
class Query_Demo extends PhalApi_ModelQuery {
public $keyWord;
public $filed;
public $page;
public $perpage;
public $order;
public function __construct($api) {
//按需獲取,自動初始化
$vars = get_object_vars($api);
foreach ($vars as $key => $var) {
if (isset($api->$key)) {
$this->$key = $api->$key;
}
}
}
}
```
然后,在接口Api中對Domain層的調用就會簡化成:
```
$query = new Query_Demo($this); //自動初始化
$domain = new Domain_Demo();
$list = $domain->getList($query); //通過查詢對象傳遞眾多參數
```
這樣的好處在于:
+ 1、更方便職能的劃分
+ 2、易于測試
+ 3、實現簡單(可提取一個Query的層超類來完成自動填充)
+ 4、便于IDE時的參數提示,同時可以提供默認值
- 歡迎使用PhalApi!
- 接口,從簡單開始!
- [1.1]-下載與安裝
- [1.2]-創建一個自己的項目
- [1.3]-在線體驗
- [1.4]-文檔、幫助和官網
- [1.10]-對PhalApi框架的抉擇
- [1.11]-快速入門(backup)
- [1.12]-參數規則:接口參數規則配置
- [1.13]-統一的接口請求方式:_sevice=XXX.XXX
- [1.14]-統一的返回格式和結構:ret-data-msg
- [1.15]-數據庫操作:基于NotORM的使用及優化
- [1.16]-配置讀取:內外網環境配置的完美切換
- [1.17]-日記紀錄:簡化版的日記接口
- [1.18]-快速函數:人性化的關懷
- [1.19]-DI服務速查:各資源服務一覽表
- [1.20]-DB操作:數據庫基本操作速查
- [1.21]-類的自動加載:遵循PEAR包的命名規范
- [1.22]-簽名驗證:自定義簽名規則
- [1.23]-請求和響應:GET和POST兩者皆可得及超越JSON格式返回
- [1.24]-緩存策略:更靈活地可配置化的多級緩存
- [1.25]-國際化翻譯:為走向國際化提前做好翻譯準備
- [1.26]-數據安全:數據對稱加密方案
- [1.27]-精益開發:更富表現力的Model層和重量級數據獲取的應對方案
- [1.28]-COOKIE:對COOKIE原生態的支持及記憶加密升級版
- [1.29]-開放與封閉:多入口和統一初始化
- [1.30]-保持的力量:接口開發最佳實踐
- [1.31]-新型計劃任務:以接口形式實現的計劃任務
- [2.11]-核心思想:DI依賴注入-讓資源更可控
- [2.12]-海量數據:可配置的分庫分表
- [2.13]-接口調試:在線SQL語句查看與性能優化
- [2.14]-測試驅動開發:意圖導向編程下的接口開發
- [2.15]-演進:新型計劃任務續篇
- [2.16]-領域驅動設計:應對復雜領域業務的Domain層
- [2.17]-微服務:Api接口服務層
- [2.18]-定制化:資源服務的再實現
- [2.19]-擴展庫:可重用的擴展類庫
- [2.20]-約定編程:架構明顯的編程風格
- [2.21]-服務器統一部署方案簡明版:CentOs---Nginx---php-fpm---MySql-[--Memcached]
- [2.22]-更多工具:精益項目和團隊建設
- [3.1]-擴展類庫:微信開發
- [3.2]-擴展類庫:代理模式下phprpc協議的輕松支持
- [3.3]-擴展類庫:基于PHPMailer的郵件發送
- [3.4]-擴展類庫:優酷開放平臺接口調用
- [3.5]-擴展類庫:七牛云存儲接口調用
- [3.6]-擴展類庫:新型計劃任務
- [3.8]-擴展類庫:用戶、會話和第三方登錄集成
- [3.9]-擴展類庫:swoole支持下的長鏈接和異步任務實現
- [3.11]-擴展類庫:基于FastRoute的快速路由
- [4.2]-開發實戰2:模擬優酷開放平臺接口項目開發
- [4.3]-開發實戰3:一個簡單的小型項目開發(奔跑吧兄弟投票活動)
- [5.1]-架構與思想:PhalApi核心設計和思想解讀
- [5.2]-雜談:扯一些PhalApi的前世和今生
- [5.3]-框架總結:術語表和PHP開發建議
- [5.4]-許可
- [5.5]-聯系和加入我們
- [5.6]-更新日記
- [5.8]-致框架貢獻者:加入PhalApi開源指南
- [6.1]-基于接口查詢語言的SDK包
- [6.2]-SDK包(JAVA版)
- [6.3]-SDK包(PHP版)
- [6.4]-SDK包(Objective-C版)
- [6.5]-SDK包(javascript版)
- [6.6]-SDK包(Ruby版)
- [8.1]-PhalApi視頻教程
- 附錄1:接口文檔參考模板