细说Laravel中存储库模式(Repository)到底有何用?

2022-05-15 0 952

Laravel中的存储库模式为什么在Laravel中使用存储库模式?

在我 上一篇文章 中,我解释了什么是存储库模式,它与Active Record模式有何不同,以及如何在Laravel中实现它。现在我想深入了解一下为什么应该使用存储库模式。

我在上一篇文章的评论中注意到,Repository模式在Laravel社区中是一个有争议的话题。有些人认为没有理由使用它,并坚持使用内置的Active Record模式。其他人则倾向于使用其他方法将数据访问从逻辑域中分离出来。请注意,我尊重这些意见,并将在接下来的博客文章中专门讨论此主题。

有了这个免责声明,让我们来了解一下使用存储库模式的优点。【相关教程推荐:laravel

单一责任原则

单一责任原则是主要鉴别器来区分Active Record模式和存储库模式。模型类已经保存数据并提供域对象的方法。当使用Active Record模式时,数据访问是额外引入的责任。这是我想在以下示例中说明的东西:

/** * @property string $first_name * @property int    $company_id */class Employee extends Model {}$jack = new Employee();$jack->first_name = 'Jack';$jack->company_id = $twitterId;$jack->save();

虽然域模型和数据访问技术的职责混合,但它直观上看还说得过去。在我们的应用程序中,员工必须以某种方式存储在数据库中,因此为什么不调用对象上的save()。单个对象被转化成单个数据行并存储。

但是,让我们更进一步,看看我们还能对员工做些什么:

$jack->where('first_name', 'John')->firstOrFail()->delete();$competition = $jack->where('company_id', $facebookId)->get();

现在,它变得不直观,甚至违背了我们的域模型。 为什么 Jack 会突然删除另一个甚至可能在不同公司工作的员工? 或者他为什么能把 Facebook 的员工拉过来?

当然,这个例子是人为设计的,但它仍然显示了 Active Record 模式如何不允许有意的域模型。 员工与所有员工列表之间的界限变得模糊。 您始终必须考虑该员工是被用作实际员工还是作为访问其他员工的机制。

仓库模式通过强制执行这个基本分区来解决这个问题。它的唯一用途是标识域对象的合集,而不是域对象的本身。

要点:

通过将所有域对象的集合与单个域对象分离, 仓库模式体现了单一责任原则

不要重复自己 (DRY)

一些项目将数据库查询洒遍了整个项目。下面是一个例子,我们从数据库中获取列表,并在 Blade 视图中显示他们。

class InvoiceController {    public function index(): View {        return view('invoices.index', [            'invoices' => Invoice::where('overdue_since', '>=', Carbon::now())                ->orderBy('overdue_since')                ->paginate()        ]);    }}

当这样的查询遍得更加复杂并且在多个地方使用时,考虑将其提取到 Repository 方法中。

存储库模式通过将重复查询打包到表达方法中来帮助减少重复查询。如果必须调整查询,只需更改一次即可。

class InvoiceController {    public __construct(private InvoiceRepository $repo) {}    public function index(): View {        return view('invoices.index', [            'invoices' => $repo->paginateOverdueInvoices()        ]);    }}

现在查询只实现一次,可以单独测试并在其他地方使用。此外,单一责任原则再次发挥作用,因为控制器不负责获取数据,而只负责处理HTTP请求和返回响应。

Takeaway:

存储库模式有助于减少重复查询

依赖反转

解释 Dependency Inversion Principle 值得发表自己的博客文章。我只是想说明存储库可以启用依赖项反转。

在对组件进行分层时,通常较高级别的组件依赖于较低级别的组件。 例如,控制器将依赖模型类从数据库中获取数据:

class InvoiceController {    public function index(int $companyId): View {        return view(            'invoices.index',            ['invoices' => Invoice::where('company_id', $companyId)->get()]        );    }}

依赖关系是自上而下的,紧密耦合的。 InvoiceController 取决于具体的 Invoice 类。 很难将这两个类解耦,例如单独测试它们或替换存储机制。 通过引入 Repository 接口,我们可以实现依赖倒置:

interface InvoiceRepository {    public function findByCompanyId($companyId): Collection;}class InvoiceController {    public function __construct(private InvoiceRepository $repo) {}    public function index(int $companyId): View {        return view(            'invoices.index',            ['invoices' => $this->repo->findByCompanyId($companyId)]        );    }}class EloquentInvoiceRepository implements InvoiceRepository {    public function findByCompanyId($companyId): Collection {        // 使用 Eloquent 查询构造器实现该方法    }}

Controller 现在只依赖于 Repository 接口, 和 Repository 实现一样. 这两个类现在只依赖于一个抽象, 从而减少耦合. 正如我将在下一节中解释的那样,这会带来更多优势.

Takeaway:

存储库模式作为一种抽象类,支持依赖反转.

抽象类

存储库 提高了可读性 因为复杂的操作被具有表达性名称的高级方法隐藏了.

访问存储库的代码与底层数据访问技术分离. 如有必要,您可以切换实现,甚至可以省略实现,仅提供 Repository 接口。 这对于旨在与框架无关的库来说非常方便。

OAuth2 服务包 —— league/oauth2-server 也用到这个抽象类机制。 Laravel Passport 也通过 实现这个库的接口 集成 league/oauth2-server 包。

正如 @bdelespierre 在 评论 里回应我之前的一篇博客文章时向我指出的那样,你不仅可以切换存储库实现,还可以将它们组合在一起。大致以他的示例为基础,您可以看到一个存储库如何包装另一个存储库以提供附加功能:

interface InvoiceRepository {    public function findById(int $id): Invoice;}class InvoiceCacheRepository implements InvoiceRepository {    public function __construct(        private InvoiceRepository $repo,        private int $ttlSeconds    ) {}    public function findById(int $id): Invoice {        return Cache::remember(            "invoice.$id",            $this->ttlSeconds,            fn(): Invoice => $this->repo->findById($id)        );    }}class EloquentInvoiceRepository implements InvoiceRepository {    public function findById(int $id): Invoice { /* 从数据库中取出 $id */ }}// --- 用法:$repo = new InvoiceCacheRepository(    new EloquentInvoiceRepository(););

要点:

存储库模式抽象了有关数据访问的详细信息。存储库将客户端与数据访问技术分离。这允许切换实现,提高可读性并实现可组合性。

可测试性

存储库模式提供的抽象也有助于测试。

如果你有一个 Repository 接口,你可以提供一个替代的测试实现。 您可以使用数组支持存储库,而不是访问数据库,将所有对象保存在数组中:

class InMemoryInvoiceRepository implements InvoiceRepositoryInterface {    private array $invoices;    // implement the methods by accessing $this->invoices...    }    // --- Test Case:    $repo = new InMemoryInvoiceRepository();    $service = new InvoiceService($repo);

通过这种方法,您将获得一个现实的实现,它速度很快并且在内存中运行。 但是您必须为测试提供正确的 Repository 实现,这 ** 本身可能需要大量工作**。 在我看来,这在两种情况下是合理的:

1.您正在开发一个(与框架无关的)库,它本身不提供存储库实现。

2.测试用例复杂,Repository 的状态很重要。

另一种方法是“模仿”,要使用这种技术,你不需要适当的接口。你可以模仿任何 non-final 类。
使用 PHPUnit API ,您可以明确规定如何调用存储库以及应该返回什么。

$companyId = 42;/** @var InvoiceRepository&MockObject */$repo = $this->createMock(InvoiceRepository::class);$repo->expects($this->once())    ->method('findInvoicedToCompany')    ->with($companyId)    ->willReturn(collect([ /* invoices to return in the test case */ ]));$service = new InvoiceService($repo);$result = $service->calculateAvgInvoiceAmount($companyId);$this->assertEquals(1337.42, $result);

有了 mock,测试用例就是一个适当的单元测试。上面示例中测试的唯一代码是服务。没有数据库访问,这使得测试用例的设置和运行非常快速

另外:

仓库模式允许进行适当的单元测试,这些单元测试运行快并且是隔离的

原文地址:https://dev.to/davidrjenni/why-use-the-repository-pattern-in-laravel-2j1m

译文地址:https://learnku.com/laravel/t/62521

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

【声明:根据2013年1月30日《计算机软件保护条例》2次修订第17条规定: 为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存 储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬! 鉴于此,也希望大家按此说明研究软件!】
本站所有源码尽量保证原汁原味,如有特殊情况会作出声明及标注,网站资源不做任何二次加密(原版加密除外,不影响程序使用的不会做解密处理),方便您更好的学习参考。 在您的能力范围内,为了大环境的良性发展,请尽可能的选择正版资源。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

即刻码站__国内靠谱的站长资源下载平台 php教程 细说Laravel中存储库模式(Repository)到底有何用? https://www.jike1995.com/36036.html

常见问题
  • 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用
查看详情
  • 最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度
查看详情

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务