关注【索引目录】服务号,更多精彩内容等你来探索!
存储库模式 - 基础知识
在存储库模式中,我们通常有少数 CRUD 方法:
class ProductRepository
{
public function create(array $data) { /* ... */ }
public function update(Product $product, array $data) { /* ... */ }
public function delete(Product $product) { /* ... */ }
public function find(int $id): ?Product { /* ... */ }
public function all(): Collection { /* ... */ }
}
这听起来简洁明了。但在实际项目中,事情很少就此结束。虽然这些方法涵盖了众多场景,但开发人员通常会更进一步——使用专门针对特定业务需求的方法来扩展存储库。
存储库模式开始崩溃的地方
迟早,你需要获取“活跃产品”、“按类别分类的产品”或“按商店分类的产品”。通常会发生什么?人们开始在同一个类中堆叠一个又一个方法:
class ProductRepository
{
// base methods
public function create(array $data) { /* ... */ }
public function update(Product $product, array $data) { /* ... */ }
public function delete(Product $product) { /* ... */ }
public function find(int $id): ?Product { /* ... */ }
public function all(): Collection { /* ... */ }
// new fetch methods
public function getActiveProducts() {
return Product::where('active', 1)->get();
}
public function getProductsByCategory(int $categoryId) {
return Product::where('category_id', $categoryId)->get();
}
public function getActiveProductsByCategory(int $categoryId) {
return Product::where('active', 1)
->where('category_id', $categoryId)
->get();
}
public function getProductsByStore(int $storeId) {
return Product::where('store_id', $storeId)->get();
}
public function getActiveProductsByStore(int $storeId) {
return Product::where('active', 1)
->where('store_id', $storeId)
->get();
}
// and so on...
}
这导致存储库包含数十种方法,违反了该模式的初衷。
存储库困境
如果开发人员坚持使用存储库模式原有的 5 种方法,他们就会将逻辑推到服务、助手和其他类中。这会将逻辑分散到多个类中,并掩盖单一事实来源。重复的情况会where('active', 1)蔓延到各个地方,使代码更难以导航和维护。
存储库中参数的作用
另一个重要方面是,存储库通常依赖参数来决定要获取哪些数据。理论上,这可以保持灵活性。但在实践中,便利性通常更重要——开发人员会创建以特定查询命名的专用方法(例如getActiveProductsByStore)。虽然更容易记忆,但这种方法很快就会导致重复和不必要的代码臃肿。因此,参数是创建新方法的决策者。
原子查询构造(AQC)如何解决问题
AQC 通过将职责分解为专注的专用类来应对这些挑战。每个 AQC 类都接受参数,动态构建查询并返回结果。AQC 不会将查询分散到多个存储库方法或服务类中,而是将它们集中在一个结构化的位置。
思维方式也发生了转变:无需创建数十个硬编码方法,只需在一个类中定义所有可能的条件。然后,查询会根据提供的参数动态构建,从而保持灵活性、一致性,并且更易于维护。
因此,我们现在不再有具有 5 种方法的单个类,而是有 5 个类,每个类都有一个handle()方法。
└── AQC/
└── Product/
└── CreateProduct.php
└── UpdateProduct.php
└── GetProducts.php
└── GetProduct.php
└── DeleteProduct.php
以GetProducts课程为例:
namespace App\AQC\Product;
use App\Models\Product;
class GetProducts
{
public static function handle($params = [], $paginate = true, $scenario = 'default')
{
$productObj = Product::latest('id');
//apply only when requested
if (isset($params['active'])) {
$productObj->where('active', $params['active']);
}
// apply only when requested
if (isset($params['store_id'])) {
$productObj->where('store_id', $params['store_id']);
}
// add more conditions for different use cases
switch ($scenario) {
case 'minimal':
$productObj->select(['id', 'name']);
break;
case 'compact':
$productObj->select(['id', 'name', 'price', 'image']);
break;
case 'admin':
$productObj->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
break;
default:
$productObj->select('*');
}
return $paginate
? $productObj->paginate(Product::PAGINATE)
: $productObj->get();
}
}
每个条件都是可选的,并且仅在相应参数存在时才适用。结果是一个可以处理多种场景的类。
现在,您不必每次都编写新方法,只需调用:
// Get active products
$products = GetProducts::handle(['active' => true]);
// Get category products
$products = GetProducts::handle(['category_id' => 5]);
// Get store products
$products = GetProducts::handle(['store_id' => 12]);
同一种方法可以处理所有情况。
灵活性:AQC 的杀手锏
这就是 AQC 真正闪耀的地方。AQC 无需为每个场景添加新方法,而是依靠参数——每个参数都能带来指数级的查询灵活性。
例如,一开始只有一个条件“活跃”。您可以获取活跃产品。添加store_id,您便可以按商店获取产品,或按商店获取活跃产品。引入category_id,现在您可以进行混合搭配:按类别获取产品、按类别获取活跃产品、按特定商店获取类别获取活跃产品等等。添加brand_id,组合数量将进一步增加——所有这些都无需编写任何额外的方法。
在传统的存储库中,每种场景通常都需要专门的方法。而对于 AQC 来说,它只是另一个参数而已。
namespace App\AQC\Product;
use App\Models\Product;
class GetProducts
{
public static function handle($params = [], $paginate = true, $scenario = 'default')
{
$productObj = Product::latest('id');
// apply only when requested
if (isset($params['active'])) {
$productObj->where('active', $params['active']);
}
// apply only when requested
if (isset($params['store_id'])) {
$productObj->where('store_id', $params['store_id']);
}
// apply only when requested
if (isset($params['category_id'])) {
$productObj->where('category_id', $params['category_id']);
}
// apply only when requested
if (isset($params['brand_id'])) {
$productObj->where('brand_id', $params['brand_id']);
}
// add more conditions for different use cases
switch ($scenario) {
case 'minimal':
$productObj->select(['id', 'name']);
break;
case 'compact':
$productObj->select(['id', 'name', 'price', 'image']);
break;
case 'admin':
$productObj->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
break;
default:
$productObj->select('*');
}
return $paginate
? $productObj->paginate(Product::PAGINATE)
: $productObj->get();
}
}
以下是它们的称呼。
// Get active products
$products = GetProducts::handle(['active' => true]);
// Get category products
$products = GetProducts::handle(['category_id' => 5]);
// Get store products
$products = GetProducts::handle(['store_id' => 12]);
// Get category products but active
$products = GetProducts::handle(['category_id' => 5, 'active' => true]);
// Get store products but active
$products = GetProducts::handle(['store_id' => 12, 'active' => true]);
// Get Store products but of only specific category and must be active
$products = GetProducts::handle(['store_id' => 12, 'category_id' => 5, 'active' => true]);
这是乘法效应:
这意味着每个新条件都会增加灵活性,而无需增加方法。在存储库中,您必须为每种组合编写一个新方法,这简直是一场噩梦。
但是您可以在存储库中执行相同操作吗?
是的,当然可以在存储库模式中实现相同的模式。让我们看看如何实现。
<?php
namespace App\Repositories;
use App\Models\Product;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class ProductRepository
{
/**
* Fetch products with dynamic filters and scenarios.
*/
public function findAll(array $params = [], bool $paginate = true, string $scenario = 'default')
{
$query = Product::latest('id');
// Apply filters only when present
if (isset($params['active'])) {
$query->where('active', $params['active']);
}
if (isset($params['store_id'])) {
$query->where('store_id', $params['store_id']);
}
if (isset($params['category_id'])) {
$query->where('category_id', $params['category_id']);
}
if (isset($params['brand_id'])) {
$query->where('brand_id', $params['brand_id']);
}
// Add more conditions as needed...
// Handle scenarios (projection logic)
switch ($scenario) {
case 'minimal':
$query->select(['id', 'name']);
break;
case 'compact':
$query->select(['id', 'name', 'price', 'image']);
break;
case 'admin':
$query->select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
break;
default:
$query->select('*');
}
return $paginate
? $query->paginate(Product::PAGINATE)
: $query->get();
}
/**
* Find a single product by ID.
*/
public function find(int $id): Product
{
return Product::findOrFail($id);
}
/**
* Create a new product.
*/
public function create(array $data): Product
{
return Product::create($data);
}
/**
* Update an existing product.
*/
public function update(int $id, array $data): Product
{
$product = $this->find($id);
$product->update($data);
return $product;
}
/**
* Delete a product by ID.
*/
public function delete(int $id): bool
{
$product = $this->find($id);
return $product->delete();
}
}
以下是我们的使用方法。
$productRepo = new \App\Repositories\ProductRepository();
// Get active products
$products = $productRepo->findAll(['active' => true]);
// Get category products
$products = $productRepo->findAll(['category_id' => 5]);
// Get store products
$products = $productRepo->findAll(['store_id' => 12]);
// Get category products but active
$products = $productRepo->findAll(['category_id' => 5, 'active' => true]);
// Get store products but active
$products = $productRepo->findAll(['store_id' => 12, 'active' => true]);
// Get Store products but of only specific category and must be active
$products = $productRepo->findAll(['store_id' => 12, 'category_id' => 5, 'active' => true]);
虽然我们已经做到了这一点,但在存储库类方法中定义它会使类变得非常庞大,有时我们可能需要滚动。与findAll()类似 ,其他方法也可能具有相同的 where 条件,从而使类变得庞大。
我们必须定义所有 Where 条件吗?
这里可能会出现一个问题。我们是否需要提前将所有列定义为可选的 where 条件?
答案很简单:不。我们只会在系统中真正需要的时候才写条件。如果我们不需要折扣商品,就不会写这个条件。
但是当我们确实添加一个时,我们添加一次并立即解锁所有组合。
除了 GetProducts 之外:其他操作
AQC 不仅限于获取查询。考虑其他操作:
- DeleteProduct
:按 id(单个记录)或category_id(多个记录)删除。 - UpdateProduct
:根据角色、上下文或参数有条件地应用更新。 - InsertProduct
:即使插入也可以根据参数而变化 - 例如,根据用户角色保存不同的数据集。
在每种情况下,由参数驱动的可选条件使类既灵活又易于维护。
存储库与 AQC – 平衡的观点
值得注意的是,AQC 的一些优势可以在精心设计的存储库中得到体现。理论上,你可以在存储库方法中使用参数构建灵活的查询。但在实践中:
-
存储库通常会变得太大(神级)。 -
或者它们分裂成多个服务/助手,分散逻辑。
AQC 通过将逻辑提取到小型、专用、参数驱动的类中,并将它们分组到公共命名空间下,从而避免了这两种极端情况。这使得代码库更简洁、更易读、更易于维护。
AQC 摘要
-
无重复——每个条件只需写一次,而不是分散在多个方法中。 -
随着复杂性而扩展——添加一个条件即可解锁指数组合。 -
单一事实来源——所有产品获取逻辑都保留在一个类中。 -
更清晰的 API – 消费者只需调用 handle()并传递参数。
Repository 与 AQC:快速比较
最后的想法
归根结底,我并不认为 AQC 会取代存储库模式——我认为它是一种进化。存储库模式非常适合 CRUD 和简单的查找,但随着实际场景的增多,它开始变得捉襟见肘。AQC 为我们提供了这种缺失的灵活性,无需将类炸成一堆杂乱的方法。一个方法,多个条件,无限的组合。这正是我在项目中追求的控制力和清晰度。
关注【索引目录】服务号,更多精彩内容等你来探索!

