- vừa được xem lúc

Áp dụng Specification Design Pattern trong Laravel

0 0 15

Người đăng: Vu Minh Hieu

Theo Viblo Asia

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();

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

Bình luận

Bài viết tương tự

- vừa được xem lúc

Khái niệm cơ bản về Domain Driven Design (DDD)

Lời nói đầu. Xin chào tất cả mọi người, hôm nay mình sẽ chuyển chủ đề một chút, thử sức mình với một số khái niệm cao cấp hơn và chủ đề hôm nay mình chọn là DDD - Domain Driven Design.

0 0 31

- vừa được xem lúc

Thực hành với Domain Driven Design (Phần 1)

Tại sao mình viết series này . Nhân tiện hiện tại trong dự án mình làm cũng áp dụng mô hình này nên chia sẻ trong tầm hiểu biết cho mọi người có thể hiểu rõ hơn về cách áp dụng nó nhé .

0 0 99

- vừa được xem lúc

Domain Driven Design (Phần 1)

Domain Driven Design là gì. OK? Vậy vấn đề với cách tiếp cận là gì nhỉ? Lâu này chúng ta vẫn tiếp cận theo cách này và vẫn làm tốt đấy chứ? Câu trả lời là Đúng và Sai.

0 0 31

- vừa được xem lúc

Thực hành với Domain Driven Design (Phần 2)

Chào mừng mọi người với phần 2 của series thực hành với DDD. API Get list meterials. . Controller.

0 0 13

- vừa được xem lúc

Software Architecture - Bài 1 - Domain Driven Design và Clean Architecture

Domain Driven Design (DDD) là một phương pháp thiết kế phần mềm dựa trên nền tảng của mô hình miền (domain model). Mục tiêu chính của DDD là tạo ra phần mềm hiệu quả, bền vững và dễ bảo trì bằng cách

0 0 23

- vừa được xem lúc

Tìm hiểu về Livewire trong Laravel

Giới Thiệu. Livewire là một full-stack framework cho Laravel giúp việc xây dựng các giao diện động trở nên đơn giản hơn.

0 1 217