如何使用 Repository 模式

houmuxu發表於2019-12-31

如何使用 Repository 模式

若將資料庫邏輯都寫在 Model 裡,會造成 model 程式碼的臃腫難以維護,基於 SOLID 原則,我們應該使用 Repository 模式輔助 Model,將相關的資料庫邏輯封裝在不同的 Repository,方便後期專案的維護。

Laravel 框架版本

Laravel 5.4.17

資料庫邏輯

在 CURD 中,CUR 比較穩定,但 Read 的部分則變化萬千,大部分的資料庫邏輯都在描述 Read 部分,若將資料庫邏輯寫在 Controller 或 Model 都不合適,會造成 Controller 或 Model 程式碼臃腫,如後難以維護。

Model

使用 Repository 模式之後,Model 僅僅當成 Eloquent Class 即可,不需要包含資料庫邏輯,僅保留如下部分:

  • Property: 如 $table``$fillable ..

  • Mutator: 包括 mutator 與 accessor

  • Method: relation 類的方法,比如使用 hasMany() 與 belongsTo()

單一對應關係:

  • hasOne
  • belongsTo
  • morphTo
  • morphOne

多個對應關係指的是使用以下關鍵詞定義的關聯模型:

  • hasMany
  • belongsToMany
  • morphMany
  • morphToMany
  • morphedByMany

因為 Eloquent 會根據資料庫欄位動態的產生 property 與 method等,若使用 Laravel IDE Helper ,會直接在Model加上 @property 與 @method 描述model的動態 proerty 與 method。 如下app\User.php中安裝完Laravel IDE Helper後執行php artisan ide-helper:models後自動生成的內容:

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

/**
 * App\User
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property string $password
 * @property string $remember_token
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
 * @method static \Illuminate\Database\Query\Builder|\App\User whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereRememberToken($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

Repository

在開發時常常會在 Controller 直接呼叫 Model 寫資料庫邏輯,如下:獲取資料庫中使用者 age>20的資料。

public function index()
{
    return User::where('age','>',20)->orderBy('age')->get();
}

這樣寫邏輯會有幾個問題:

  • 將資料庫邏輯寫在 Controller,造成 Controller 程式碼臃腫難以維護。

  • 違反了 SOLID 的單一職責原則,資料庫邏輯不應該寫在 Controller 中。

  • Controller 直接操作 Model,使得對 Controller 做單元測試困難。

比較好的方式是使用 Repository:

  • 將 Model 依賴注入到 Repository。

  • 將資料庫邏輯寫在 Repository。

  • 將 Repository 依賴注入到 Service。

app/Repositories/UserRepostitory.php 中的內容:

<?php

namespace App\Repositories;

use App\User;

/**
 * Class UserRepository
 * @package App\Repositories
 */
class UserRepository
{
    /**
     * @var User
     */
    private $user;

    /**
     * UserRepository constructor.
     * @param $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * @param $age
     * @return \Illuminate\Database\Eloquent\Collection|static[]
     */
    public function getAgeLargerThan($age)
    {
        return $this->user
            ->where('age', '>', $age)
            ->orderBy('age')
            ->get();
    }

}

在控制器app\Controllers\UserController.php中使用依賴注入:

<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;
use Illuminate\Http\Request;

/**
 * Class UserController
 *
 * @package App\Http\Controllers
 */
class UserController extends Controller
{
    /**
     * @var \App\Repositories\UserRepository
     */
    protected $userRepository;

    /**
     * UserController constructor.
     * @param $userRepository
     */
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * @return \Illuminate\Database\Eloquent\Collection|static[]
     */
    public function index()
    {
        return $this->userRepository->getAgeLargerThan(20);
    }
}

將相依的 UserRepository 依賴注入到 UserController,並從原本直接依賴 User Model改成依賴注入的 UserRepository

優點

  • 將資料庫邏輯寫在 Repository 裡,解決了 Controller 程式碼臃腫的問題。

  • 符合 SOLID 的單一職責原則:資料庫邏輯寫在 Repository 裡,沒寫在 Controller 裡。

  • 符合 SOLID 的依賴反轉原則:Controller 並非直接相依與 Repositroy,而是將 Repository 依賴注入進 Controller。

實際上建議 Repository 僅依賴注入進 Service,而不是直接注入在 Controller。

是否該建立 Repository Interface?

理論上使用依賴注入時,應該使用 Interface ,不過 Interface 目的在於更換資料庫,讓程式碼達到開放封閉的要求,但是實際上要更改 Reposiroty 的機會也不多,除非是從 MySQL 更換到 MongoDB,此時就應該建立 Repository Interface。

不過由於我們使用了依賴注入,將來要從 Class 改成 Interface 也很方便,只要在 Constructor 的 type hint 改成 Interface 即可,維護成本很低,所以在此大可使用 Repository Class 即可,不一定得用Interface而造成 Over Design,等真正需要修改時,再重構 Interface 即可。

是否該使用 Query Scope?

Laravel 4.2 就有 QueryScope,到 Laravel5.1 都還保留著,它讓我們可以將邏輯程式碼寫在 Model ,解決了維護與重複使用的問題。

如 app/User.php 裡的程式碼:

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

/**
 * App\User
 *
 * @property int                                                                                                   $id
 * @property string                                                                                                         $name
 * @property string                                                                                                         $email
 * @property string                                                                                                         $password
 * @property string                                                                                                         $remember_token
 * @property \Carbon\Carbon                                                                                                 $created_at
 * @property \Carbon\Carbon                                                                                                 $updated_at
 * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
 * @method static \Illuminate\Database\Query\Builder|\App\User whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereRememberToken($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     *
     * @param Builder $query
     * @param integer $age
     *
     * @return Builder
     */
    public function scopeGetAgerLargerThan($query, $age)
    {
        return $query->where('age', '>', $age)
            ->orderBy('age');
    }
}

QueryScope 必須以 scope開頭,第一個引數為 queryBuilder,一定要加上;第二個引數以後為自己要傳入的引數。
由於回傳必須是一個 queryBuilder ,因此不需要加上 get()

app/Controllers/UserController.php 中使用程式碼:

<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;
use App\User;
use Illuminate\Http\Request;

/**
 * Class UserController
 *
 * @package App\Http\Controllers
 */
class UserController extends Controller
{
    /**
     * @var \App\Repositories\UserRepository
     */
    protected $userRepository;

    /**
     * UserController constructor.
     * @param $userRepository
     */
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * @return \Illuminate\Database\Eloquent\Collection|static[]
     */
    public function index()
    {
        return User::getAgerLargerThan(20)->get();
    }

}

在 Controller 中使用 QueryScope 時,不需要加上 Prefix,由於其本質是 queryBuilder,所以還要加上 get() 才能獲得 Conllection 資料。

由於 QueryScope 是寫在 Model,不是寫在 Controller,所以基本上解決了 Controller 臃腫違反 SOLID 的單一職責原則的問題, Controller 也可以重複使用 QueryScope ,已經比直接將資料庫邏輯寫在 Controlelr 中好很多。

不過若在中大型專案中,仍然有以下問題:

  • Model 已經有原來的責任,若再加上 queryScope,造成 Model 過於臃腫難以維護。

  • 若資料庫邏輯很多,可能拆成多個 Repository,可是確很難拆成多個 Model。

  • 單元測試困難,必須面臨 mock Eloquent 的問題。

最後

實際開發時,可以一開始 1 個 Repository 對應 1 個 Model,但是也不必太過執著於 1 個 Repository,一定要對應 1 個 Model,可將 Repository 視為邏輯上的資料庫邏輯類別即可,可以橫跨多個Model處理,也可以 1 個 Model 拆成多個 Repository,視情況而定。

Repository 使得資料庫邏輯從 Controller 或 Model 中解放,不僅更容易維護、更容易擴充、更容易重複使用,也更容易測試。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章