如何使用 Repository 模式?

如何使用 Repository 模式?

 使用 Repository 辅助 Model

Contents

  1. Version
  2. 资料库逻辑
  3. Model
  4. Repository
  5. Conclusion
  6. Sample Code

若将资料库逻辑都写在 model,会造成 model 的肥大而难以维护,基于SOLID原則,我们应该使用 Repository 模式辅助 model,将相关的资料库逻辑封装在不同的 repository,方便中大型项目的维护。

Version


Laravel 5.1.22

资料库逻辑

在 CRUD 中,CUD 比较稳定,但 R 的部分则千变万化,大部分的资料库逻辑都在描述 R 的部分,若将资料库逻辑写在 controller 或 model 都不适当,会造成 controller 与 model 肥大,造成日后难以维护。

Model


使用 repository 之后,model 仅当成Eloquent class 即可,不要包含资料库逻辑,仅保留以下部分 :

  • Property : 如$table$fillable…等。
  • Mutator: 包括 mutator 与 accessor。
  • Method : relation 类的 method,如使用 hasMany() 与 belongsTo()
  • 注解 : 因为 Eloquent 会根据资料库栏位动态产生 property 与 method,等。若使用 Laravel IDE Helper,会直接在 model 加上 @property 与 @method 描述 model 的动态 property 与 method。

User.php

app/User.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
namespace MyBlog;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;


/**
 * MyBlog\User
 *
 * @property integer $id
 * @property string $name
 * @property string $email
 * @property string $password
 * @property string $remember_token
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereEmail($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User wherePassword($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereRememberToken($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereUpdatedAt($value)
 */
class User extends Model implements AuthenticatableContract,
                                    AuthorizableContract,
                                    CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

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

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['password', 'remember_token'];
}

 

12行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * MyBlog\User
 *
 * @property integer $id
 * @property string $name
 * @property string $email
 * @property string $password
 * @property string $remember_token
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereEmail($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User wherePassword($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereRememberToken($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereUpdatedAt($value)
 */

 

IDE-Helper 帮我们替 model 加上注解,让我们可以在 PhpStorm 的语法提示使用 model 的 property 与 method。

Repository


初学者常会在 controller 直接调用 model 写资料库逻辑 :

1
2
3
4
5
6
7
8
public function index()
{
    $users = User::where('age', '>', 20)
                ->orderBy('age')
                ->get();

    return view('users.index', compact('users'));
}

资料库逻辑是要抓 20 岁以上的资料

在中大型专案中,会有几个问题 :

  1. 将资料库逻辑写在 controller,造成 controller 的肥大难以维护。
  2. 违反 SOLID 的单一职责原則 : 资料库逻辑不应该写在 controller。
  3. controller 直接相依于model,使得我们无法对 controller 做单元测试。

比较好的方式是使用 repository :

  1. 将 model 依赖注入到 repository。
  2. 将资料库逻辑写在 repository。
  3. 将 repository 依赖注入到 service。

UserRepository.php

app/Repositories/UserRepository.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
namespace MyBlog\Repositories;

use Doctrine\Common\Collections\Collection;
use MyBlog\User;

class UserRepository
{
    /** @var User 注入的User model */
    protected $user;

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

    /**
     * 回传大于?年级的资料
     * @param integer $age
     * @return Collection
     */
    public function getAgeLargerThan($age)
    {
        return $this->user
            ->where('age', '>', $age)
            ->orderBy('age')
            ->get();
    }
}

 

第 8 行

1
2
3
4
5
6
7
8
9
10
11
/** @var User 注入的User model */
protected $user;

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

 

将相依的 User model 依赖注入到 UserRepository

21 行

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 回传大于?年级的资料
 * @param integer $age
 * @return Collection
 */
public function getAgeLargerThan($age)
{
    return $this->user
        ->where('age', '>', $age)
        ->orderBy('age')
        ->get();
}

 

将抓 20 岁以上的资料的资料库逻辑写在 getAgeLargerThan()

不是使用User facade,而是使用注入的$this->user。33这里也可以使用User facade 的方式,并不会影响可测试性,因为在测试 repository 时,会真的去读写资料库,而不会去 mock User model,因此可以依可测试性決定要用依赖注入还是 Facade。

UserController.php

app/Http/Controllers/UserController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace App\Http\Controllers;

use App\Http\Requests;
use MyBlog\Repositories\UserRepository;

class UserController extends Controller
{
    /** @var  UserRepository 注入的UserRepository */
    protected $userRepository;

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

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $users = $this->userRepository
            ->getAgeLargerThan(20);

        return view('users.index', compact('users'));
    }
}

 

第8行

1
2
3
4
5
6
7
8
9
10
11
12
/** @var  UserRepository 注入的UserRepository */
protected $userRepository;

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

 

将相依的 UserRepository 依赖注入到 UserController

26行

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * Display a listing of the resource.
 *
 * @return \Illuminate\Http\Response
 */
public function index()
{
    $users = $this->userRepository
        ->getAgeLargerThan(20);

    return view('users.index', compact('users'));
}

 

从原本直接相依的 User model,改成依赖注入的 UserRepository

 改用这种写法,有几个优点 :

  1. 资料库逻辑料写在 repository,解决 controller 肥大问题。
  2. 符合 SOLID 的单一职责原則 : 资料库逻辑写在 repository,沒写在 controller。
  3. 符合 SOLID 的依賴反转原則 : controller 并非直接相依于 repository,而是将 repository 依赖注入进 controller。

 实际上建议 repository 仅依赖注入于 service,而不要直接注入在 controller,本范例因为还沒介绍到 servie 模式,为了简化起见,所以直接注入于 controller。

 是否该建立 Repository Interface?

理论上使用依赖注入时,应该使用 interface,不过 interface 目的在于抽象化方便抽换,让程式码达到开放封闭的要求,但是实际上要抽换 repository 的机会不高,除非你有抽换资料库的需求,如从 MySQL 抽换到 MongoDB,此时就该建立 repository interface。

不过由于我们使用了依赖注入,将来要从 class 改成 interface 也很方便,只要在 constructor 的 type hint 改成 interface 即可,维护成本很低,所以在此大可使用 repository class 即可,不一定得用 interface 而造成 over design,等真正需求来时再重构成 interface 即可。

 是否该使用 Query Scope?

Laravel 4.2 就有 query scope,到 5.1 都还留着,它让我们可以将商业逻辑写在 model,解決了维护与重复使用的问题。

User.php

app/User.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
namespace MyBlog;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

/**
 * (注解:略)
 */
class User extends Model implements AuthenticatableContract,
                                    AuthorizableContract,
                                    CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

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

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @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');
    }
}

 

42行

1
2
3
4
5
6
7
8
9
10
11
/**
 * 回传大于?年级的资料
 * @param Builder $query
 * @param integer $age
 * @return Builder
 */
public function scopeGetAgerLargerThan($query, $age)
{
    return $query->where('age', '>', $age)
        ->orderBy('age');
}

 

Query scope 必須以 scope 为 prefix,第 1 个参数为 query builder,一定要加,是 Laravel 要用的。

第2个参数以后为自己要传入的参数。

由于回传也必须是一个 query builder,因此不加上 get()

UserController.php

app/Http/Controllers/UserController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace App\Http\Controllers;

use App\Http\Requests;
use MyBlog\User;

class UserController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $users = User::getAgerLargerThan(20)->get();

        return view('users.index', compact('users'));
    }
}

 

在 controller 呼叫 query scope 时,不要加上 prefix,由于其本质是 query builder,所以还要加上 get() 才能抓到Collection。

由于 query scope 是写在 model,不是写在 controller,所以基本上解決了 controller 肥大与违反 SOLID 的单一职责原则的问题,controller 也可以重复使用 query scope,已经比直接将资料库写在 controller 好很多了。

不过若在中大型项目,仍有以下问题 :

  1. Model 已经有原来的责任,若再加上 query scope,造成 model 过于肥大难以维护。
  2. 若资料库逻辑很多,可以拆成多 repository,可是却很难拆成多 model。
  3. 单元测试困难,必须面临 mock Eloquent 的问题。

Conclusion


  • 业务上可以一开始 1 个 repository 对应 1 个 model,但不用太执着于 1 个 repository 一定要对应 1 个 model,可将 repository 视为逻辑上的资料库逻辑类别即可,可以横跨多个 model 处理,也可以 1 个 model 拆成多个 repository,这看需求而定。
  • Repository 使得资料库逻辑从 controller 或 model中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。

文章来源:https://oomusou.io/laravel/repository

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值