+4

Đừng để code ngập ngụa trong IF-ELSE: Giải cứu Backend với Strategy Pattern

Chào anh em đồng âm Viblo!

Hôm nay chúng ta sẽ bàn về một chủ đề muôn thuở trong thiết kế phần mềm. Bạn đã bao giờ nhìn vào một function dài 500 dòng, chứa đến cả chục khối if-else hay switch-case lồng nhau chỉ để xử lý các logic tính toán khác nhau chưa?

Ví dụ nhé: Hệ thống e-commerce đang có 3 cổng thanh toán (COD, MoMo, VNPay). Tuần sau, sếp yêu cầu tích hợp thêm ZaloPay, ShopeePay, Apple Pay. Nếu cứ mở class cũ ra và nhét thêm if-else, sớm muộn gì class đó cũng trở thành một "đống rác" không thể bảo trì, rủi ro bug sinh ra ở những luồng cũ là cực kỳ cao.

Đó là lúc chúng ta cần đến chiếc phao cứu sinh mang tên Strategy Pattern. Bài viết này sẽ không chỉ nói lý thuyết suông, chúng ta sẽ đi từ code "bốc mùi" đến code sạch, và áp dụng tận răng vào framework cụ thể. Lên xe thôi!

1. Strategy Pattern là gì? (Nói theo ngôn ngữ loài người)

Theo sách vở (GoF): "Strategy Pattern định nghĩa một tập hợp các thuật toán, đóng gói từng thuật toán lại, và làm cho chúng có thể thay thế lẫn nhau."

Nói một cách thực tế: Thay vì bạn nhét 10 cách xử lý khác nhau (10 thuật toán) vào chung một class, bạn bóc tách mỗi cách xử lý ra thành một class riêng biệt (gọi là Strategy). Class chính (Context) lúc này không cần biết bên trong xử lý thế nào, nó chỉ gọi một "hợp đồng" (Interface) và nhận kết quả.

Tưởng tượng: Bạn đi làm từ nhà đến công ty. Mục tiêu (hành vi) là "Di chuyển". Nhưng thuật toán (Strategy) có thể là: Đi xe máy, đi xe bus, hoặc đi tàu điện ngầm (Metro). Bạn có thể dễ dàng "switch" chiến lược tùy vào thời tiết mà không cần thay đổi bản thân bạn.

2. Nỗi đau thực tế: The "Spaghetti" Code

Giả sử chúng ta đang code module Checkout cho một hệ thống bán lẻ mỹ phẩm, cần tính toán phí vận chuyển (Shipping Fee) dựa trên các đơn vị vận chuyển khác nhau: Giao Hàng Nhanh (GHN), Giao Hàng Tiết Kiệm (GHTK), Viettel Post.

Code thiếu kinh nghiệm (Bad Practice):

class OrderCheckout
{
    public function calculateShippingFee($order, $carrierType)
    {
        if ($carrierType === 'GHN') {
            // Logic phức tạp: gọi API GHN, tính toán khối lượng, khoảng cách...
            return 30000;
        } elseif ($carrierType === 'GHTK') {
            // Logic gọi API GHTK, xử lý bảo hiểm hàng hóa...
            return 25000;
        } elseif ($carrierType === 'VIETTEL_POST') {
            // Logic tính cước Viettel Post...
            return 35000;
        } else {
            throw new Exception("Không hỗ trợ đơn vị vận chuyển này");
        }
    }
}

Vấn đề ở đây là gì?

  • Vi phạm Open/Closed Principle (OCP): Khi thêm nhà vận chuyển mới (NinjaVan, J&T), bạn bắt buộc phải sửa class OrderCheckout.
  • Phình to (Fat Class): Logic tính toán của mỗi bên rất dài, file này sẽ nhanh chóng đạt mốc hàng ngàn dòng code.
  • Khó viết Unit Test: Test một cục khổng lồ luôn là ác mộng.

3. Lột xác với Strategy Pattern

Chúng ta sẽ đập đi xây lại theo 3 thành phần chính: Strategy Interface, Concrete Strategies, và Context.

Bước 1: Định nghĩa Interface (Hợp đồng) Mọi chiến lược tính phí vận chuyển đều phải tuân thủ hợp đồng này:

interface ShippingStrategyInterface
{
    public function calculate(array $orderData): int;
}

Bước 2: Tạo các Concrete Strategies (Các thuật toán cụ thể) Mỗi nhà vận chuyển sẽ nằm gọn gàng trong một class riêng. Ai làm việc người nấy!

class GhnShippingStrategy implements ShippingStrategyInterface
{
    public function calculate(array $orderData): int
    {
        // Thực hiện call API GHN, xử lý dữ liệu GHN ở đây
        // Rất độc lập, không đụng chạm đến ai
        return 30000; 
    }
}

class GhtkShippingStrategy implements ShippingStrategyInterface
{
    public function calculate(array $orderData): int
    {
        // Logic riêng của GHTK
        return 25000;
    }
}

Bước 3: Class Context sử dụng Strategy

Class OrderCheckout giờ đây đã được "thanh lọc". Nó không quan tâm GHN hay GHTK tính toán thế nào nữa.

class OrderCheckout
{
    private ShippingStrategyInterface $shippingStrategy;

    // Inject Strategy vào thông qua Constructor hoặc Setter
    public function setShippingStrategy(ShippingStrategyInterface $strategy)
    {
        $this->shippingStrategy = $strategy;
    }

    public function processCheckout(array $orderData)
    {
        if (!$this->shippingStrategy) {
            throw new Exception("Vui lòng chọn phương thức vận chuyển");
        }

        // Ủy quyền (Delegate) việc tính toán cho Strategy
        $fee = $this->shippingStrategy->calculate($orderData);
        
        return "Tổng phí vận chuyển của bạn là: " . $fee;
    }
}

Cùng xem lúc chạy (Client code) nó gọn thế nào nhé:

$checkout = new OrderCheckout();
$orderData = ['weight' => 2, 'distance' => 10];

// Khách chọn GHN
$checkout->setShippingStrategy(new GhnShippingStrategy());
echo $checkout->processCheckout($orderData); // Cước: 30k

// Khách đổi ý chọn GHTK lúc runtime, không cần khởi tạo lại OrderCheckout!
$checkout->setShippingStrategy(new GhtkShippingStrategy());
echo $checkout->processCheckout($orderData); // Cước: 25k

4. Kinh nghiệm hạng nặng (Advanced Practical Tips)

Đây mới là phần đáng tiền của bài viết. Ở quy mô thực tế, chúng ta không ngồi new các object thủ công như trên. Hệ thống lớn cần sự tự động hóa.

Tip 1: Áp dụng Strategy Pattern đỉnh cao trong Laravel (Dùng Factory Pattern)

Để tránh việc phải check if-else lúc khởi tạo các class Strategy, tôi thường kết hợp nó với Factory Pattern.

class ShippingStrategyFactory
{
    public static function make(string $carrierType): ShippingStrategyInterface
    {
        return match ($carrierType) {
            'GHN' => new GhnShippingStrategy(),
            'GHTK' => new GhtkShippingStrategy(),
            'VIETTEL' => new ViettelPostShippingStrategy(),
            default => throw new InvalidArgumentException("Carrier không hợp lệ"),
        };
    }
}

// Lúc sử dụng ở Controller
$strategy = ShippingStrategyFactory::make($request->input('carrier_type'));
$checkout->setShippingStrategy($strategy);

Nhờ biểu thức match của PHP 8, code của bạn trông cực kỳ sexy và clean!

Tip 2: Tận dụng Laravel Service Container (Contextual Binding) Nếu bạn thiết kế hệ thống tính toán giá vé (Fare Collection) tự động cho nhiều tuyến đường sắt/metro khác nhau, bạn có thể ép Laravel tự động bind class Strategy tương ứng dựa vào Controller mà không cần Factory luôn.

// Trong AppServiceProvider
$this->app->when(MetroLine1Controller::class)
          ->needs(FareCalculationStrategy::class)
          ->give(DistanceBasedFareStrategy::class);

$this->app->when(MetroLine2Controller::class)
          ->needs(FareCalculationStrategy::class)
          ->give(FlatRateFareStrategy::class);

Tip 3: Đừng chỉ gò bó trong PHP - Nhìn sang Go (Golang) Tư duy Strategy này là phổ quát. Nếu anh em nào đang nhảy sang viết Microservices bằng Go, Strategy Pattern cực kỳ tỏa sáng nhờ hệ thống Interface ngầm định (Implicit Interfaces) của Go.

// Go Demo nhanh
type PaymentStrategy interface {
    Pay(amount float64) string
}

type MoMo struct{}
func (m MoMo) Pay(amount float64) string {
    return "Thanh toán bằng MoMo"
}

// Struct Checkout dùng strategy
type Checkout struct {
    strategy PaymentStrategy
}

Cú pháp khác nhau, nhưng cái "Core System Thinking" – bóc tách thuật toán – thì không đổi.

5. Lời kết

Strategy Pattern không phải là viên đạn bạc (Silver Bullet). Đừng lạm dụng nó nếu logic của bạn chỉ có đúng 2 nhánh if-else đơn giản và sẽ không bao giờ thay đổi. Việc tạo ra quá nhiều class nhỏ cũng có thể làm hệ thống khó theo dõi ban đầu.

Tuy nhiên, đối với các domain có tính mở rộng liên tục như: Payment Gateways, Shipping Providers, Caching Drivers, hay Rules Validation,... Strategy Pattern chính là "thần dược" giúp hệ thống của bạn bền vững qua thời gian.

Anh em đang xử lý đống IF-ELSE rác rưởi ở dự án hiện tại như thế nào? Áp dụng ngay Strategy xem sếp có lác mắt không nhé!

Nếu thấy bài viết chất lượng, đừng tiếc 1 Upvote và Bookmark để mình có động lực chia sẻ thêm các patterns "khó nhằn" khác nha! Happy Coding!


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í