+2

Series Cấu trúc Modules trong Laravel | Bài 3: Nghệ thuật giao tiếp giữa các Modules - Không để code "dính chùm"

Chào anh em, ở Bài 2 chúng ta đã dùng nwidart/laravel-modules để phân lô bán nền, chia project thành các Modules độc lập như Order, Payment, Inventory...

Tuy nhiên, trong thực tế nghiệp vụ, các module này không thể sống cô lập. Giả sử hệ thống của chúng ta là hệ thống bán vé tự động (AFC - Automatic Fare Collection) cho tuyến Metro. Khách hàng quẹt thẻ mua vé (thuộc Module Order). Sau khi mua thành công, hệ thống phải ghi nhận thanh toán (Module Payment) và gửi tín hiệu mở cổng kiểm soát hoặc đồng bộ dữ liệu vé xuống trạm (Module AFC/Station).

Nếu trong OrderController, bạn viết dòng code này:

// Bad Practice (High Coupling)
$paymentModel = new \Modules\Payment\Entities\Payment();
$paymentModel->process($order);

Xin chúc mừng, bạn vừa tự tay phá nát kiến trúc Modular! Lúc này Module Order đã bị "trói chặt" (tight coupling) vào Module Payment. Nếu cấu trúc database của Payment thay đổi, Order sẽ chết theo.

Trong bài này, chúng ta sẽ học cách gỡ rối vấn đề này bằng 2 phương pháp chuẩn kỹ sư: Service Contracts và Event-Driven Architecture.

1. Phương pháp 1: Giao tiếp qua Interface (Service Contracts)

Thay vì Module Order gọi trực tiếp Class của Module Payment, chúng ta sẽ ép chúng giao tiếp qua một "Bản hợp đồng" (Interface).

Bước 1: Tạo một thư mục chứa Hợp đồng chung (Core/Contracts)

Bạn có thể tạo một Module tên là Core hoặc Shared, nơi chứa các Interfaces mà mọi module khác đều biết.

// Modules/Core/Contracts/PaymentServiceInterface.php
namespace Modules\Core\Contracts;

interface PaymentServiceInterface
{
    public function processPayment(int $orderId, float $amount): bool;
}

Bước 2: Implement hợp đồng tại Module Payment

Module Payment sẽ cam kết thực hiện đúng hợp đồng này.

// Modules/Payment/Services/PaymentService.php
namespace Modules\Payment\Services;
use Modules\Core\Contracts\PaymentServiceInterface;

class PaymentService implements PaymentServiceInterface
{
    public function processPayment(int $orderId, float $amount): bool
    {
        // Xử lý logic trừ tiền phức tạp ở đây...
        return true;
    }
}

Sau đó bind interface này trong PaymentServiceProvider:

$this->app->bind(PaymentServiceInterface::class, PaymentService::class);

Bước 3: Sử dụng tại Module Order Lúc này, Module Order chỉ cần biết đến cái Interface, hoàn toàn mù tịt về logic bên trong của Payment.

// Modules/Order/Http/Controllers/OrderController.php
use Modules\Core\Contracts\PaymentServiceInterface;

class OrderController extends Controller
{
    public function checkout(Request $request, PaymentServiceInterface $paymentService)
    {
        // Tạo order...
        $status = $paymentService->processPayment($order->id, 100000);
    }
}

Ưu điểm: Code sạch, dễ test (Mocking). Nhưng nó vẫn là giao tiếp Đồng bộ (Synchronous).

2. Phương pháp 2: Kiến trúc Hướng Sự Kiện (Event-Driven)

Đây là "trùm cuối" của giao tiếp lỏng (Loosely Coupled), đặc biệt hiệu quả khi một hành động kích hoạt nhiều tác vụ khác nhau. Nó chính là tư duy nền tảng của các Message Broker (như Kafka hay RabbitMQ) nhưng được thực hiện ngay trong nội bộ bộ nhớ của Laravel.

Khi một vé được bán thành công, Module Order không cần quan tâm ai sẽ xử lý tiếp theo. Nó chỉ đơn giản là "cầm loa hét lên": "Ê, tao vừa tạo xong một cái vé mới nhé!" (Bắn Event).

Bất kỳ Module nào quan tâm (như Payment, AFC, Notification) sẽ tự động lắng nghe (Listen) và xử lý phần việc của mình.

Bước 1: Khai báo Event tại Module Order (hoặc Core)

// Modules/Order/Events/TicketPurchased.php
namespace Modules\Order\Events;
use Illuminate\Foundation\Events\Dispatchable;

class TicketPurchased
{
    use Dispatchable;
    public $ticketData;

    public function __construct($ticketData)
    {
        $this->ticketData = $ticketData;
    }
}

Bước 2: Phát (Dispatch) sự kiện khi xong việc

// Modules/Order/Http/Controllers/OrderController.php
public function store(Request $request)
{
    $ticket = Order::create($request->all());
    
    // Bắn sự kiện ra toàn hệ thống
    \Modules\Order\Events\TicketPurchased::dispatch($ticket);
    
    return response()->json(['msg' => 'Mua vé thành công']);
}

Bước 3: Lắng nghe sự kiện tại các Module khác

Bây giờ, tại Module AFC, chúng ta tạo một Listener để hứng sự kiện này và đồng bộ dữ liệu xuống thiết bị phần cứng tại trạm.

// Modules/AFC/Listeners/SyncTicketToStation.php
namespace Modules\AFC\Listeners;
use Modules\Order\Events\TicketPurchased;

class SyncTicketToStation
{
    public function handle(TicketPurchased $event)
    {
        $ticket = $event->ticketData;
        // Logic gọi API hoặc đẩy data xuống thiết bị đọc thẻ tại trạm (Gate/TVM)
        Log::info("Đã đồng bộ vé {$ticket->id} xuống trạm.");
    }
}

Đăng ký Listener này trong EventServiceProvider của Module AFC. Như vậy, Module AFC hoạt động hoàn toàn độc lập, và Module Order cũng không bao giờ sợ lỗi nếu API của trạm bị rớt mạng. Nếu kết hợp Event với tính năng Queues của Laravel (đẩy vào Redis/RabbitMQ), bạn đã có một hệ thống xử lý bất đồng bộ hoàn hảo!

Tổng kết

Việc chọn Giao tiếp qua Interface (Contracts) hay Sự kiện (Events) phụ thuộc vào nghiệp vụ.

  • Dùng Contracts khi bạn cần nhận lại kết quả ngay lập tức (Synchronous) - ví dụ: kiểm tra số dư.
  • Dùng Events khi bạn muốn thông báo một trạng thái đã thay đổi và xử lý bất đồng bộ (Asynchronous) - ví dụ: gửi email, đồng bộ dữ liệu trạm.

Cả hai cách này đều giúp các Module của bạn có ranh giới rõ ràng. Sau này, nếu công ty yêu cầu tách hệ thống AFC ra thành một Microservice riêng biệt viết bằng Golang, bạn chỉ cần thay Laravel Event bằng Kafka/RabbitMQ là hệ thống gần như sẵn sàng để "cất cánh"!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí