+3

Series Thiết kế Kiến trúc Modules Thủ công | Bài 3: Nghệ thuật Giao tiếp chéo - Giữ code lỏng lẻo (Loose Coupling)

Chào anh em! Khởi tạo Module bằng lệnh custom ở Bài 2 thì dễ, nhưng làm sao để các Module này "nói chuyện" được với nhau mới là thước đo đẳng cấp của một Backend Engineer.

Hãy thử tưởng tượng: Trong Module Order, sau khi tạo đơn hàng thành công, bạn cần trừ số lượng sản phẩm trong kho (thuộc Module Inventory).

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

// Bad Practice
$inventory = \Modules\Inventory\Models\Product::find($productId);
$inventory->decrement('stock', $quantity);

Bùm! Bạn vừa tự tay bóp chết kiến trúc Modular. Module Order lúc này đã bị trói chặt (tightly coupled) vào Module Inventory. Nếu ngày mai, database của Inventory thay đổi, hoặc bạn muốn tách Inventory ra thành một Microservice riêng, Module Order của bạn sẽ "chết đứng".

Hôm nay, chúng ta sẽ xem xét 2 vũ khí tối thượng để giải quyết bài toán giao tiếp chéo này trong môi trường Module thủ công.

1. Giải pháp Đồng bộ (Synchronous): Service Contracts (Hợp đồng)

Nếu Module Order cần nhận lại kết quả ngay lập tức từ Inventory (ví dụ: phải kiểm tra xem kho còn hàng không thì mới cho đặt), chúng ta sẽ bắt chúng giao tiếp qua một Interface.

Bước 1: Tạo một "Vùng đất chung" (Core/Shared Module) Trong kiến trúc thủ công, chúng ta nên có một module Core hoặc thư mục Contracts chung mà mọi module khác đều có quyền truy cập.

// modules/Core/Contracts/InventoryServiceInterface.php
namespace Modules\Core\Contracts;

interface InventoryServiceInterface
{
    public function checkAndDeductStock(int $productId, int $quantity): bool;
}

Bước 2: Implement (Thực thi) Hợp đồng tại Module Inventory Module Inventory sẽ chịu trách nhiệm viết logic thực tế và đăng ký với Laravel Container thông qua Provider của chính nó.

// modules/Inventory/Services/InventoryService.php
namespace Modules\Inventory\Services;

use Modules\Core\Contracts\InventoryServiceInterface;

class InventoryService implements InventoryServiceInterface
{
    public function checkAndDeductStock(int $productId, int $quantity): bool
    {
        // Logic truy vấn db, trừ kho phức tạp ở đây...
        return true;
    }
}

Trong file InventoryServiceProvider.php, dặn dò Laravel:

public function register()
{
    $this->app->bind(
        \Modules\Core\Contracts\InventoryServiceInterface::class,
        \Modules\Inventory\Services\InventoryService::class
    );
}

Bước 3: Sử dụng tại Module Order (Dependency Injection) Giờ đây, OrderController chỉ cần chọc vào Interface, hoàn toàn mù tịt về logic bên trong của Inventory.

namespace Modules\Order\Http\Controllers;

use Modules\Core\Contracts\InventoryServiceInterface;

class OrderController extends Controller
{
    public function store(Request $request, InventoryServiceInterface $inventory)
    {
        // Kiểm tra và trừ kho thông qua Interface
        if (!$inventory->checkAndDeductStock($request->product_id, $request->qty)) {
            return response()->json(['error' => 'Hết hàng!'], 400);
        }
        
        // Tạo order...
    }
}

Lưu ý sinh tử trong config/app.php: Vì Order phụ thuộc vào Interface do Inventory cung cấp, anh em phải đảm bảo thứ tự load Providers trong config/app.php thật chuẩn. InventoryServiceProvider nên được khai báo TRƯỚC OrderServiceProvider để tránh lỗi "Target binding not found".

2. Giải pháp Bất đồng bộ (Asynchronous): Kiến trúc Event-Driven

Nếu hành động không cần kết quả trả về ngay (ví dụ: gửi Email thông báo, cộng điểm tích lũy cho Customer), đừng bắt người dùng phải chờ. Hãy dùng Event.

Trong kiến trúc Module thủ công, việc sử dụng Event rất dễ vì chúng ta đang dùng chung vùng nhớ (RAM) của Laravel.

Bước 1: Khai báo Event tại Module Order

// modules/Order/Events/OrderCreated.php
namespace Modules\Order\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderCreated
{
    use Dispatchable, SerializesModels;

    public $order;

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

Tại OrderController, khi tạo xong đơn hàng, chỉ cần "bắn" tín hiệu:

\Modules\Order\Events\OrderCreated::dispatch($order);

Bước 2: Lắng nghe Event tại Module Customer

Module Customer sẽ có một Listener để hứng sự kiện này và cộng điểm.

// modules/Customer/Listeners/AddRewardPoints.php
namespace Modules\Customer\Listeners;

class AddRewardPoints
{
    public function handle($event)
    {
        $order = $event->order;
        // Logic cộng điểm cho Customer dựa vào tổng tiền của Order...
    }
}

Bước 3: Đăng ký Event & Listener

Anh em có thể đăng ký chúng ngay trong CustomerServiceProvider bằng cách gọi Event Facade ở hàm boot():

// modules/Customer/Providers/CustomerServiceProvider.php
use Illuminate\Support\Facades\Event;
use Modules\Order\Events\OrderCreated;
use Modules\Customer\Listeners\AddRewardPoints;

public function boot()
{
    // ... load routes, views...
    
    Event::listen(OrderCreated::class, AddRewardPoints::class);
}

Bằng cách này, Module Order chỉ làm đúng việc của nó là tạo đơn hàng rồi "hét lên". Còn việc cộng điểm, trừ kho, gửi mail do ai làm thì nó không cần quan tâm. Mã nguồn đạt được sự độc lập tối đa!

Tổng kết Series

Xây dựng kiến trúc Module thủ công (Zero Dependency) là một bài toán đánh đổi. Anh em phải tự tay viết lệnh sinh code, tự quản lý thứ tự Provider, tự thiết lập hợp đồng giao tiếp (Contracts/Events). Tuy nhiên, thành quả nhận lại là một hệ thống cực kỳ "nhẹ gánh", minh bạch 100% trong luồng thực thi và không bao giờ sợ các package bên thứ ba bị "đem con bỏ chợ" khi Laravel ra phiên bản mới.

Sự khắt khe trong việc chia tách logic này chính là bước đệm hoàn hảo. Sau này, nếu hệ thống phình to đến mức phải áp dụng Microservices (ví dụ: dời module Inventory sang một con server khác chạy C++ hay Golang để tính toán tồn kho theo thời gian thực), anh em chỉ việc thay thế Laravel Event bằng Message Broker (Kafka/RabbitMQ) là xong, cấu trúc thư mục và logic nghiệp vụ gần như không suy suyển.

Chúc anh em áp dụng thành công và làm chủ được mọi ngóc ngách của Laravel!


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í