0

Đừng new vô tội vạ nữa! Cách Service Provider thay đổi tư duy thiết kế hệ thống của tôi.

Chào anh em, lại là mình đây!

Dạo gần đây, có một bạn junior trong team review code với mình và thắc mắc: "Anh ơi, sao framework này chỗ nào cũng thấy Provider, Provider? Tại sao mình không new luôn cái class ra dùng cho lẹ mà phải đi đường vòng chi cho mệt?".

Câu hỏi này làm mình nhớ lại chính mình của 5 năm về trước. Lúc đó, mình cũng từng nghĩ như vậy, cho đến khi dự án phình to và mình phải trả một cái giá cực đắt. Hôm nay, rảnh rỗi ngồi nhâm nhi ly cà phê, mình quyết định viết bài này để "bóc phốt" chính mình ngày xưa, đồng thời chia sẻ với anh em về một pattern đã thay đổi hoàn toàn tư duy code của mình: Service Provider Pattern.

1. Mở bài: Đêm định mệnh và mớ code "bốc mùi"

Đó là một đêm thứ Sáu, 11h đêm, lẽ ra giờ đó mình đang ngồi quẩy rank, thì lại phải hì hục fix bug trên production. Dự án lúc đó là một hệ thống e-commerce tự build (hồi trẻ trâu thích tự làm bánh xe cơ).

Yêu cầu cực kỳ đơn giản: Thay đổi dịch vụ gửi Email từ SMTP nội bộ sang AWS SES.

Vấn đề là gì? Ở thời điểm đó, mình theo trường phái "bạ đâu new đó". Bất cứ khi nào cần gửi email (đăng ký, quên mật khẩu, hóa đơn...), mình đều gọi:

$mailer = new SmtpMailer('host', 'port', 'user', 'pass');
$mailer->send($to, $content);

Và bùm! Class SmtpMailer bị nằm rải rác ở 42 file khác nhau. Mình phải đi tìm từng file một để đổi thành AwsSesMailer, chưa kể cấu hình của AWS SES hoàn toàn khác, nó cần API Key, Secret Key chứ không phải host/port. Đêm đó mình đã "Find and Replace" trong tuyệt vọng, sửa sót 2 chỗ, và kết quả là cuối tuần đó sếp gọi cháy máy vì user không nhận được email confirm đơn hàng.

Đó là lúc mình nhận ra: Code của mình đang bị "Tight Coupling" (kết dính chặt) một cách trầm trọng. Mình cần một người "quản gia" đứng ra lo liệu việc khởi tạo này.

2. Hành trình giác ngộ: Service Provider là cái quái gì?

Sau cú ngã sấp mặt đó, mình bắt đầu cày cuốc về Design Pattern và Architecture. Và mình va phải Service Provider (đặc biệt nổi tiếng trong hệ sinh thái Laravel, hoặc Angular ở phía FE).

Nói một cách dân dã, Service Provider giống như một nhà cung cấp dịch vụ (nhà thầu). Thay vì bạn (Controller, Model) phải tự tay đi mua gạch, mua xi măng, tự trộn vữa (khởi tạo dependencies, truyền config)... thì bạn chỉ cần ra lệnh: "Ê nhà thầu, xây cho tôi cái nhà vệ sinh!". Thằng Service Provider sẽ biết chính xác cần gọi ai, cấu hình thế nào để trả về cho bạn một cái nhà vệ sinh hoàn chỉnh.

Nó là trung tâm khởi tạo (bootstrap) mọi thứ trong ứng dụng của bạn.

Cấu trúc kinh điển của một Service Provider thường có 2 hàm chính:

  • register(): Dùng để đăng ký các service vào Service Container (chỉ đăng ký cách tạo ra nó, chứ chưa chạy).
  • boot(): Chạy sau khi tất cả các service đã được register xong. Thường dùng để setup event listeners, routes, hoặc những thứ cần các service khác đã sẵn sàng.

Giải pháp cho đống code cũ của mình:

Mình thiết lập một MailServiceProvider. Ở hàm register, mình dạy cho hệ thống biết: Từ nay, hễ ai đòi interface MailerInterface, hãy đưa cho nó thằng AwsSesMailer đã được cấu hình sẵn.

interface MailerInterface {
    public function send($to, $content);
}

class MailServiceProvider extends ServiceProvider 
{
    public function register() 
    {
        // Đăng ký binding vào Container
        $this->app->singleton(MailerInterface::class, function ($app) {
            $config = $app['config']->get('mail.aws_ses');
            return new AwsSesMailer($config['api_key'], $config['secret']);
        });
    }
}

Từ đó trở đi, ở các file xử lý nghiệp vụ, mình chỉ việc dùng Dependency Injection (DI):

class OrderController 
{
    protected $mailer;

    // Chỉ cần đòi hỏi MailerInterface, Container sẽ nhờ Service Provider cấp phát đúng hàng
    public function __construct(MailerInterface $mailer) 
    {
        $this->mailer = $mailer;
    }

    public function checkout() 
    {
        // ... xử lý logic ...
        $this->mailer->send('user@viblo.asia', 'Cảm ơn bạn đã mua hàng!');
    }
}

Sự kỳ diệu xảy ra: Nếu sếp lại đổi ý, muốn dùng Mailgun thay vì AWS SES? Dễ ợt! Mình chỉ cần sửa duy nhất 1 dòng trong file MailServiceProvider. Controller không hề biết, và không cần biết sự thay đổi đó. Quá sướng!

3. Bài học rút ra (The Takeaways)

Sau nhiều dự án từ nhỏ đến lớn, đây là những "trái ngọt" mà Service Provider mang lại cho mình:

  1. Centralized Configuration: Tất cả logic khởi tạo phức tạp, đọc config, setup credentials đều nằm ở một nơi. Code logic nghiệp vụ (business logic) trở nên sạch sẽ và thuần khiết.
  2. Decoupling (Lỏng lẻo hóa): Các module không còn phụ thuộc cứng (hard-code) vào nhau nữa. Việc thay thế 3rd-party services trở thành chuyện nhỏ
  3. Dễ Test vô cùng: Vì Controller chỉ nhận Interface, khi viết Unit Test, mình có thể dễ dàng mock ra một DummyMailer thông qua Provider để chạy test mà không cần gửi email thật.

Lời kết

Service Provider không phải là một viên đạn bạc (silver bullet) cho mọi bài toán. Với những dự án siêu nhỏ, việc set up Container và Provider có thể hơi "over-engineering" (chết chìm trong thiết kế). Nhưng một khi dự án có dấu hiệu scale up, nó là một chiếc phao cứu sinh tuyệt vời giúp bạn quản lý dependency dễ thở hơn rất nhiều.

Đừng để đến lúc fix bug lúc 3h sáng như mình rồi mới nhận ra giá trị của nó nhé anh em!

Còn anh em thì sao? Trong các project hiện tại (không giới hạn ngôn ngữ hay framework), anh em đang dùng cách nào để quản lý việc khởi tạo và cấu hình các third-party services? Cùng comment chia sẻ bên dưới để chúng ta cùng học hỏi 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í