通過Eloquent實(shí)現(xiàn)Repository模式

胖胖的Eloquent

Eloquent采用了ActiveRecord的模式葛虐,這也讓Eloquent招致了好多批評(píng)棉钧,讓我們?nèi)タ船F(xiàn)在Eloquent/Model.php文件涕蚤, 該文件已經(jīng)有3500多行万栅,此時(shí)的Model集成了太多的功能了烦粒,一個(gè)新人很難短時(shí)去理解Model并去很好的使用了代赁,目前Eloquent/Model中主要混合了4個(gè)功能:

  1. Domain Model(包括了data model和領(lǐng)域邏輯)
  2. Row Data Gateway(例如save,delete等數(shù)據(jù)持久化操作)
  3. Table data gateway(各種find方法)
  4. Factory(新建model)

上面介紹的幾種ORM設(shè)計(jì)模式徒役,可以去之前的文章查看:orm 系列 之 常用設(shè)計(jì)模式

我們可以看到Model中混合了各種模式忧勿,這就要求使用者在使用的時(shí)候清楚的知道怎么使用鸳吸,這里的清楚知道怎么用是指根據(jù)SOILD原則,優(yōu)雅的使用Model速勇,本文的目的就是幫助Model的使用者達(dá)成優(yōu)雅的目標(biāo)。

那理想的Model使用是什么樣子的呢养匈?我們希望Model的使用不是ActiveRecord个初,而是較為清晰的DataMapper模式猴蹂,能夠讓domain model和database解耦,然后由DataMapper來完成映射工作磅轻,更具體點(diǎn)聋溜,我們希望的是像clean architecture中定義的架構(gòu)一樣,內(nèi)層是Domain Model漱病,外面是Domain Services,Domain Services又可以具體分為:

  • Repositories

    服務(wù)領(lǐng)域?qū)ο蟮拇嫒±齑绻蠖耸菙?shù)據(jù)庫晃危,就是負(fù)責(zé)將數(shù)據(jù)從數(shù)據(jù)庫中取出老客,將對(duì)象存入數(shù)據(jù)庫胧砰。

  • Factories

    負(fù)責(zé)對(duì)象的創(chuàng)建。

  • Services

    具體的業(yè)務(wù)邏輯权纤,通過調(diào)用多個(gè)對(duì)象和其他服務(wù)來完成一個(gè)業(yè)務(wù)目標(biāo)乌妒。

由此實(shí)現(xiàn)很好的解耦和關(guān)注點(diǎn)分離撤蚊,更具體的關(guān)注clean architecture可以查看簡(jiǎn)書專題:clean architecture.

Eloquent拆解

講述了一些方法論后,我們來動(dòng)手實(shí)作一下

talk is cheap, show me the code

第一步槽唾,我們定義一個(gè)member表

php artisan make:migration MemberCreate

第二步庞萍,編寫表定義

Schema::create('members',function(Blueprint $table){
    $table->increments('id');
    $table->string('login_name');
    $table->string('display_name');
    $table->integer('posts')->unsigned();
});

第三步忘闻,執(zhí)行migration

php artisan migrate

第四步齐佳,生成model文件

php make:model Member

下面開始定義一些接口

The Member Model Interface

interface MemberInterface
{
    public function getID();
    public function getLoginName();
    public function getDisplayName();   
    public function getPostCount();
    public function incrementPostCount();
    public function decrementPostCount();
}

The Eloquent Member Model Implementation

class Member extends Model implements MemberInterface
{
    const ATTR_DISPLAY_NAME = ‘display_name’;
    const ATTR_LOGIN_NAME = ‘login_name’;
    const ATTR_POST_COUNT = ‘posts’;
 
    protected $fillable = [
        self::ATTR_LOGIN_NAME,
        self::ATTR_DISPLAY_NAME
    ];
    public function getID()
    {
        return $this->getKey()
    }
    public function getLoginName()
    {
        return $this->{self::ATTR_LOGIN_NAME};
    }
 
    public function getDisplayName()
    {
        return $this->{self::ATTR_DISPLAY_NAME};
    }
 
    public function getPostCount()
    {
        return $this->{self::ATTR_POST_COUNT};
    }
 
    public function incrementPostCount()
    {
        $this->{self::ATTR_POST_COUNT}++;
    }
    public function decrementPostCount()
    {
        if ($this->getPostCount() > 0) {
            $this->{self::ATTR_POST_COUNT}--;
        }
    }
}

The Member Repository Interface

interface MemberRepositoryInterface
{
   public function find($id);
   public function findTopPosters($count = 10);
   public function save(MemberInterface $member);
}

The Eloquent Member Repository Implementation

class EloquentMemberRepository implements MemberRepositoryInterface {
    /** @var  Member */
    protected $model;

    /**
     * EloquentMemberRepository constructor.
     *
     * @param \App\Member $model
     */
    public function __construct( Member $model )
    {
        $this->model = $model;
    }

    public function find( $id )
    {
        return $this->model->find($id);
    }

    /**
     * @param int $count
     *
     * @return \Illuminate\Support\Collection
     */
    public function findTopPosters( $count = 10 )
    {
        return $this->model
            ->orderBy(($this->model)::ATTR_POST_COUNT, 'DESC')
            ->take($count)
            ->get();
    }

    public function save( MemberInterface $member )
    {
        $member->save();
        /*
      注意:此處我們接口聲明上是 MemberInterface 本鸣,這個(gè)是普適的規(guī)則
        但是此處的 Eloquent 實(shí)現(xiàn)是基于 Eloquent Model的荣德,因此假設(shè) 傳入的
        MemberInterface 實(shí)現(xiàn)了 save 方法
      */
    }
}

基本使用

DemonstrationController
{
    public function createPost(MemberRepositoryInterface $repository)
    {
        // validate request, create the post, and...
        $member = Auth::user()->member();
        $member->incrementPostCount();
        $respository->save($member);
    }
    public function deletePost(MemberRepositoryInterface $repository)
    {
        // validate request, delete the post, and...
        $member = Auth::user()->member();
        $member->decrementPostCount();
        $respository->save($member);
     }
    public function dashboard(MemberRespositoryInterface $repository)
    {
        $members = $repository->findTopPosters(20);
        return view('dashboard', compact('members'));
    }
}

使用的時(shí)候我們看到了好的方式,那如果我們沒有定義repository和interface曹傀,會(huì)怎么樣呢皆愉?

DemonstrationController
{
    public function createPost()
    {
        // validate request, create the post, and...
        $member = Auth::user()->getMember();
        $member->posts++;
        $member->save();
    }
    public function deletePost()
    {
        // validate request, delete the post, and...
        $member = Auth::user()->getMember();
        if ($member->posts > 0) {
            $member->posts--;
            $member->save();
        }
    }
    public function dashboard()
    {
        $members = Member::orderBy('posts', 'DESC')->take(20)->get(); 
        return view('dashboard', compact('members'));
    }
}

上面簡(jiǎn)單的業(yè)務(wù)邏輯posts不能小于0,都沒有很好的封裝异剥,如果上面我們一些增加和減少的功能和save封裝到一起呢冤寿?

DemonstrationController
{
    public function createPost()
    {
        // validate request, create the post, and...
        $member = Auth::user()->member();
        $member->incrementPostCount();
    }
    public function deletePost()
    {
        // validate request, delete the post, and...
        $member = Auth::user()->member();
        $member->decrementPostCount();
    }
}
class Member extends Model implements MemberInterface
{
    //...
    
  
    public function incrementPostCount()
    {
        $this->{self::ATTR_POST_COUNT}++;
        $this->save();
    }
    public function decrementPostCount()
    {
        if ($this->getPostCount() > 0) {
            $this->{self::ATTR_POST_COUNT}--;
            $this->save();
        }
    }
    
    // ...
}

這樣做主要有兩個(gè)問題

  1. 如果我們將Model的實(shí)現(xiàn)由Eloquen轉(zhuǎn)換為其他呢?這將會(huì)使應(yīng)用出錯(cuò)
  2. 我們每個(gè)更改都是執(zhí)行一個(gè)sql語句号杠,嚴(yán)重浪費(fèi)姨蟋,我們完全可以做完更改后,統(tǒng)一一次update

通過上面的對(duì)比堂飞,我們更能發(fā)現(xiàn)使用Repository和Interface的好處酝静,能讓我們更好的實(shí)現(xiàn)關(guān)注點(diǎn)分離,下面我們會(huì)更深入的討論一些問題:包括Collections, Relations, Eager Loading, 和 Schema Changes稼稿。

Eloquent進(jìn)階

首先介紹collection的問題敞恋,看代碼

class FooController
{
   public function bar(PostRepositoryInterface $repository)
   {
      $posts = $repository->findActivePosts();
      $posts->load('author');
   }
}

上面的代碼中硬猫,雖然我們使用的type hint表明使用的repository是PostRepositoryInterface,但是方法findActivePosts返回的collection顯然是跟Eloquent耦合的Eloquent\Collection衬横,那怎么解決這個(gè)問題呢蜂林?有以下幾個(gè)方案

  1. findActivePosts返回?Collection,而不是Eloquent\Collection构眯,避免在Repository之外使用Eloquent相關(guān)的功能
  2. 通過custom collections方法惫霸,返回自定義的collection

下面介紹第二個(gè)議題Eager Loading

還是看代碼

class FooController
{
   public function bar(PostRepositoryInterface $repository)
   {
      $posts = $repository->findActivePosts(['author']);
   }
}

上面的代碼通過參數(shù)['author']的傳入壹店,將eager loading的操作封裝在了findActivePosts之內(nèi),但是這樣子做将塑,反而讓調(diào)用方必須知道實(shí)現(xiàn)細(xì)節(jié)点寥,即本來是功能上的優(yōu)化敢辩,通過eager loading來解決N+1問題的方案盗冷,變?yōu)榱藰I(yè)務(wù)需要知道的業(yè)務(wù)的邏輯了仪糖,明顯是不合理的乓诽。

更可怕的時(shí)候鸠天,你可能會(huì)希望通過傳入?yún)?shù)讓findActivePosts實(shí)現(xiàn)更多的功能,于是變?yōu)榱讼旅娴暮瘮?shù)findActivePostsInDateRange($start, $end, $eagerLoading = null)剥纷,我們看到隨著項(xiàng)目復(fù)雜度的提升晦鞋,我們不得不通過通過參數(shù)來滿足更多的需求,但是這也使得接口變得更復(fù)雜确买,功能更多湾趾,到最后我們不得不面對(duì)各種ugly的代碼,那面對(duì)Eager Loading我們到底應(yīng)該怎么辦呢艺普?下面給出一個(gè)建議:

在提供非eager loading的方法同時(shí)衷敌,提供一個(gè)eager loading的方法。這可能會(huì)被人說:這也不是讓用戶知道了實(shí)現(xiàn)細(xì)節(jié)了嘛面氓。是的舌界,這方法是一個(gè)性能和使用上的妥協(xié)呻拌。

最后介紹Relations藐握,看到代碼

interface MemberInterface
{
   public function getID();
   public function getLoginName();
   public function getDisplayName();   
   public function getPostCount();
   public function incrementPostCount();
   public function decrementPostCount();
   public function getPosts();
   public function getFavoritePosts();
}
class Member extends Model implements MemberInterface
{
   ...

   public function posts()
   {
      return $this->hasMany(Post::class);
   }

   public function getPosts()
   {
       return $this->posts;
   }

   public function getFavoritePosts()
   {
       // I think this will work!
       return $this->posts()
                   ->newQuery()
                   ->whereHas('favorites', function($q) {
                        $q->where(Favorite::ATTR_MEMBER_ID, $this->getID()); 
                     })->get();
   }

   ...
}

我們沒有辦法將relation Method設(shè)置為protect或者private(這樣設(shè)置的目的是讓外面不使用本谜,限制使用范圍)乌助,但是這樣子會(huì)導(dǎo)致想whereHas這種方法執(zhí)行不成功炕泳。

此處還注意到一個(gè)問題培遵,我們此時(shí)使用的posts是表示relation,但是之前是member的一個(gè)字段皇耗,明顯沖突了郎楼,我們需要修改字段名敌买,從postspost_count,因?yàn)槲覀冎笆褂昧顺A縼矶x屬性膘融,因此只需要下面一行代碼就解決問題了:

const ATTR_POST_COUNT = ‘post_count’;

總結(jié)

介紹了這么多春畔,我們解決了一個(gè)核心問題:因?yàn)镋loquent的功能耦合,我們應(yīng)該正確的使用它线召,Eloquent的ActiveRecord模式可以讓我們非常容易的實(shí)現(xiàn)DataMapper缓淹,根據(jù)Clean architecture的定義讯壶,我們將domain services分為了Repositories,F(xiàn)actories躏吊,Services比伏,實(shí)現(xiàn)了關(guān)注點(diǎn)分離。

但是到目前悠菜,還有一個(gè)問題沒有解決,那就是通過Repository篙顺,我們很難實(shí)先Eloquent/Builder那樣豐富的查詢功能德玫,我們不得不每次新增一個(gè)查詢條件,就去新增接口或者參數(shù)椎麦,不慎其煩宰僧,就像之前的findActivePostsInDateRange方法一樣丑陋,那到底有什么辦法解決呢观挎?盡情期待下一篇內(nèi)容琴儿,Repository的實(shí)作。

參考

Separation of Concerns with Laravel’s Eloquent Part 1: An Introduction

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件净当,死亡現(xiàn)場(chǎng)離奇詭異真朗,居然都是意外死亡旗扑,警方通過查閱死者的電腦和手機(jī)边败,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來艳悔,“玉大人乔外,你說我怎么就攤上這事聂渊”睿” “怎么了撰筷?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)肉微。 經(jīng)常有香客問我劳曹,道長(zhǎng)蜕劝,這世上最難降的妖魔是什么蒋荚? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任容为,我火速辦了婚禮得滤,結(jié)果婚禮上龄捡,老公的妹妹穿的比我還像新娘蘑辑。我一直安慰自己庄岖,他們只是感情好链峭,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布不瓶。 她就那樣靜靜地躺著昭娩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪涎劈。 梳的紋絲不亂的頭發(fā)上白筹,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天爷狈,我揣著相機(jī)與錄音妈倔,去河邊找鬼。 笑死宏所,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的撕瞧。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼碗硬,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了稠曼?” 一聲冷哼從身側(cè)響起抵赢,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤款违,失蹤者是張志新(化名)和其女友劉穎当窗,沒想到半個(gè)月后疏遏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體司致,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡扒俯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年斋配,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尖奔,死狀恐怖毁习,靈堂內(nèi)的尸體忽然破棺而出恐仑,到底是詐尸還是另有隱情,我是刑警寧澤歧斟,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布花枫,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一牙寞、第九天 我趴在偏房一處隱蔽的房頂上張望用狱。 院中可真熱鬧镀梭,春花似錦、人聲如沸锻煌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽萌踱。三九已至赁严,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間橱夭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工而钞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留沙廉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓臼节,卻偏偏與公主長(zhǎng)得像撬陵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子网缝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容