Strategy — mẫu chiến lược trong iOS và Android là gì

Tác giả: IT Sectr Đã đăng: 2026-02-18 Thời gian đọc: 9 phút

Strategy (Chiến lược) là một mẫu thiết kế hành vi định nghĩa một họ các thuật toán có thể hoán đổi và đặt mỗi thuật toán vào một lớp riêng (Strategy). Mẫu này cho phép chọn thuật toán ngay trong khi chạy: mã client làm việc thông qua một giao diện Strategy chung, và việc triển khai cụ thể được thay thế trong thời gian chạy. Trên iOS, mẫu được triển khai qua Protocol + các lớp chiến lược, trên Android — qua Interface + các triển khai. Strategy là một trong 23 mẫu GoF, được sử dụng rộng rãi để xử lý thanh toán, xác thực, sắp xếp và lọc dữ liệu. Để biết thêm chi tiết — hãy xem mô tả gốc GoF.

Các điểm chính

  • Strategy — mẫu hành vi GoF cho một họ các thuật toán có thể hoán đổi
  • Đóng gói thuật toán — mỗi thuật toán được cô lập trong lớp riêng của nó
  • Giao diện Strategy — một hợp đồng chung được tất cả các chiến lược cụ thể triển khai
  • Hợp thành thay vì kế thừa — ngữ cảnh giữ tham chiếu đến một chiến lược, không kế thừa hành vi
  • Nguyên tắc Open/Closed — có thể thêm chiến lược mới mà không thay đổi mã hiện có

Mẫu Strategy là gì: bản chất và cấu trúc

Strategy là một trong 23 mẫu GoF (Gang of Four), được mô tả trong cuốn sách "Design Patterns: Elements of Reusable Object-Oriented Software" (1994). Mẫu này giải quyết vấn đề chọn thuật toán trong thời gian chạy. Thay vì viết một lớp duy nhất với nhiều câu lệnh điều kiện (if-else, switch), Strategy đề xuất trích xuất mỗi thuật toán vào một lớp riêng với một giao diện chung. Ngữ cảnh (lớp sử dụng chiến lược) giữ tham chiếu đến giao diện Strategy và ủy quyền thực thi cho chiến lược cụ thể.

Cấu trúc của mẫu bao gồm ba thành phần: Context (ngữ cảnh) giữ tham chiếu đến Strategy và gọi phương thức của nó; Strategy (giao diện) khai báo một phương thức chung cho tất cả các thuật toán; ConcreteStrategy (chiến lược cụ thể) triển khai giao diện và chứa thuật toán cụ thể. Client tạo chiến lược mong muốn và truyền nó vào ngữ cảnh qua hàm tạo, setter hoặc tham số phương thức. Ngữ cảnh không biết chiến lược cụ thể nào đang được thực thi — nó chỉ làm việc với giao diện.

Thành phầnVai tròVí dụ
ContextGiữ tham chiếu đến StrategyPaymentProcessor, Sorter
StrategyGiao diện chung cho các thuật toánProtocol PaymentStrategy
ConcreteStrategyTriển khai cụ thể của thuật toánCardPayment, PayPalPayment

Nguyên tắc Open/Closed — ưu điểm chính của Strategy. Hệ thống mở cho việc mở rộng (có thể thêm chiến lược mới) và đóng cho việc sửa đổi (mã ngữ cảnh không cần thay đổi). Không có mẫu, việc thêm thuật toán mới yêu cầu thay đổi lớp hiện có, vi phạm OCP và tăng nguy cơ lỗi hồi quy. Strategy cũng giảm kích thước lớp: thay vì một lớp 200 dòng với switch-case, bạn có 6 lớp mỗi lớp 20 dòng.

Strategy trong iOS: triển khai bằng Swift với Protocol

Strategy trong Swift được triển khai qua Protocol (giao diện chiến lược) và các lớp hoặc cấu trúc chiến lược. Các giao thức Swift hỗ trợ kiểu liên kết và ràng buộc generic, mang lại sự linh hoạt khi thiết kế chiến lược. Ngữ cảnh thường là một lớp ViewModel hoặc dịch vụ chấp nhận chiến lược trong init hoặc qua một thuộc tính. Mẫu được sử dụng rộng rãi trong các dự án iOS để xử lý sự kiện, hoạt ảnh, định dạng dữ liệu và chiến lược UI.

swift
// 1. Protocol Strategy
protocol PaymentStrategy {
    func pay(amount: Decimal) async throws -> PaymentResult
}

// 2. Concrete Strategies
struct CardPaymentStrategy: PaymentStrategy {
    let cardNumber: String
    let cvv: String

    func pay(amount: Decimal) async throws -> PaymentResult {
        // Gửi yêu cầu đến API ngân hàng
        return PaymentResult(status: .success, transactionId: "tx_\(UUID())")
    }
}

struct PayPalPaymentStrategy: PaymentStrategy {
    let email: String

    func pay(amount: Decimal) async throws -> PaymentResult {
        // Chuyển hướng đến PayPal SDK
        return PaymentResult(status: .success, transactionId: "pp_\(UUID())")
    }
}

// 3. Context
class PaymentProcessor {
    private var strategy: PaymentStrategy

    init(strategy: PaymentStrategy) {
        self.strategy = strategy
    }

    func setStrategy(_: PaymentStrategy) {
        strategy = strategy
    }

    func processPayment(amount: Decimal) async throws -> PaymentResult {
        return try await strategy.pay(amount: amount)
    }
}

// Sử dụng
let processor = PaymentProcessor(strategy: CardPaymentStrategy(cardNumber: "4111...", cvv: "123"))
let result = try await processor.processPayment(amount: 99.99)

Strategy trong SwiftUI — mẫu tích hợp tự nhiên với MVVM. ViewModel chứa một thuộc tính chiến lược và gọi phương thức của nó khi người dùng thực hiện hành động. View SwiftUI nhận dữ liệu qua @Published hoặc @State — chiến lược ẩn chi tiết triển khai khỏi View. Ví dụ, một chiến lược xác thực văn bản (emailValidator, phoneValidator) được hoán đổi tùy theo loại trường nhập. Kết hợp Strategy với SwiftUI mang lại sự linh hoạt mà không cần kế thừa từ UIKit.

Strategy trong Android: triển khai bằng Kotlin với Interface

Strategy trong Kotlin sử dụng Interface ở cấp độ ngôn ngữ và các giao diện chức năng (SAM) để đơn giản hóa. Kotlin hỗ trợ lambda, cho phép truyền các thuật toán như hàm mà không cần khai báo một lớp chiến lược riêng. Trong Android, mẫu được sử dụng trong ViewModel và Use Cases để cô lập các thuật toán tải dữ liệu, lưu cache và xử lý lỗi. Các dự án Android với Clean Architecture sử dụng Strategy để tiêm các triển khai kho lưu trữ khác nhau tùy theo cờ (mock, real, cache).

kotlin
// 1. Interface Strategy
interface PaymentStrategy {
    suspend fun pay(amount: BigDecimal): PaymentResult
}

// 2. Concrete Strategies
class CardPaymentStrategy(
    private val cardNumber: String,
    private val cvv: String
) : PaymentStrategy {
    override suspend fun pay(amount: BigDecimal): PaymentResult {
        // API ngân hàng qua Retrofit
        return PaymentResult(success = true, transactionId = "tx_${UUID.randomUUID()}")
    }
}

class PayPalPaymentStrategy(
    private val email: String
) : PaymentStrategy {
    override suspend fun pay(amount: BigDecimal): PaymentResult {
        // Tích hợp PayPal SDK
        return PaymentResult(success = true, transactionId = "pp_${UUID.randomUUID()}")
    }
}

// 3. Context
class PaymentProcessor(
    private val strategy: PaymentStrategy
) {
    fun setStrategy(strategy: PaymentStrategy): PaymentProcessor {
        return PaymentProcessor(strategy)
    }

    suspend fun processPayment(amount: BigDecimal): PaymentResult {
        return strategy.pay(amount)
    }
}

// Sử dụng trong ViewModel
class CheckoutViewModel : ViewModel() {
    private var processor = PaymentProcessor(CardPaymentStrategy("4111...", "123"))

    fun payWithCard() {
        viewModelScope.launch {
            val result = processor.processPayment(BigDecimal("99.99"))
            // Xử lý kết quả
        }
    }
}

Strategy với Hilt/Dagger — trong các dự án Android, các chiến lược thường được tiêm qua DI. Hilt cung cấp một triển khai cụ thể của PaymentStrategy qua @Binds hoặc @Provides. Điều này cho phép thay đổi chiến lược mà không sửa đổi mã ngữ cảnh — chỉ cần thay đổi mô-đun DI cho một bản dựng khác (debug/release). Ví dụ, MockPaymentStrategy được tiêm để gỡ lỗi, một chiến lược ngân hàng thực cho sản xuất. Sự kết hợp Strategy + DI mang lại sự linh hoạt tối đa.

So sánh Strategy với State, Command và Template Method

Strategy vs State — về mặt cấu trúc, các mẫu giống hệt nhau: cả hai đều sử dụng hợp thành với một giao diện và các lớp cụ thể. Sự khác biệt là ở mục đích: Strategy chọn một thuật toán độc lập, State điều khiển hành vi của đối tượng tùy theo trạng thái của nó. Trong State, ngữ cảnh tự thay đổi chiến lược khi trạng thái thay đổi; trong Strategy, ngữ cảnh không kiểm soát việc chuyển đổi — client đặt thuật toán một cách rõ ràng. Các chiến lược không biết về nhau, trong khi các trạng thái có thể chuyển đổi lẫn nhau.

Strategy vs Command — Command đóng gói một hành động duy nhất như một đối tượng, Strategy đóng gói một tập hợp các thuật toán có thể hoán đổi. Command là "làm gì" (một lời gọi execute), Strategy là "làm như thế nào" (một thuật toán gồm nhiều bước). Command được sử dụng cho hàng đợi, thực thi trễ, hoàn tác/làm lại. Strategy được sử dụng để chọn cách thực hiện một tác vụ trong thời gian chạy. Các lệnh có thể được tham số hóa với các chiến lược, kết hợp cả hai mẫu.

Đặc điểmStrategyStateCommandTemplate Method
Mục đíchThuật toán có thể hoán đổiHành vi theo trạng tháiĐóng gói yêu cầuKhung thuật toán
Chuyển đổiClient một cách rõ ràngNgữ cảnh tự độngClient hoặc hàng đợiBằng kế thừa
Cấp độĐối tượng (hợp thành)Đối tượng (hợp thành)Đối tượngLớp (kế thừa)

Strategy vs Template Method — cả hai mẫu đều định nghĩa thuật toán nhưng theo những cách khác nhau. Template Method sử dụng kế thừa: một lớp cơ sở định nghĩa khung thuật toán (phương thức template), các lớp con ghi đè các bước riêng lẻ. Strategy sử dụng hợp thành: thuật toán được chuyển hoàn toàn ra một lớp riêng. Template Method đơn giản hơn cho các trường hợp có cấu trúc thuật toán cố định, Strategy — khi các thuật toán hoàn toàn khác nhau và có thể thay đổi động.

Ví dụ thực tế về sử dụng mẫu Strategy

Xử lý thanh toán — ví dụ kinh điển của Strategy. Giỏ hàng của cửa hàng trực tuyến chứa danh sách các mặt hàng và phương thức thanh toán được người dùng chọn. Mỗi phương thức (thẻ, PayPal, Apple Pay, Google Pay, tiền điện tử) là một chiến lược riêng với chữ ký pay(amount) chung. Ngữ cảnh PaymentProcessor không biết chính xác thanh toán được xử lý như thế nào — nó gọi phương thức chung. Thêm phương thức thanh toán mới không yêu cầu thay đổi mã giỏ hàng.

Xác thực dữ liệu — Strategy được sử dụng cho các quy tắc xác thực khác nhau của cùng một trường. EmailValidatorStrategy, PhoneValidatorStrategy, AgeValidatorStrategy triển khai một giao diện ValidationStrategy chung với phương thức validate(input). Biểu mẫu đăng ký sử dụng một tập hợp các chiến lược để kiểm tra từng trường. Các chiến lược xác thực có thể được kết hợp trong một chuỗi (Chain of Responsibility) hoặc áp dụng tất cả cùng một lúc trong một vòng lặp. Điều này thay thế các kiểm tra if-else dài bằng một tập hợp các bộ xác thực đa hình.

swift
// Chiến lược sắp xếp
protocol SortingStrategy {
    func sort<T>(_ items: [T]) -> [T] where T: Comparable
}

struct QuickSortStrategy: SortingStrategy {
    func sort<T>(_ items: [T]) -> [T] { /* quicksort */ items }
}

struct MergeSortStrategy: SortingStrategy {
    func sort<T>(_ items: [T]) -> [T] { /* mergesort */ items }
}

class SortedDataSource<T> {
    private var strategy: SortingStrategy
    func display(_ items: [T]) { let sorted = strategy.sort(items) }
}

Xác thực — trong ứng dụng di động, các chiến lược xác thực được chuyển đổi tùy theo nhà cung cấp. AuthStrategy với các phương thức login(), logout(), getToken() được triển khai cho EmailPasswordAuth, GoogleAuth, AppleAuth, BiometricAuth. Ngữ cảnh AuthManager chấp nhận chiến lược qua DI hoặc factory. Điều này cho phép thêm các nhà cung cấp xác thực mới mà không thay đổi màn hình đăng nhập. Mẫu Strategy là nền tảng cho nhiều thư viện OAuth và Firebase Authentication.

Các câu hỏi thường gặp

Khi nào nên sử dụng Strategy thay vì if-else?

Strategy phù hợp khi bạn có 3+ thuật toán có thể thay đổi hoặc mở rộng. Nếu có 2 thuật toán và chúng ổn định — if-else đơn giản ít tốn kém hơn. Sử dụng Strategy khi các thuật toán được sử dụng trong các phần khác nhau của ứng dụng, khi bạn cần hoán đổi thuật toán trong thời gian chạy, hoặc khi mỗi thuật toán yêu cầu các phụ thuộc và kiểm thử riêng.

Strategy có giống State không?

Không, đây là các mẫu khác nhau với cấu trúc tương tự. Strategy — client chọn thuật toán một cách rõ ràng và các chiến lược độc lập. State — đối tượng tự thay đổi hành vi khi trạng thái nội bộ của nó thay đổi và các trạng thái có thể chuyển đổi lẫn nhau. Trong State, ngữ cảnh quản lý việc thay đổi trạng thái; trong Strategy, mã client làm điều đó.

Có thể sử dụng Strategy mà không cần lớp — qua closures không?

Có, trong Swift và Kotlin, một chiến lược có thể được truyền như một closure hoặc lambda. Swift: typealias PaymentHandler = (Decimal) async throws -> PaymentResult. Kotlin: typealias PaymentFun = suspend (BigDecimal) -> PaymentResult. Điều này đơn giản hóa mã cho các trường hợp đơn giản nhưng mất đi tên gọi và tài liệu. Cho 1-2 thuật toán, closure là đủ; cho 4+, tốt hơn nên sử dụng các lớp riêng.

Làm thế nào để kiểm thử mẫu Strategy?

Mỗi chiến lược được kiểm thử bằng một kiểm thử đơn vị riêng với các phụ thuộc giả (mock). Ngữ cảnh được kiểm thử với một chiến lược giả — xác minh rằng ngữ cảnh gọi phương thức của chiến lược và truyền các tham số chính xác. Trong Swift, sử dụng XCTest + giao thức cho mock; trong Kotlin, sử dụng MockK hoặc Mockito. Ưu điểm chính: mỗi chiến lược được kiểm thử riêng biệt mà không cần thiết lập phức tạp.

Strategy có phải là mẫu GoF không?

Có, Strategy là một trong 23 mẫu được mô tả trong cuốn sách "Design Patterns: Elements of Reusable Object-Oriented Software" (Gamma, Helm, Johnson, Vlissides, 1994). Nó thuộc nhóm các mẫu hành vi. Tên khác: Policy. Mã ví dụ gốc trên Smalltalk-80 có sẵn trong ấn bản gốc của GoF.

Tổng kết

  • Strategy — mẫu hành vi cho các thuật toán có thể hoán đổi với giao diện chung
  • Đóng gói — mỗi thuật toán được cô lập trong một lớp chiến lược riêng
  • iOS Swift — triển khai qua Protocol, cấu trúc và closures
  • Android Kotlin — triển khai qua Interface, lambda và DI với Hilt
  • Nguyên tắc Open/Closed — các chiến lược mới được thêm mà không thay đổi ngữ cảnh
  • Ứng dụng — thanh toán, xác thực, sắp xếp, xác thực, định dạng

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm