細說Laravel中存儲庫模式(Repository)到底有何用?

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

laravel中的存儲庫模式為什么在Laravel中使用存儲庫模式?

在我 上一篇文章 中,我解釋了什么是存儲庫模式,它與active record模式有何不同,以及如何在laravel中實現它。現在我想深入了解一下為什么應該使用存儲庫模式。

我在上一篇文章的評論中注意到,Repository模式在Laravel社區中是一個有爭議的話題。有些人認為沒有理由使用它,并堅持使用內置的Active Record模式。其他人則傾向于使用其他方法將數據訪問從邏輯域中分離出來。請注意,我尊重這些意見,并將在接下來的博客文章中專門討論此主題。

有了這個免責聲明,讓我們來了解一下使用存儲庫模式的優點。【相關教程推薦:上一篇文章

單一責任原則

單一責任原則是主要鑒別器來區分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

? 版權聲明
THE END
喜歡就支持一下吧
點贊8 分享