1、概述

首先需要声明的是设计模式和使用的框架以及语言是无关的,关键是要理解设计模式背后的原则,这样才能不管你用的是什么技术,都能够在实践中实现相应的设计模式。

按照最初提出者的介绍,Repository 是衔接数据映射层和领域层之间的一个纽带,作用相当于一个在内存中的域对象集合。客户端对象把查询的一些实体进行组合,并把它 们提交给 Repository。对象能够从 Repository 中移除或者添加,就好比这些对象在一个 Collection 对象上进行数据操作,同时映射层的代码会对应的从数据库中取出相应的数据。

从概念上讲,Repository 是把一个数据存储区的数据给封装成对象的集合并提供了对这些集合的操作。

Repository 模式将业务逻辑和数据访问分离开,两者之间通过 Repository 接口进行通信,通俗点说,可以把 Repository 看做仓库管理员,我们要从仓库取东西(业务逻辑),只需要找管理员要就是了(Repository),不需要自己去找(数据访问),具体流程如下图所示:

这种将数据访问从业务逻辑中分离出来的模式有很多好处:

  • 集中数据访问逻辑使代码易于维护
  • 业务和数据访问逻辑完全分离
  • 减少重复代码
  • 使程序出错的几率降低

2、只是接口而已

要实现 Repository 模式,首先需要定义接口,这些接口就像 Laravel 中的契约一样,需要具体类去实现。现在我们假定有两个数据对象 Actor 和 Film。这两个数据对象上可以进行哪些操作呢?一般情况下,我们会做这些事情:

  • 获取所有记录
  • 获取分页记录
  • 创建一条新的记录
  • 通过主键获取指定记录
  • 通过属性获取相应记录
  • 更新一条记录
  • 删除一条记录

现在你已经意识到如果我们为每个数据对象实现这些操作要编写多少重复代码!当然,对小型项目而言,这不是什么大问题,但如果对大型应用而言,这显然是个坏主意。

现在,如果我们要定义这些操作,需要创建一个 Repository 接口:

interface RepositoryInterface {
public function all($columns = array('*'));
public function paginate($perPage = 15, $columns = array('*'));
public function create(array $data);
public function update(array $data, $id);
public function delete($id);
public function find($id, $columns = array('*'));
public function findBy($field, $value, $columns = array('*'));
}

3、目录结构

在我们继续创建具体的 Repository 实现类之前,让我们先想想要如何组织我们要编写的代码,通常,当我要创建一些类的时候,我喜欢以组件的方式来组织代码,因为我希望这些代码可以很方便地在其他项目中被复用。我为 Repository 组件定义的目录结构如下:

但是这也不是一成不变的,要视具体情况来定。比如如果组件包括配置项,或者迁移之类的话,目录结构会有所不同。

在 src 目录下,我创建了三个子目录:ContractsEloquent 和 Exceptions。这样命令的原因是显而易见的,一眼就能看出里面存放的是什么类。我们会将接口放在 Contracts 目录下,Eloquent 目录用于存放实现 Repository接口的抽象类和具体类,而 Exceptions 目录用于存放异常处理类。

由于我们创建的是一个扩展包,需要创建一个 composer.json 文件用于定义命名空间映射目录,包依赖以及其它的元数据。下面是我的 composer.json 文件内容:

{
"name": "bosnadev/repositories",
"description": "Laravel Repositories",
"keywords": [
"laravel",
"repository",
"repositories",
"eloquent",
"database"
],
"licence": "MIT",
"authors": [
{
"name": "Mirza Pasic",
"email": "mirza.pasic@edu.fit.ba"
}
],
"require": {
"php": ">=5.4.0",
"illuminate/support": "5.*",
"illuminate/database": "5.*"
},
"autoload": {
"psr-4": {
"Bosnadev\\Repositories\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Bosnadev\\Tests\\Repositories\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "0.x-dev"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

正如你所看到的,我们将 Bosnadev\Repository 映射到了 src 目录。另外,在我们实现 RepositoryInterface 之前,由于其位于 Contracts 目录下,我们需要为其设置正确的命名空间:

<?php
namespace Bosnadev\Repositories\Contracts; interface RepositoryInterface {
...
}

下面我们准备正式开始实现这个契约。

4、Repository 实现

使用 Repository 允许我们在数据源(Data Source)中查询数据,并将这些数据返回给业务逻辑(Business Entity),同时也能够将业务逻辑中的数据修改持久化到数据源中:

当然,每一个具体的子 Repository 都继承自抽象的 Repository 父类,这个抽像的 Repository 父类则实现了 RepositoryInterface 契约。现在,我们正式开始实现这个契约。

契约中的第一个方法是 all(),用于为具体业务逻辑获取所有记录,该方法只接收一个数组参数 $columns,该参数用于指定从数据源中返回的字段,默认返回所有字段:

public function all($columns = array('*')) {
return Bosnadev\Models\Actor::get($columns);
}

但是这样还不够,我们想让它成为一个更通用的方法:

public function all($columns = array('*')) {
return $this->model->get($columns);
}

其中 $this->model 是 Bosnadev\Models\Actor 的实例,这样的话,我们还需要定义设置该实例的方法:

<?php
namespace Bosnadev\Repositories\Eloquent; use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Exceptions\RepositoryException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Container\Container as App; /**
* Class Repository
* @package Bosnadev\Repositories\Eloquent
*/
abstract class Repository implements RepositoryInterface { /**
* @var App
*/
private $app; /**
* @var
*/
protected $model; /**
* @param App $app
* @throws \Bosnadev\Repositories\Exceptions\RepositoryException
*/
public function __construct(App $app) {
$this->app = $app;
$this->makeModel();
} /**
* Specify Model class name
*
* @return mixed
*/
abstract function model(); /**
* @return Model
* @throws RepositoryException
*/
public function makeModel() {
$model = $this->app->make($this->model()); if (!$model instanceof Model)
throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"); return $this->model = $model;
}
}

我们在该抽象类中定义了一个抽象方法 model(),强制在实现类中实现该方法已获取当前实现类对应的模型:

<?php
namespace App\Repositories; use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Eloquent\Repository; class ActorRepository extends Repository { /**
* Specify Model class name
*
* @return mixed
*/
function model()
{
return 'Bosnadev\Models\Actor';
}
}

现在我们在抽象类中实现其它契约方法:

<?php
namespace Bosnadev\Repositories\Eloquent; use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Exceptions\RepositoryException; use Illuminate\Database\Eloquent\Model;
use Illuminate\Container\Container as App; /**
* Class Repository
* @package Bosnadev\Repositories\Eloquent
*/
abstract class Repository implements RepositoryInterface { /**
* @var App
*/
private $app; /**
* @var
*/
protected $model; /**
* @param App $app
* @throws \Bosnadev\Repositories\Exceptions\RepositoryException
*/
public function __construct(App $app) {
$this->app = $app;
$this->makeModel();
} /**
* Specify Model class name
*
* @return mixed
*/
abstract function model(); /**
* @param array $columns
* @return mixed
*/
public function all($columns = array('*')) {
return $this->model->get($columns);
} /**
* @param int $perPage
* @param array $columns
* @return mixed
*/
public function paginate($perPage = 15, $columns = array('*')) {
return $this->model->paginate($perPage, $columns);
} /**
* @param array $data
* @return mixed
*/
public function create(array $data) {
return $this->model->create($data);
} /**
* @param array $data
* @param $id
* @param string $attribute
* @return mixed
*/
public function update(array $data, $id, $attribute="id") {
return $this->model->where($attribute, '=', $id)->update($data);
} /**
* @param $id
* @return mixed
*/
public function delete($id) {
return $this->model->destroy($id);
} /**
* @param $id
* @param array $columns
* @return mixed
*/
public function find($id, $columns = array('*')) {
return $this->model->find($id, $columns);
} /**
* @param $attribute
* @param $value
* @param array $columns
* @return mixed
*/
public function findBy($attribute, $value, $columns = array('*')) {
return $this->model->where($attribute, '=', $value)->first($columns);
} /**
* @return \Illuminate\Database\Eloquent\Builder
* @throws RepositoryException
*/
public function makeModel() {
$model = $this->app->make($this->model()); if (!$model instanceof Model)
throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"); return $this->model = $model->newQuery();
}
}

很简单,是吧?现在剩下唯一要做的就是在 ActorsController 中依赖注入 ActorRepository

<?php
namespace App\Http\Controllers; use App\Repositories\ActorRepository as Actor; class ActorsController extends Controller { /**
* @var Actor
*/
private $actor; public function __construct(Actor $actor) {
$this->actor = $actor;
} public function index() {
return \Response::json($this->actor->all());
}
}

5、Criteria 查询实现

上面的实现对简单查询而言已经足够了,但是对大型项目而言,有时候需要通过 Criteria 创建一些自定义查询获取一些更加复杂的查询结果集。

要实现这一功能,我们首先定义如下这个抽象类:

<?php
namespace Bosnadev\Repositories\Criteria; use Bosnadev\Repositories\Contracts\RepositoryInterface as Repository;
use Bosnadev\Repositories\Contracts\RepositoryInterface; abstract class Criteria { /**
* @param $model
* @param RepositoryInterface $repository
* @return mixed
*/
public abstract function apply($model, Repository $repository);
}

该抽象类中声明了一个抽象方法 apply,在继承该抽象类的具体实现类中需要实现这个方法实现 Criteria 查询。在定义实现该抽象类的具体类之前,我们先为 Repository 类创建一个新的契约:

<?php
namespace Bosnadev\Repositories\Contracts; use Bosnadev\Repositories\Criteria\Criteria; /**
* Interface CriteriaInterface
* @package Bosnadev\Repositories\Contracts
*/
interface CriteriaInterface { /**
* @param bool $status
* @return $this
*/
public function skipCriteria($status = true); /**
* @return mixed
*/
public function getCriteria(); /**
* @param Criteria $criteria
* @return $this
*/
public function getByCriteria(Criteria $criteria); /**
* @param Criteria $criteria
* @return $this
*/
public function pushCriteria(Criteria $criteria); /**
* @return $this
*/
public function applyCriteria();
}

接下来我们修改 Repository 的抽象类如下:

<?php
namespace Bosnadev\Repositories\Eloquent; use Bosnadev\Repositories\Contracts\CriteriaInterface;
use Bosnadev\Repositories\Criteria\Criteria;
use Bosnadev\Repositories\Contracts\RepositoryInterface;
use Bosnadev\Repositories\Exceptions\RepositoryException; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Container\Container as App; /**
* Class Repository
* @package Bosnadev\Repositories\Eloquent
*/
abstract class Repository implements RepositoryInterface, CriteriaInterface { /**
* @var App
*/
private $app; /**
* @var
*/
protected $model; /**
* @var Collection
*/
protected $criteria; /**
* @var bool
*/
protected $skipCriteria = false; /**
* @param App $app
* @param Collection $collection
* @throws \Bosnadev\Repositories\Exceptions\RepositoryException
*/
public function __construct(App $app, Collection $collection) {
$this->app = $app;
$this->criteria = $collection;
$this->resetScope();
$this->makeModel();
} /**
* Specify Model class name
*
* @return mixed
*/
public abstract function model(); /**
* @param array $columns
* @return mixed
*/
public function all($columns = array('*')) {
$this->applyCriteria();
return $this->model->get($columns);
} /**
* @param int $perPage
* @param array $columns
* @return mixed
*/
public function paginate($perPage = 1, $columns = array('*')) {
$this->applyCriteria();
return $this->model->paginate($perPage, $columns);
} /**
* @param array $data
* @return mixed
*/
public function create(array $data) {
return $this->model->create($data);
} /**
* @param array $data
* @param $id
* @param string $attribute
* @return mixed
*/
public function update(array $data, $id, $attribute="id") {
return $this->model->where($attribute, '=', $id)->update($data);
} /**
* @param $id
* @return mixed
*/
public function delete($id) {
return $this->model->destroy($id);
} /**
* @param $id
* @param array $columns
* @return mixed
*/
public function find($id, $columns = array('*')) {
$this->applyCriteria();
return $this->model->find($id, $columns);
} /**
* @param $attribute
* @param $value
* @param array $columns
* @return mixed
*/
public function findBy($attribute, $value, $columns = array('*')) {
$this->applyCriteria();
return $this->model->where($attribute, '=', $value)->first($columns);
} /**
* @return \Illuminate\Database\Eloquent\Builder
* @throws RepositoryException
*/
public function makeModel() {
$model = $this->app->make($this->model()); if (!$model instanceof Model)
throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"); return $this->model = $model->newQuery();
} /**
* @return $this
*/
public function resetScope() {
$this->skipCriteria(false);
return $this;
} /**
* @param bool $status
* @return $this
*/
public function skipCriteria($status = true){
$this->skipCriteria = $status;
return $this;
} /**
* @return mixed
*/
public function getCriteria() {
return $this->criteria;
} /**
* @param Criteria $criteria
* @return $this
*/
public function getByCriteria(Criteria $criteria) {
$this->model = $criteria->apply($this->model, $this);
return $this;
} /**
* @param Criteria $criteria
* @return $this
*/
public function pushCriteria(Criteria $criteria) {
$this->criteria->push($criteria);
return $this;
} /**
* @return $this
*/
public function applyCriteria() {
if($this->skipCriteria === true)
return $this; foreach($this->getCriteria() as $criteria) {
if($criteria instanceof Criteria)
$this->model = $criteria->apply($this->model, $this);
} return $this;
}
}

创建一个新的 Criteria

有了 Criteria 查询,你现在可以更简单的组织 Repository 代码:

你可以这样定义 Criteria 类:

<?php
namespace App\Repositories\Criteria\Films; use Bosnadev\Repositories\Contracts\CriteriaInterface;
use Bosnadev\Repositories\Contracts\RepositoryInterface as Repository;
use Bosnadev\Repositories\Contracts\RepositoryInterface; class LengthOverTwoHours implements CriteriaInterface { /**
* @param $model
* @param RepositoryInterface $repository
* @return mixed
*/
public function apply($model, Repository $repository)
{
$query = $model->where('length', '>', 120);
return $query;
}
}

在控制器中使用 Criteria

现在我们已经定义了一些简单的 Criteria,现在我们来看看如何使用它们。在 Repository 中有两种方式使用 Criteria,第一种个是使用 pushCriteria 方法:

<?php
namespace App\Http\Controllers; use App\Repositories\Criteria\Films\LengthOverTwoHours;
use App\Repositories\FilmRepository as Film; class FilmsController extends Controller { /**
* @var Film
*/
private $film; public function __construct(Film $film) {
$this->film = $film;
} public function index() {
$this->film->pushCriteria(new LengthOverTwoHours());
return \Response::json($this->film->all());
}
}

这个方法在你需要多个 Criteria 时很有用。然而,如果你只想使用一个 Criteria,可以使用 getByCriteria() 方法:

<?php
namespace App\Http\Controllers; use App\Repositories\Criteria\Films\LengthOverTwoHours;
use App\Repositories\FilmRepository as Film; class FilmsController extends Controller { /**
* @var Film
*/
private $film; public function __construct(Film $film) {
$this->film = $film;
} public function index() {
$criteria = new LengthOverTwoHours();
return \Response::json($this->film->getByCriteria($criteria)->all());
}
}

6、安装依赖包

本教程提及的 Repository 实现在 GitHub 上有对应扩展包:https://github.com/Bosnadev/Repositories

你可以通过在项目根目录下的 composer.json 中添加如下这行依赖:

"bosnadev/repositories": "0.*"

然后运行 composer update 来安装这个 Repository 包。

在 Laravel 5 中使用 Repository 模式实现业务逻辑和数据访问的分离的更多相关文章

  1. PHP7 学习笔记(十五)Repository 模式实现业务逻辑和数据访问的分离

    参考: 1.http://laravelacademy.org/post/3063.html

  2. (转)MVC中的Repository模式

    1.首先创建一个空的MVC3应用程序,命名为MyRepository.Web,解决方案命名为MyRepository. 2.添加一个类库项目,命名为MyRepository.DAL,添加一个文件夹命名 ...

  3. MVC中的Repository模式

    1.首先创建一个空的MVC3应用程序,命名为MyRepository.Web,解决方案命名为MyRepository. 2.添加一个类库项目,命名为MyRepository.DAL,添加一个文件夹命名 ...

  4. MVC架构中的Repository模式 个人理解

    关于MVC架构中的Repository模式   个人理解:Repository是一个独立的层,介于领域层与数据映射层(数据访问层)之间.它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接 ...

  5. Laravel 中使用 Repository 模式

    在本文中,我会向你展示如何在 Laravel 中从头开始实现 repository 设计模式.我将使用 Laravel 5.8.3 版,但 Laravel 版本不是最重要的.在开始写代码之前,你需要了 ...

  6. 在 ASP.NET 中创建数据访问和业务逻辑层(转)

    .NET Framework 4 当在 ASP.NET 中处理数据时,可从使用通用软件模式中受益.其中一种模式是将数据访问代码与控制数据访问或提供其他业务规则的业务逻辑代码分开.在此模式中,这两个层均 ...

  7. js原生设计模式——13桥接模式(相同业务逻辑抽象化处理的职责链模式)

    桥接模式之多元化类之间的实例化调用实例 <!DOCTYPE html><html lang="en"><head>    <meta ch ...

  8. Repository模式介绍汇总

    1.Linq To Sql中Repository模式应用场景 http://www.cnblogs.com/zhijianliutang/archive/2012/02/24/2367305.html ...

  9. App 组件化/模块化之路——Repository 模式

    什么是 Repository 模式 Repository 这个词直译过来仓库.仓储的意思.这个意思其实也能反应出 Repository 模式作用.App 开发中少不了对数据的操作,数据的来源可能有很多 ...

随机推荐

  1. (GoRails )使用Vue.js制作拖拉list功能(v5-8)

    视频5 改进视觉效果,让list看起来更舒服.新增横向滚动功能. 参考我的trello:https://trello.com/b/BYvCBpyZ/%E6%AF%8F%E6%97%A5%E8%AE%B ...

  2. ubuntu下使用CAJ云阅读--CAJViewer(Cloud)

    摘要:Linux(Ubuntu)没有直接打开caj论文格式的软件.网上流传最多的“CAJViewer6.0_green”.“CAJViewer7.2”都没法正常使用,所以迫切需要新的方法或软件;我发现 ...

  3. LTE时代的定位技术:OTDOA,LPP,SUPL2.0

    LTE时代的定位技术:OTDOA,LPP,SUPL2.0 移动定位技术的发展历程 如今智能手机已经在整个社会普及,数量众多的手机应用成为了人们生活当中不可或缺的一部分.越来越多的手机应用都用到了手机定 ...

  4. Leetcode 74

    class Solution { public: bool searchMatrix(vector<vector<int>>& matrix, int target) ...

  5. 铺音out2

    1◆ 忘记的 ed t   d     du dʒ dge   si ʒ su         ph f gh     ck k ch gh   2◆ 整理 success

  6. POJ 3013最短路变形....

    DES:计算输的最小费用.如果不能构成树.输出-1.每条边的费用=所有的子节点权值*这条边的权值.计算第二组样例可以知道树的费用是所有的节点的权值*到根节点的最短路径的长度. 用dij的邻接矩阵形式直 ...

  7. Get gcc built-in macros using command gcc -dM -E - < /dev/null

    root@vmuser-virtual-machine:/home/vmuser# gcc -dM -E - < /dev/null #define __SSP_STRONG__ 3#defin ...

  8. mysql主从搭建之诡异事件

    今天在搭建主从后出现了主库system账号丢失INSERT权限的情况,记录如下 主库: system账号权限同root权限,并且mysql库已经删除 从库: mysql库存在,无system账号 主从 ...

  9. bzoj1215

    题解: 暴力枚举每一种方案,然后hash判重 代码: #include<bits/stdc++.h> #define eps 1e-7 using namespace std; ],r[] ...

  10. Python & PyCharm & Django 搭建web开发环境(续)

    由于Django自带轻量级的server,因此在前篇博文中,默认使用该server,但实际生产中是不允许这么干的,生产环境中通常使用Apache Httpd Server结合mod_wsgi.so来做 ...