Command Bus Design Pattern

Cách thiết kế phổ biến nhất mình thấy khi làm việc với Laravel là mô hình MVC + Service. Service thường được thiết kế để gom các business logic lại, để ae không làm cho Controller / Model trở thành "hố đen vũ trụ". Ví dụ chúng ta sẽ có:

Controller: ProductController

Model: Product

Service: ProductService

Mô hình thiết kế này bộc lộ một số nhược điểm khi ứng dụng của chúng ta lớn dần lên:

  1. Không thể biết use-case trên hệ thống là gì. Về cơ bản thì ta biết là project có ProductService, còn muốn biết nó làm gì thì thường là sẽ phải mò vào file route, đọc code, hoặc đặt debug. Về cơ bản thì nếu business logic phức tạp thì sẽ khá oải.
  2. Khó mà đảm bảo SOLID: Controller của chúng ta thường được inject nguyên các file Service vào để xử lý. Nếu quản lý các file Service không tốt, thì dễ dẫn đến việc phụ thuộc chặt chẽ vào nhau. Nó làm tăng khả năng gây ra bug nếu có nhiều người cùng làm việc trên các file Service lớn này.

Vậy có cách nào đơn giản để giải quyết vấn đề này không ?. Project của tôi đã được triển khai như thế này, tôi cũng không có (không muốn) phải refactor nó quá nhiều hoặc áp dụng kiến trúc quá phức tạp. Câu trả lời khả dĩ là áp dụng CommandBus Design Pattern. Thay vì thiết kế logic trong các service, tôi sẽ chia logic của mình thành:

  1. Command: Một class dùng để wrap dữ liệu, yêu cầu cần thiết để thực thi một logic trên hệ thống (Data Transfer Object). Lấy ví dụ nếu action là tạo sản phẩm, tôi sẽ tạo CreateProductCommand, trong đó sẽ có các thuộc tính name được gán từ request gửi lên.
  2. Handler: Class để xử lý duy nhất một logic cụ thể. Đầu vào sẽ là command bên trên. Ví dụ CreateProductHandler sẽ nhận đầu vào là CreateProductCommand. Trong Handler có thể gọi Repository để save vào DB, hoặc gọi các Handler khác cho các action khác bên trong.
  3. CommandBus: Luồng định tuyến, ghép Command-Handler, và decorate thêm các phần chúng ta cần. Lấy ví dụ ta muốn bọc các Handler trong một Transaction, ta có thể dựa vào CommandBus

Việc áp dụng CommandBus vào Project không làm hệ thống của các bạn phải thay đổi quá nhiều, nhưng đem lại những ích lợi khá lớn:

  1. Chỉ rõ các use-case trên hệ thống
  2. Chia nhỏ logic, dễ dàng debug, tái sử dụng code
  3. Có thể dễ dàng chuyển đổi Handler trực tiếp sang Queue, Transaction nhanh chóng.

Laravel đã support sẵn CommandBus thông qua Facade Bus, ngoài ra thì có tactician của PHP League là một thư viện CommandBus rất nổi tiếng và dễ áp dụng trong Project Laravel. Nếu mọi người thấy quen, thì Event - Listener của Laravel cũng rất gần với cách thiết kế này. Về cơ bản, mình sẽ chuyển đổi Request thành các Command, và Service thành các Handler => khá dễ để áp dụng ngay vào dự án.

Một thư viện triển khai Command Bus phổ biến là tactician