Là một lập trình viên, chắc hẳn mỗi chúng ta đều không xa lạ với khái niệm Design Pattern. Đó là các mẫu thiết kế chuẩn, những khuôn mẫu cho các vấn đề chung trong thiết kế phần mềm. Trong bài viết này, mình sẽ giới thiệu một design pattern phổ biến - Specification và cách triển khai nó trong Laravel.
1. Specification Design Pattern là gì?
Specification pattern là một mẫu thiết kế, theo đó các quy tắc nghiệp vụ có thể được kết hợp lại bằng cách xâu chuỗi. Mỗi specification có một rule cần phải tuân theo. Specification cho phép chúng ta đóng gói một vài thông tin về nghiệp vụ vào trong một đơn vị code và có thể sử dụng lại chúng trong những chỗ khác nhau, giúp cho code của chúng ta tăng tính sử dụng lại và dễ đọc hiểu, dễ bảo trì hơn.
Cách tiếp cận này không chỉ giúp loại bỏ nghiệp vụ bị trùng lặp, nó cũng cho phép kết hợp nhiều các nghiệp vụ lại bởi nhiều Specification. Giúp cho việc dễ dàng cài đặt các điều kiện tìm kiếm phức tạp và kiểm tra dữ liệu. Có 3 trường hợp chính sử dụng Specification Pattern:
- Tìm kiếm dữ liệu trong DB. Tìm kiếm các bản ghi thỏa mãn với điều kiện.
- Kiểm tra các đối tượng trong bộ nhớ. Nói cách khác, kiểm tra xem một đối tượng có phù hợp với điều kiện không
- Tạo mới một thể hiện thỏa mãn điều kiện.
Giả sử ta có một bài toán truy xuất các hóa đơn và gửi chúng đến cơ quan thu nợ nếu ba điều kiện sau được thỏa mãn: (1) hóa đơn đã quá hạn, (2) thông báo quá hạn đã được gửi đi, (3) hóa đơn chưa được gửi đến cơ quan thu nợ. Ví dụ này nhằm cho thấy kết quả cuối cùng về cách các logic nghiệp vụ được 'xâu chuỗi' lại với nhau. Ta hoàn toàn có thể viết một phương thức check điều kiện có gửi hóa đơn đến cơ quan thu nợ trong lớp model Invoice. Tuy nhiên, việc viết như vậy vi phạm nguyên lý Single Responsibility trong SOLID, hơn nữa lại không thể tái sử dụng các logic nghiệp vụ đó.
Giải pháp: Ta có một lớp OverdueSpecification được được thỏa mãn khi ngày đến hạn của hóa đơn là 30 ngày trở lên, một lớp NoticeSentSpecification được thỏa mãn khi thông báo đã được gửi cho khách hàng và một lớp InCollectionSpecification được thỏa mãn khi hóa đơn đã được gửi đến cơ quan thu nợ. Sử dụng ba lớp specification này, ta tạo ra một lớp specification mới có tên SendToCollection, sẽ được thỏa mãn khi hóa đơn quá hạn, khi thông báo đã được gửi cho khách hàng và chưa được gửi đến cơ quan thu nợ.
var OverDue = new OverDueSpecification();
var NoticeSent = new NoticeSentSpecification();
var InCollection = new InCollectionSpecification(); // example of specification pattern logic chaining
var SendToCollection = OverDue.And(NoticeSent).And(InCollection.Not()); var InvoiceCollection = Service.GetInvoices(); foreach (var currentInvoice in InvoiceCollection) { if (SendToCollection.IsSatisfiedBy(currentInvoice)) { currentInvoice.SendToCollection(); }
}
2. Triển khai Specification Pattern trong Laravel
Khi truy vấn cơ sở dữ liệu, ta rất hay phải kết hợp các điều kiện truy vấn khác nhau (where, orWhere, whereIn...). Ví dụ lấy ra các jobs có company_id tương ứng, lấy ra 1 model có id tương ứng, orderBy theo cột, eager loading relationships... Mỗi điều kiện ta sẽ tạo ra một class Specification tương ứng (như CompanyIdSpecification, IdSpecification, OrderBySpecification, WithRelationsSpecification), có thể tái sử dụng ở nhiều nơi trong code.
Interface SpecificationInterface
<?php namespace App\Repositories; use Illuminate\Database\Eloquent\Builder; interface SpecificationInterface
{ public function apply(Builder $query): Builder;
}
Class AndSpecification để xâu chuỗi các Specification theo logic và
<?php namespace App\Repositories\Specification; use App\Repositories\SpecificationInterface;
use Illuminate\Database\Eloquent\Builder; class AndSpecification implements SpecificationInterface
{ /** * @var SpecificationInterface[] */ private array $listSpecs; /** * @param SpecificationInterface[] $listSpecs */ public function __construct(array $listSpecs) { $this->listSpecs = $listSpecs; } /** * @param Builder $query * @return Builder */ public function apply(Builder $query): Builder { foreach ($this->listSpecs as $specification) { $specification->apply($query); } return $query; }
}
Class OrSpecification để xâu chuỗi các Specification theo logic hoặc
<?php namespace App\Repositories\Specification; use App\Repositories\SpecificationInterface;
use Illuminate\Database\Eloquent\Builder; class OrSpecification implements SpecificationInterface
{ /** * @var SpecificationInterface[] */ private array $listSpecs; /** * @param SpecificationInterface[] $listSpecs */ public function __construct(array $listSpecs) { $this->listSpecs = $listSpecs; } /** * @param Builder $query * @return Builder */ public function apply(Builder $query): Builder { return $query->where(function ($q1) { foreach ($this->listSpecs as $specification) { $q1->orWhere(function ($q2) use ($specification) { $specification->apply($q2); }); } }); }
}
Class NoneSpecification
<?php namespace App\Repositories\Specification; use App\Repositories\SpecificationInterface;
use Illuminate\Database\Eloquent\Builder; class NoneSpecification implements SpecificationInterface
{ /** * @param Builder $query * @return Builder */ public function apply(Builder $query): Builder { return $query; }
}
Class CompanyIdSpecification
<?php namespace App\Repositories\Specification\Impl; use App\Repositories\SpecificationInterface;
use Illuminate\Database\Eloquent\Builder; class CompanyIdSpecification implements SpecificationInterface
{ /** * @var int */ private int $id; /** * @param int $id */ public function __construct(int $id) { $this->id = $id; } /** * @param Builder $query * @return Builder */ public function apply(Builder $query): Builder { return $query->where('company_id', $this->id); }
}
Class IdSpecification
<?php namespace App\Repositories\Specification\Impl; use App\Repositories\SpecificationInterface;
use Illuminate\Database\Eloquent\Builder; class IdSpecification implements SpecificationInterface
{ /** * @var int */ private int $id; /** * @param int $id */ public function __construct(int $id) { $this->id = $id; } /** * @param Builder $query * @return Builder */ public function apply(Builder $query): Builder { return $query->where('id', $this->id); }
}
Class OrderBySpecification
<?php namespace App\Repositories\Specification\Impl; use App\Repositories\SpecificationInterface;
use Illuminate\Database\Eloquent\Builder; class OrderBySpecification implements SpecificationInterface
{ /** * @var string */ private string $column; /** * @var string */ private string $order; /** * @param string $column * @param string $order */ public function __construct(string $column, string $order = 'DESC') { $this->column = $column; $this->order = $order; } /** * @param Builder $query * @return Builder */ public function apply(Builder $query): Builder { return $query->orderBy($this->column, $this->order); }
}
Class WithRelationsSpecification
<?php namespace App\Repositories\Specification\Impl; use App\Repositories\SpecificationInterface;
use Illuminate\Database\Eloquent\Builder; class WithRelationsSpecification implements SpecificationInterface
{ /** * @var array */ private array $with; /** * @param array $with */ public function __construct(array $with = []) { $this->with = $with; } /** * @param Builder $query * @return Builder */ public function apply(Builder $query): Builder { return $query->with($this->with); }
}
Sử dụng specification trong lớp controller, service hoặc repository: Ví dụ lấy ra các jobs có company_id = 15, eager loading các ứng viên của mỗi job và sắp xếp các jobs theo thời gian tạo
$specs = [ new CompanyIdSpecification(15), new WithRelationsSpecification(['candidates']), new OrderBySpecification('created_at', 'DESC'), ];
$specification = new AndSpecification($specs);
$jobs = $specification->apply(Job::query())->get();