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