精益開發(fā):更富表現(xiàn)力的Model層和重量級數(shù)據(jù)獲取的應(yīng)對方案

2018-11-21 21:18 更新

我們能夠有意識地推斷我們想要在哪一條想法的溪流中遨游,然而此后與那些想法的接觸會潛在地塑造我們的習(xí)慣和信仰。 -- 《智慧社會》

1.27.1 寫在前面的話

此篇章有點長,但我認(rèn)為是值得一讀的。因為這里我將逐步講述如何在已有的基礎(chǔ)上演變擴(kuò)展出更高層次的代碼結(jié)構(gòu)和系統(tǒng)架構(gòu),而不致于因目前頻繁的需求變更而導(dǎo)致代碼凌亂不堪。更為重要的是,你將能從中發(fā)現(xiàn),如何在一個框架中持續(xù)演變,最終體驗浮現(xiàn)式設(shè)計的樂趣。如果你的項目亦能如此,我相信你會找到編程如同搭建積木般輕便明了的感覺。

1.27.2 更富表現(xiàn)力的Model層

接口不盡相同,主要區(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ù)組更加通用,不需要額外添加實體。

1.27.3 重量級數(shù)據(jù)獲取的應(yīng)對方案

縱使更富表現(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ù)的傳遞。

(1)緩存的控制

不管是單點緩存,還是多級緩存,都希望使用原有已經(jīng)注冊的cache組件服務(wù)。所以,應(yīng)該使用委托。委托的另一個好處在于使用外部依賴注入可以獲得更好的測試性。

(2)源數(shù)據(jù)的獲取

源數(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)整。

(3)復(fù)雜參數(shù)的傳遞

敏銳的讀者會發(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ù)量、排序種類等。

(4)最終的調(diào)用

在完成了上面的工作后,讓我們看下最終呈現(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,和可選的查詢類。

(5)UML靜態(tài)圖

至此,我們很好地在源數(shù)據(jù)的獲取基礎(chǔ)上,統(tǒng)一結(jié)合緩存策略。你會發(fā)現(xiàn): 緩存節(jié)點可變、具體的源數(shù)據(jù)可變、復(fù)雜的查詢亦可變

重量級數(shù)據(jù)獲取的應(yīng)對方案

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

重量級數(shù)據(jù)獲取的應(yīng)對方案-small

這樣的設(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ù)器的壓力。

重量級數(shù)據(jù)獲取的應(yīng)對方案-first

1.27.4 細(xì)粒度和可測試性

這無疑是細(xì)粒度的劃分,但對于支撐復(fù)雜的領(lǐng)域業(yè)務(wù)卻發(fā)揮著重要的作用。一來是如此清楚明了,二來則是帶來了可測試性。

正如前面提及到的,我們在預(yù)覽、調(diào)試、單元測試或者后臺計劃任務(wù)時,不希望有緩存的干擾。在細(xì)粒度劃分的基礎(chǔ)上,可輕松用以下方法實現(xiàn)而不必?fù)?dān)心會破壞代碼的簡潔性。

(1)取消緩存的方法1: 外部注入模擬緩存

在構(gòu)造Model代理時,默認(rèn)情況下使用了DI()->cache作為緩存,當(dāng)需要進(jìn)行單元測試時,我們可以兩種途徑在外部注入模擬的緩存而達(dá)到測試的目的:替換全局的DI()->cache,或單次構(gòu)造注入。對于計劃任務(wù)則可以在統(tǒng)一的后臺任務(wù)啟動文件將DI()->cache設(shè)置成空對象。

(2)取消緩存的方法2: 查詢中的緩存控制

在項目層次,我們可以統(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)試的能力。

1.27.5 何時使用此方案?

可以看到,此方案是在緩存策略(包括單點緩存、低高速緩存、多級緩存)和廣義Model層基礎(chǔ)上擴(kuò)展的,以便應(yīng)對重量級的業(yè)務(wù)數(shù)據(jù)獲取。此方案有一定的優(yōu)勢,但作為代價則是額外的代碼編寫以及層級復(fù)雜性。并且,我們還沒談及到數(shù)據(jù)變更時的處理。

所以,請在確切需要統(tǒng)一封裝高成本的數(shù)據(jù)獲取時,才使用此方案。

1.27.6 擴(kuò)展:多接口參數(shù)傳遞的優(yōu)雅處理方案

當(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ù)

這樣的好處在于:

  • 1、更方便職能的劃分
  • 2、易于測試
  • 3、實現(xiàn)簡單(可提取一個Query的層超類來完成自動填充)
  • 4、便于IDE時的參數(shù)提示,同時可以提供默認(rèn)值
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號