我們能夠有意識地推斷我們想要在哪一條想法的溪流中遨游,然而此后與那些想法的接觸會潛在地塑造我們的習(xí)慣和信仰。 -- 《智慧社會》
此篇章有點長,但我認(rèn)為是值得一讀的。因為這里我將逐步講述如何在已有的基礎(chǔ)上演變擴(kuò)展出更高層次的代碼結(jié)構(gòu)和系統(tǒng)架構(gòu),而不致于因目前頻繁的需求變更而導(dǎo)致代碼凌亂不堪。更為重要的是,你將能從中發(fā)現(xiàn),如何在一個框架中持續(xù)演變,最終體驗浮現(xiàn)式設(shè)計的樂趣。如果你的項目亦能如此,我相信你會找到編程如同搭建積木般輕便明了的感覺。
接口不盡相同,主要區(qū)別在于領(lǐng)域業(yè)務(wù)數(shù)據(jù)的處理。而數(shù)據(jù)的來源則更為廣泛,可能是來自數(shù)據(jù)庫,可能來自第三方平臺接口,可能存放于內(nèi)存。所以,PhalApi這里的Model層,則是 廣義上的數(shù)據(jù)源層 ,用于獲取原始的業(yè)務(wù)數(shù)據(jù),而不管來自何方,何種存儲媒介。這也是為什么我們沒有將Model層打造成活動紀(jì)錄或者數(shù)據(jù)映射器的原因。當(dāng)然,如果你確實需要,也可以自行調(diào)整。
如果數(shù)據(jù)來源于數(shù)據(jù)庫,我們則需要考慮到數(shù)據(jù)庫服務(wù)器的感受,保證不會有過載的請求而導(dǎo)致它罷工。對此,我們可以結(jié)合緩存來進(jìn)行性能優(yōu)化。
如,一般地:
// 版本1:簡單的獲取
$model = new Model_User();
$rs = $model->getByUserId($userId);
這種是沒有緩存的情況,當(dāng)發(fā)現(xiàn)有性能問題并且可以通過緩存來解決時,我們可以在調(diào)用時簡單引入緩存:
// 版本2:使用單點緩存/多級緩存 (應(yīng)該移至Model層中)
$key = 'userbaseinfo_' . $userId;
$rs = DI()->cache->get($key);
if ($rs === NULL) {
$rs = $model->getByUserId($userId);
DI()->cache->set($key, $rs, 600);
}
但不建議在領(lǐng)域Domain層中引入緩存,因為會導(dǎo)致混淆和不便進(jìn)行測試。更好是將緩存的處理移至Model,保持?jǐn)?shù)據(jù)獲取的透明性:
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;
}
對應(yīng)地,外部的調(diào)用調(diào)整成:
// 版本2:使用單點緩存/多級緩存 (應(yīng)該移至Model層中)
$model = new Model_User();
$rs = $model->getByUserIdWithCache($userId);
至此,Model層對于上層如Domain來說,負(fù)責(zé)獲取源數(shù)據(jù),而不管此數(shù)據(jù)來自于數(shù)據(jù)庫,還是遠(yuǎn)程接口,抑或是緩存包裝下的數(shù)據(jù)。這正是我們使用數(shù)組在Model層和Domain層通訊的原因,因為數(shù)組更加通用,不需要額外添加實體。
縱使更富表現(xiàn)力的Model很好地封裝了源數(shù)據(jù)的獲取,但是仍然會遇到一些尷尬的問題。特別地,當(dāng)我們大量地進(jìn)行緩存讀取判斷時,會出現(xiàn)很多重復(fù)的代碼,這樣既不雅觀也難以管理,甚至?xí)霈F(xiàn)一些簡單的人為編寫錯誤而導(dǎo)致的BUG。另外,當(dāng)我們需要進(jìn)行預(yù)覽、調(diào)試或測試時,我們是不希望看到緩存的,即我們能夠手工指定是否需要緩存。
這里再稍微簡單回顧總結(jié)一下我們現(xiàn)在的問題:我們希望通過緩存策略來優(yōu)化Model層的源數(shù)據(jù)獲取,特別當(dāng)源數(shù)據(jù)獲取的成本非常大時。但我們又希望我們可以輕易控制何時需要緩存,何時不需要,并且希望原有的代碼能在OCP的原則下不需要修改,但又能很好地傳遞源數(shù)據(jù)獲取的復(fù)雜參數(shù)。歸納一下,則可分為三點:緩存的控制、源數(shù)據(jù)的獲取、復(fù)雜參數(shù)的傳遞。
不管是單點緩存,還是多級緩存,都希望使用原有已經(jīng)注冊的cache組件服務(wù)。所以,應(yīng)該使用委托。委托的另一個好處在于使用外部依賴注入可以獲得更好的測試性。
源數(shù)據(jù)的獲取,作為源數(shù)據(jù)獲取的主要過程和主要實現(xiàn),需要進(jìn)行緩存的控制(可細(xì)分為:是否允許讀緩存、和是否允許寫緩存)、 獲取緩存的key值和有效時間,以及最終原始數(shù)據(jù)的獲取。明顯,這里應(yīng)該使用模板方法,然后提供鉤子函數(shù)給具體子類。
這里,我們提供了Model代理抽象類PhalApi_ModelProxy。
之所以使用代理模式,是因為實際上并不一定會真正調(diào)用到最終源數(shù)據(jù)的獲取,因為往往源數(shù)據(jù)的獲取成本非常高,故而我們希望通過緩存來攔截數(shù)據(jù)的獲取。
由于Model代理被上層的Domain領(lǐng)域?qū)诱{(diào)用,但又依賴于下層Model層獲得原始數(shù)據(jù),所以處于Domain和Model之間。為了保持良好的項目代碼層級,如果需要創(chuàng)建PhalApi_ModelProxy子類,建議新建一個ModelProxy目錄。
如對用戶基本信息的獲取,我們添加了一個代理:
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)方法由具體子類實現(xiàn),委托給Model_User的實例進(jìn)行源數(shù)據(jù)獲取。另外,實現(xiàn)鉤子函數(shù)以返回緩存唯一key,和緩存的有效時間。
這里只是作為簡單的示例,更好的建議是應(yīng)該將緩存的時間納入配置中管理,如 配置四個緩存級別:低(5 min)、中(10 min)、高(30 min)、超(1 h) ,然后根據(jù)不同的業(yè)務(wù)數(shù)據(jù)使用不同的緩存級別。這樣,即便于團(tuán)隊交流,也便于緩存時間的統(tǒng)一調(diào)整。
敏銳的讀者會發(fā)現(xiàn),上面有一個$query查詢對象,這就是我們即將談到的復(fù)雜參數(shù)的傳遞。
$query是查詢對象PhalApi_ModelQuery的實例。我們強(qiáng)烈建議此類實例應(yīng)當(dāng)被作為 值對象 對待。雖然我們出于便利將此類對象設(shè)計成了結(jié)構(gòu)化的使用。但你可以輕松通過new PhalApi_ModelQuery($query->toArray())來拷貝一個新的查詢對象。
此查詢對象,目前包括了四個成員變量:是否讀緩存、 是否寫緩存、主鍵id、時間戳。
很多時候,這四個基本的變量是滿足不了各項目的實際需求的,因此你可以定義你的查詢子類, 以支持豐富的數(shù)據(jù)獲取。如調(diào)用優(yōu)酷平臺接口獲取用戶最近上傳發(fā)布的視頻時,需要用戶昵稱、獲取的數(shù)量、排序種類等。
在完成了上面的工作后,讓我們看下最終呈現(xiàn)的效果:
// 版本3:緩存 + 代理
$query = new PhalApi_ModelQuery();
$query->id = $userId;
$modelProxy = new ModelProxy_UserBaseInfo();
$rs = $modelProxy->getData($query);
在領(lǐng)域?qū)又?,我們切換到了Model代理獲取數(shù)據(jù),而不再是原來的Model直接獲取。其中新增的是代理具體類 ModelProxy_UserBaseInfo,和可選的查詢類。
至此,我們很好地在源數(shù)據(jù)的獲取基礎(chǔ)上,統(tǒng)一結(jié)合緩存策略。你會發(fā)現(xiàn): 緩存節(jié)點可變、具體的源數(shù)據(jù)可變、復(fù)雜的查詢亦可變 。
將此圖簡化一下,可得到:
這樣的設(shè)計是合理的,因為緩存節(jié)點我們希望能在項目內(nèi)共享,而不管是哪塊的業(yè)務(wù)數(shù)據(jù);對于具體的源數(shù)據(jù)獲取明顯也是不盡相同,所以也需要各自實現(xiàn),同時對于同一類業(yè)務(wù)數(shù)據(jù)(如用戶基本信息)則使用一樣的緩存有效時間和指定格式的緩存key(通常結(jié)合不同的id組成唯一key);最后在前面的緩存共享和同類數(shù)據(jù)的基礎(chǔ)上,還需要支持不同數(shù)據(jù)的具體獲取,因此需要查詢對象。也就是說,你可以在不同的層級不同的范疇內(nèi)進(jìn)行自由的控制和定制。
如果退回到最初的版本,我們可以對比發(fā)現(xiàn),Model Proxy就是Domain和Model間的橋梁,即:中間層。因為每次直接通過Model獲取源數(shù)據(jù)的成本較大,我們可以通過Model Proxy模型代理來緩存獲取的數(shù)據(jù)來減輕服務(wù)器的壓力。
這無疑是細(xì)粒度的劃分,但對于支撐復(fù)雜的領(lǐng)域業(yè)務(wù)卻發(fā)揮著重要的作用。一來是如此清楚明了,二來則是帶來了可測試性。
正如前面提及到的,我們在預(yù)覽、調(diào)試、單元測試或者后臺計劃任務(wù)時,不希望有緩存的干擾。在細(xì)粒度劃分的基礎(chǔ)上,可輕松用以下方法實現(xiàn)而不必?fù)?dān)心會破壞代碼的簡潔性。
在構(gòu)造Model代理時,默認(rèn)情況下使用了DI()->cache作為緩存,當(dāng)需要進(jìn)行單元測試時,我們可以兩種途徑在外部注入模擬的緩存而達(dá)到測試的目的:替換全局的DI()->cache,或單次構(gòu)造注入。對于計劃任務(wù)則可以在統(tǒng)一的后臺任務(wù)啟動文件將DI()->cache設(shè)置成空對象。
在項目層次,我們可以統(tǒng)一構(gòu)造自己的查詢基類,以實現(xiàn)對緩存的控制。
如:
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的設(shè)置,則可以在入口文件中根據(jù)約定的接口參數(shù)設(shè)定,簡單地如:
if (isset($_GET['debug']) && $_GET['debug'] == 1) {
DI()->debug = true;
}
這樣便可以獲得了接口預(yù)覽和調(diào)試的能力。
可以看到,此方案是在緩存策略(包括單點緩存、低高速緩存、多級緩存)和廣義Model層基礎(chǔ)上擴(kuò)展的,以便應(yīng)對重量級的業(yè)務(wù)數(shù)據(jù)獲取。此方案有一定的優(yōu)勢,但作為代價則是額外的代碼編寫以及層級復(fù)雜性。并且,我們還沒談及到數(shù)據(jù)變更時的處理。
所以,請在確切需要統(tǒng)一封裝高成本的數(shù)據(jù)獲取時,才使用此方案。
當(dāng)接口的查詢參數(shù)過多時,我們需要手工重復(fù)地將接口參數(shù)從Api層傳遞到Domain層,再通過Query對象傳遞到Model層,這中間任何一個環(huán)節(jié)的缺失或遺漏都會造成一個BUG。
為此,項目可以考慮使用一種更為優(yōu)雅的方案來進(jìn)行整合,并實現(xiàn)自動化參數(shù)獲取,但又保留接口原來的參數(shù)驗證。
假設(shè),我們需要以下多個接口參數(shù):
function getRules() {
return array(
'getList' => array(
'keyword' => array(...),
'filed' => array(...),
'page' => array(...),
'perpage' => array(...),
'order' => array(...),
),
);
}
為避免出現(xiàn)以下這樣的手工調(diào)用(而且也不符合值對象的特征):
$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層的調(diào)用就會簡化成:
$query = new Query_Demo($this); //自動初始化
$domain = new Domain_Demo();
$list = $domain->getList($query); //通過查詢對象傳遞眾多參數(shù)
這樣的好處在于:
更多建議: