Áp dụng Specification Design Pattern trong Laravel

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à

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.

image.png

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

<?phpnamespaceAppRepositories;useIlluminateDatabaseEloquentBuilder;interfaceSpecificationInterface{publicfunctionapply(Builder$query):Builder;}

Class AndSpecification để xâu chuỗi các Specification theo logic và

<?phpnamespaceAppRepositoriesSpecification;useAppRepositoriesSpecificationInterface;useIlluminateDatabaseEloquentBuilder;classAndSpecificationimplementsSpecificationInterface{/**
     * @var SpecificationInterface[]
     */privatearray$listSpecs;/**
     * @param  SpecificationInterface[]  $listSpecs
     */publicfunction__construct(array$listSpecs){$this->listSpecs=$listSpecs;}/**
     * @param  Builder  $query
     * @return Builder
     */publicfunctionapply(Builder$query):Builder{foreach($this->listSpecsas$specification){$specification->apply($query);}return$query;}}

Class OrSpecification để xâu chuỗi các Specification theo logic hoặc

<?phpnamespaceAppRepositoriesSpecification;useAppRepositoriesSpecificationInterface;useIlluminateDatabaseEloquentBuilder;classOrSpecificationimplementsSpecificationInterface{/**
     * @var SpecificationInterface[]
     */privatearray$listSpecs;/**
     * @param  SpecificationInterface[]  $listSpecs
     */publicfunction__construct(array$listSpecs){$this->listSpecs=$listSpecs;}/**
     * @param  Builder  $query
     * @return Builder
     */publicfunctionapply(Builder$query):Builder{return$query->where(function($q1){foreach($this->listSpecsas$specification){$q1->orWhere(function($q2)use($specification){$specification->apply($q2);});}});}}

Class NoneSpecification

<?phpnamespaceAppRepositoriesSpecification;useAppRepositoriesSpecificationInterface;useIlluminateDatabaseEloquentBuilder;classNoneSpecificationimplementsSpecificationInterface{/**
     * @param  Builder  $query
     * @return Builder
     */publicfunctionapply(Builder$query):Builder{return$query;}}

Class CompanyIdSpecification

<?phpnamespaceAppRepositoriesSpecificationImpl;useAppRepositoriesSpecificationInterface;useIlluminateDatabaseEloquentBuilder;classCompanyIdSpecificationimplementsSpecificationInterface{/**
     * @var int
     */privateint$id;/**
     * @param  int  $id
     */publicfunction__construct(int$id){$this->id=$id;}/**
     * @param  Builder  $query
     * @return Builder
     */publicfunctionapply(Builder$query):Builder{return$query->where('company_id',$this->id);}}

Class IdSpecification

<?phpnamespaceAppRepositoriesSpecificationImpl;useAppRepositoriesSpecificationInterface;useIlluminateDatabaseEloquentBuilder;classIdSpecificationimplementsSpecificationInterface{/**
     * @var int
     */privateint$id;/**
     * @param  int  $id
     */publicfunction__construct(int$id){$this->id=$id;}/**
     * @param  Builder  $query
     * @return Builder
     */publicfunctionapply(Builder$query):Builder{return$query->where('id',$this->id);}}

Class OrderBySpecification

<?phpnamespaceAppRepositoriesSpecificationImpl;useAppRepositoriesSpecificationInterface;useIlluminateDatabaseEloquentBuilder;classOrderBySpecificationimplementsSpecificationInterface{/**
     * @var string
     */privatestring$column;/**
     * @var string
     */privatestring$order;/**
     * @param  string  $column
     * @param  string  $order
     */publicfunction__construct(string$column,string$order='DESC'){$this->column=$column;$this->order=$order;}/**
     * @param  Builder  $query
     * @return Builder
     */publicfunctionapply(Builder$query):Builder{return$query->orderBy($this->column,$this->order);}}

Class WithRelationsSpecification

<?phpnamespaceAppRepositoriesSpecificationImpl;useAppRepositoriesSpecificationInterface;useIlluminateDatabaseEloquentBuilder;classWithRelationsSpecificationimplementsSpecificationInterface{/**
     * @var array
     */privatearray$with;/**
     * @param  array  $with
     */publicfunction__construct(array$with=[]){$this->with=$with;}/**
     * @param  Builder  $query
     * @return Builder
     */publicfunctionapply(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=[newCompanyIdSpecification(15),newWithRelationsSpecification(['candidates']),newOrderBySpecification('created_at','DESC'),];$specification=newAndSpecification($specs);$jobs=$specification->apply(Job::query())->get();

3. Tài liệu tham khảo

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ