Á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

Thay đổi Package Name của Android Studio dể dàng với plugin APR

Nếu bạn đang gặp khó khăn hoặc bế tắc trong việc thay đổi package name trong And

Lỗi không Update Meta_Value Khi thay thế hình ảnh cũ bằng hình ảnh mới trong WordPress

Mã dưới đây hoạt động tốt có 1 lỗi không update được postmeta ” meta_key=

Bài 1 – React Native DevOps các khái niệm và các cài đặt căn bản

Hướng dẫn setup jenkins agent để bắt đầu build mobile bằng jenkins cho devloper an t

Chuyển đổi từ monolith sang microservices qua ví dụ

1. Why microservices? Microservices là kiến trúc hệ thống phần mềm hướng dịch vụ,