Thanh toán Đa kênh với Strategy Pattern

· 4 min read

Xử lý nhiều cổng thanh toán (Stripe, PayPal, Momo) thường dẫn đến code điều kiện lộn xộn:

if ($type === 'stripe') {
    // logic stripe
} elseif ($type === 'paypal') {
    // logic paypal
}

Strategy Pattern giải quyết vấn đề này bằng cách định nghĩa một họ các thuật toán (phương thức thanh toán), đóng gói từng cái, và làm cho chúng có thể thay thế lẫn nhau.

1. Interface

Định nghĩa một hợp đồng (contract) mà tất cả các cổng thanh toán phải tuân theo.

namespace App\Contracts;

interface PaymentGateway
{
    public function charge(float $amount, array $details): string; // Trả về Transaction ID
    public function refund(string $transactionId): bool;
}

2. Concrete Strategies

Triển khai interface cho từng nhà cung cấp.

Stripe Strategy

namespace App\Services\Payments;

use App\Contracts\PaymentGateway;
use Stripe\StripeClient;

class StripePayment implements PaymentGateway
{
    protected $stripe;

    public function __construct() {
        $this->stripe = new StripeClient(config('services.stripe.secret'));
    }

    public function charge(float $amount, array $details): string
    {
        $charge = $this->stripe->charges->create([
            'amount' => $amount * 100, // Cents
            'currency' => 'usd',
            'source' => $details['token'],
        ]);

        return $charge->id;
    }

    public function refund(string $transactionId): bool
    {
        // Implementation
        return true;
    }
}

PayPal Strategy

namespace App\Services\Payments;

use App\Contracts\PaymentGateway;

class PaypalPayment implements PaymentGateway
{
    public function charge(float $amount, array $details): string
    {
        // PayPal specific logic
        return 'paypal_txn_id';
    }

    public function refund(string $transactionId): bool
    {
        return true;
    }
}

3. Context (PaymentManager)

Chúng ta cần một cách để chọn đúng strategy tại runtime. Manager pattern của Laravel (giống như Cache::driver()) rất hoàn hảo cho việc này.

namespace App\Services\Payments;

use Illuminate\Support\Manager;

class PaymentManager extends Manager
{
    public function getDefaultDriver()
    {
        return config('payment.default');
    }

    protected function createStripeDriver()
    {
        return new StripePayment();
    }

    protected function createPaypalDriver()
    {
        return new PaypalPayment();
    }
}

Đăng ký nó trong Service Provider:

// AppServiceProvider
$this->app->singleton(PaymentManager::class, function ($app) {
    return new PaymentManager($app);
});

4. Sử dụng

Bây giờ, việc chuyển đổi gateway rất thanh lịch.

use App\Services\Payments\PaymentManager;

class CheckoutController extends Controller
{
    public function store(Request $request, PaymentManager $payment)
    {
        // Lấy driver dựa trên lựa chọn của user, ví dụ: 'stripe' hoặc 'paypal'
        $gateway = $payment->driver($request->payment_method);

        try {
            $transactionId = $gateway->charge(100.00, $request->all());
            
            Order::create(['transaction_id' => $transactionId]);
            
            return response()->json(['status' => 'success']);
        } catch (\Exception $e) {
            return response()->json(['error' => 'Thanh toán thất bại'], 422);
        }
    }
}

Factory Alternative

Nếu bạn không muốn sử dụng full Manager class, một Factory đơn giản cũng hoạt động tốt:

class PaymentFactory
{
    public static function make(string $type): PaymentGateway
    {
        return match ($type) {
            'stripe' => app(StripePayment::class),
            'paypal' => app(PaypalPayment::class),
            default => throw new \Exception("Phương thức thanh toán không xác định"),
        };
    }
}

Xử lý các yêu cầu đầu vào khác nhau

Stripe cần token, PayPal có thể cần return URL. Làm thế nào để chuẩn hóa charge($amount, $details)?

Sử dụng Data Transfer Object (DTO) thay vì array.

class PaymentData
{
    public function __construct(
        public float $amount,
        public ?string $token = null,
        public ?string $payerId = null,
    ) {}
}

Cập nhật interface thành strict type: charge(PaymentData $data).

Lợi ích

  1. Open/Closed Principle: Thêm gateways mới (Momo, ZaloPay) bằng cách thêm một class mới, không thay đổi code hiện có.
  2. Testability: Bạn có thể dễ dàng mock PaymentGateway interface trong tests.
  3. Clarity: Logic cho từng nhà cung cấp được cô lập trong file riêng của nó.

Tóm tắt

Strategy Pattern về cơ bản là "Tính đa hình" (Polymorphism) áp dụng cho các thuật toán. Trong Laravel, kết hợp với Service Container, nó cung cấp một cách sạch sẽ, mở rộng được để quản lý nhiều triển khai của cùng một khả năng nghiệp vụ—như thanh toán, tính phí vận chuyển, hoặc kênh thông báo.

Bình luận