Back to blog

Strategy và Template Method Patterns Giải Thích

oopbehavioral-patternsdesign-patternstypescriptpythonjava
Strategy và Template Method Patterns Giải Thích

Giới Thiệu

Ở bài trước, bạn đã học hai structural pattern kiểu wrap object: Decorator thêm behavior, Proxy kiểm soát access. Giờ chúng ta bước sang Phase 5: Behavioral Patterns — những pattern liên quan đến cách object giao tiếp và phân chia trách nhiệm.

Bài này sẽ cover hai behavioral pattern kinh điển để thuần hóa sự phức tạp của algorithm:

PatternTóm Tắt Một Câu
StrategyĐóng gói một họ algorithm, cho phép swap chúng lúc runtime
Template MethodĐịnh nghĩa skeleton của algorithm, để subclass tự điền vào phần còn thiếu

Cả hai đều xoay quanh algorithm — nhưng tiếp cận vấn đề từ hai hướng ngược nhau. Strategy chọn algorithm nào dùng. Template Method quy định cấu trúc của algorithm như thế nào.

Bạn Sẽ Học Được Gì

✅ Thay thế đống if/else và switch bằng Strategy pattern
✅ Chọn và swap algorithm lúc runtime — không cần đụng vào client code
✅ Định nghĩa algorithm skeleton với Template Method pattern
✅ Dùng hook method để cho subclass các điểm override tùy chọn
✅ Implement cả hai pattern bằng TypeScript, Python và Java
✅ Biết khi nào dùng Strategy vs Template Method — và khi nào kết hợp cả hai

Điều Kiện Cần


Phần 1: Strategy Pattern

Vấn Đề: Conditional Logic Bùng Nổ

Bạn đang build một navigation app. Tùy theo phương tiện di chuyển, cách tính route sẽ khác nhau:

// ❌ Trước khi dùng Strategy — conditional ngày càng phình to
class Navigator {
  buildRoute(origin: string, destination: string, mode: string): void {
    if (mode === "car") {
      console.log(`Tính route xe hơi: đường nhanh nhất từ ${origin} đến ${destination}`);
      // ... logic tính route cho xe hơi
    } else if (mode === "bike") {
      console.log(`Tính route xe đạp: làn dành cho xe đạp từ ${origin} đến ${destination}`);
      // ... logic tính route xe đạp
    } else if (mode === "walk") {
      console.log(`Tính route đi bộ: vỉa hè từ ${origin} đến ${destination}`);
      // ... logic đi bộ
    } else if (mode === "transit") {
      console.log(`Tính route xe bus: bus và metro từ ${origin} đến ${destination}`);
      // ... logic xe công cộng
    }
    // Thêm phương tiện mới = thêm một nhánh else-if nữa
  }
}

Class này có bốn vấn đề:

  1. Vi phạm Open/Closed Principle — muốn thêm phương tiện mới là phải sửa class này
  2. Mỗi phương tiện mới làm method phình to thêm một đống conditional
  3. Unit test buộc phải test tất cả các nhánh cùng nhau — không test riêng lẻ được
  4. Logic tính route không tái sử dụng được ở chỗ khác

Strategy pattern giải quyết chỗ này: tách mỗi biến thể algorithm ra thành class riêng, bắt tất cả implement một interface chung, và để client tự chọn dùng cái nào.


Cấu Trúc Pattern

Các thành phần:

  • Strategy — interface mà tất cả các biến thể algorithm đều implement
  • ConcreteStrategy — một algorithm cụ thể (ví dụ: CarRouteStrategy, BikeRouteStrategy)
  • Context — giữ reference đến strategy hiện tại và ủy quyền việc thực thi algorithm cho nó

Insight chính: Context không quan tâm nó đang giữ concrete strategy nào — nó chỉ gọi execute(). Nghĩa là bạn có thể swap strategy lúc runtime mà không cần thay đổi bất kỳ dòng code nào trong Context.


TypeScript Implementation

Ví Dụ 1: Route Navigation

// Strategy interface — contract mà tất cả routing algorithm phải thỏa mãn
interface RouteStrategy {
  buildRoute(origin: string, destination: string): string;
}
 
// Concrete strategies — mỗi cái đóng gói một routing algorithm
class CarRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚗 Route xe hơi: Đường nhanh nhất từ ${origin} đến ${destination} (tránh đường toll)`;
  }
}
 
class BikeRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚴 Route xe đạp: Làn xe đạp chuyên dụng từ ${origin} đến ${destination}`;
  }
}
 
class WalkingRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚶 Route đi bộ: Vỉa hè và đường bộ hành từ ${origin} đến ${destination}`;
  }
}
 
class TransitRouteStrategy implements RouteStrategy {
  buildRoute(origin: string, destination: string): string {
    return `🚌 Route xe công cộng: Bus + Metro từ ${origin} đến ${destination}`;
  }
}
 
// Context — dùng bất kỳ strategy nào client truyền vào
class Navigator {
  private strategy: RouteStrategy;
 
  constructor(strategy: RouteStrategy) {
    this.strategy = strategy;
  }
 
  // Swap strategy bất cứ lúc nào — open/closed
  setStrategy(strategy: RouteStrategy): void {
    this.strategy = strategy;
  }
 
  navigate(origin: string, destination: string): void {
    const route = this.strategy.buildRoute(origin, destination);
    console.log(route);
  }
}
 
// Usage — chọn algorithm lúc runtime
const nav = new Navigator(new CarRouteStrategy());
nav.navigate("Nhà", "Văn phòng");
// 🚗 Route xe hơi: Đường nhanh nhất từ Nhà đến Văn phòng (tránh đường toll)
 
nav.setStrategy(new BikeRouteStrategy());
nav.navigate("Nhà", "Văn phòng");
// 🚴 Route xe đạp: Làn xe đạp chuyên dụng từ Nhà đến Văn phòng
 
nav.setStrategy(new TransitRouteStrategy());
nav.navigate("Nhà", "Sân bay");
// 🚌 Route xe công cộng: Bus + Metro từ Nhà đến Sân bay

Không có if/else nào trong Navigator. Thêm phương tiện mới chỉ là thêm một class mới implement RouteStrategy — Navigator không cần chỉnh sửa.


Ví Dụ 2: Payment Processing

Một scenario thực tế — checkout e-commerce với nhiều phương thức thanh toán:

interface PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean;
}
 
class CreditCardStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`💳 Xử lý $${amount} qua thẻ tín dụng kết thúc bằng ${details.cardNumber?.slice(-4)}`);
    // ... credit card processing logic
    return true;
  }
}
 
class PayPalStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`🅿️ Xử lý $${amount} qua tài khoản PayPal ${details.email}`);
    // ... PayPal API call
    return true;
  }
}
 
class CryptoStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`₿ Xử lý $${amount} tương đương ${details.currency} tới ví ${details.walletAddress}`);
    // ... blockchain transaction
    return true;
  }
}
 
class BuyNowPayLaterStrategy implements PaymentStrategy {
  pay(amount: number, details: Record<string, string>): boolean {
    console.log(`📅 Chia $${amount} thành 4 kỳ qua ${details.provider}`);
    return true;
  }
}
 
class ShoppingCart {
  private items: { name: string; price: number }[] = [];
  private paymentStrategy: PaymentStrategy;
 
  constructor(paymentStrategy: PaymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }
 
  setPaymentStrategy(strategy: PaymentStrategy): void {
    this.paymentStrategy = strategy;
  }
 
  addItem(name: string, price: number): void {
    this.items.push({ name, price });
  }
 
  checkout(details: Record<string, string>): void {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    console.log(`\nTổng đơn hàng: $${total}`);
    const success = this.paymentStrategy.pay(total, details);
    if (success) {
      console.log("✅ Thanh toán thành công!\n");
    }
  }
}
 
// Usage
const cart = new ShoppingCart(new CreditCardStrategy());
cart.addItem("Laptop", 999);
cart.addItem("Chuột", 49);
cart.checkout({ cardNumber: "4111111111112345" });
 
// User đổi sang PayPal lúc checkout
cart.setPaymentStrategy(new PayPalStrategy());
cart.checkout({ email: "[email protected]" });

Ví Dụ 3: Sort Strategy

interface SortStrategy<T> {
  sort(data: T[]): T[];
}
 
class BubbleSortStrategy<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    const arr = [...data];
    for (let i = 0; i < arr.length - 1; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}
 
class QuickSortStrategy<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    if (data.length <= 1) return data;
    const pivot = data[Math.floor(data.length / 2)];
    const left = data.filter(x => x < pivot);
    const middle = data.filter(x => x === pivot);
    const right = data.filter(x => x > pivot);
    return [...this.sort(left), ...middle, ...this.sort(right)];
  }
}
 
class Sorter<T> {
  constructor(private strategy: SortStrategy<T>) {}
 
  setStrategy(strategy: SortStrategy<T>): void {
    this.strategy = strategy;
  }
 
  sort(data: T[]): T[] {
    return this.strategy.sort(data);
  }
}
 
// Chọn algorithm tùy theo kích thước dataset
const sorter = new Sorter<number>(new BubbleSortStrategy());
const smallDataset = [5, 2, 8, 1, 9];
console.log(sorter.sort(smallDataset)); // [1, 2, 5, 8, 9]
 
sorter.setStrategy(new QuickSortStrategy());
const largeDataset = Array.from({ length: 1000 }, () => Math.random() * 1000);
sorter.sort(largeDataset); // QuickSort xử lý dataset lớn hiệu quả hơn

Python Implementation

Python có first-class function nên Strategy implement còn gọn hơn — dùng class hoặc plain function đều được:

from abc import ABC, abstractmethod
from typing import Callable
 
# --- Class-based Strategy (giống TypeScript) ---
 
class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, filename: str) -> str:
        pass
 
class ZipStrategy(CompressionStrategy):
    def compress(self, filename: str) -> str:
        return f"Nén {filename} kiểu ZIP (tỷ lệ nén tốt)"
 
class RarStrategy(CompressionStrategy):
    def compress(self, filename: str) -> str:
        return f"Nén {filename} kiểu RAR (file nhỏ hơn)"
 
class TarGzStrategy(CompressionStrategy):
    def compress(self, filename: str) -> str:
        return f"Nén {filename} kiểu TAR.GZ (chuẩn Unix/Linux)"
 
class Archiver:
    def __init__(self, strategy: CompressionStrategy):
        self._strategy = strategy
 
    def set_strategy(self, strategy: CompressionStrategy) -> None:
        self._strategy = strategy
 
    def archive(self, filename: str) -> str:
        return self._strategy.compress(filename)
 
# Class-based usage
archiver = Archiver(ZipStrategy())
print(archiver.archive("documents.tar"))
# Nén documents.tar kiểu ZIP (tỷ lệ nén tốt)
 
archiver.set_strategy(TarGzStrategy())
print(archiver.archive("backup.tar"))
# Nén backup.tar kiểu TAR.GZ (chuẩn Unix/Linux)
 
 
# --- Function-based Strategy (kiểu Pythonic) ---
# Khi algorithm đơn giản, dùng callable trực tiếp cho gọn
 
StrategyFn = Callable[[str], str]
 
def zip_compress(filename: str) -> str:
    return f"ZIP: {filename}.zip"
 
def rar_compress(filename: str) -> str:
    return f"RAR: {filename}.rar"
 
class FunctionalArchiver:
    def __init__(self, strategy: StrategyFn):
        self._strategy = strategy
 
    def archive(self, filename: str) -> str:
        return self._strategy(filename)
 
fa = FunctionalArchiver(zip_compress)
print(fa.archive("report"))  # ZIP: report.zip
 
fa._strategy = rar_compress
print(fa.archive("report"))  # RAR: report.rar
 
# Gọn hơn nữa: dùng lambda
fa._strategy = lambda f: f"GZIP: {f}.gz"
print(fa.archive("report"))  # GZIP: report.gz

Java Implementation

// Strategy Pattern — Tính discount trong hệ thống e-commerce
 
// Strategy interface
public interface DiscountStrategy {
    double calculateDiscount(double originalPrice);
    String getDescription();
}
 
// Concrete strategies
public class NoDiscountStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double originalPrice) {
        return 0;
    }
 
    @Override
    public String getDescription() {
        return "Không có discount";
    }
}
 
public class PercentageDiscountStrategy implements DiscountStrategy {
    private final double percentage;
 
    public PercentageDiscountStrategy(double percentage) {
        this.percentage = percentage;
    }
 
    @Override
    public double calculateDiscount(double originalPrice) {
        return originalPrice * (percentage / 100);
    }
 
    @Override
    public String getDescription() {
        return String.format("Giảm %.0f%%", percentage);
    }
}
 
public class FixedAmountDiscountStrategy implements DiscountStrategy {
    private final double amount;
 
    public FixedAmountDiscountStrategy(double amount) {
        this.amount = amount;
    }
 
    @Override
    public double calculateDiscount(double originalPrice) {
        return Math.min(amount, originalPrice);
    }
 
    @Override
    public String getDescription() {
        return String.format("Giảm $%.2f", amount);
    }
}
 
public class BuyOneGetOneFreeStrategy implements DiscountStrategy {
    @Override
    public double calculateDiscount(double originalPrice) {
        return originalPrice / 2; // Hiệu quả giảm 50%
    }
 
    @Override
    public String getDescription() {
        return "Mua 1 tặng 1";
    }
}
 
// Context
public class PricingEngine {
    private DiscountStrategy strategy;
 
    public PricingEngine(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
 
    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
 
    public double getFinalPrice(double originalPrice) {
        double discount = strategy.calculateDiscount(originalPrice);
        double finalPrice = originalPrice - discount;
        System.out.printf("Gốc: $%.2f | %s | Cuối: $%.2f%n",
                originalPrice, strategy.getDescription(), finalPrice);
        return finalPrice;
    }
}
 
// Usage
public class Main {
    public static void main(String[] args) {
        PricingEngine engine = new PricingEngine(new NoDiscountStrategy());
        engine.getFinalPrice(100.0);
        // Gốc: $100.00 | Không có discount | Cuối: $100.00
 
        engine.setStrategy(new PercentageDiscountStrategy(20));
        engine.getFinalPrice(100.0);
        // Gốc: $100.00 | Giảm 20% | Cuối: $80.00
 
        engine.setStrategy(new FixedAmountDiscountStrategy(15));
        engine.getFinalPrice(100.0);
        // Gốc: $100.00 | Giảm $15.00 | Cuối: $85.00
 
        engine.setStrategy(new BuyOneGetOneFreeStrategy());
        engine.getFinalPrice(100.0);
        // Gốc: $100.00 | Mua 1 tặng 1 | Cuối: $50.00
    }
}

Phần 2: Template Method Pattern

Vấn Đề: Cấu Trúc Algorithm Bị Nhân Bản

Bạn đang build một ứng dụng data mining. Nó đọc file theo nhiều format khác nhau (CSV, JSON, XML) rồi generate báo cáo. Quy trình tổng thể lúc nào cũng như nhau:

1. Mở file  →  2. Trích xuất data  →  3. Parse data  →  4. Phân tích  →  5. Tạo báo cáo  →  6. Đóng file

Nhưng bước 2 và 3 khác nhau tùy format. Nếu không có Template Method, bạn sẽ có ba class copy-paste y chang cái skeleton đó:

// ❌ Trùng lắp — cấu trúc giống nhau, chỉ khác bước bên trong
class CSVDataMiner {
  mine(filePath: string): void {
    this.openFile(filePath);   // giống
    this.extractCSVData();     // khác
    this.parseCSVData();       // khác
    this.analyzeData();        // giống
    this.generateReport();     // giống
    this.closeFile();          // giống
  }
  // ...
}
 
class JSONDataMiner {
  mine(filePath: string): void {
    this.openFile(filePath);   // giống (bị nhân bản)
    this.extractJSONData();    // khác
    this.parseJSONData();      // khác
    this.analyzeData();        // giống (bị nhân bản)
    this.generateReport();     // giống (bị nhân bản)
    this.closeFile();          // giống (bị nhân bản)
  }
  // ...
}

Bốn trong sáu bước hoàn toàn giống nhau — nhưng lại bị copy-paste vào từng class. Nếu analyzeData thay đổi, bạn phải cập nhật ở ba nơi.

Template Method pattern giải quyết chỗ này: đặt algorithm skeleton vào base class và để subclass chỉ override những bước thay đổi.


Cấu Trúc Pattern

Các thành phần:

  • AbstractClass — định nghĩa templateMethod() gọi tất cả các bước theo thứ tự; implement các bước bất biến; khai báo abstract method cho các bước biến đổi; tùy chọn cung cấp hook method
  • ConcreteClass — implement các abstract step cần thay đổi; có thể tùy chọn override hook method

Hai loại method trong base class:

  • Abstract method — bắt buộc phải override ở subclass (phần biến đổi)
  • Hook method — có implementation mặc định (rỗng hoặc tối giản); subclass có thể override nhưng không bắt buộc (điểm customization tùy chọn)

TypeScript Implementation

Ví Dụ 1: Data Mining Pipeline

// Abstract base — định nghĩa algorithm skeleton
abstract class DataMiner {
  // Template method — subclass không được thay đổi thứ tự này
  public mine(filePath: string): void {
    this.openFile(filePath);   // bất biến: giống nhau với mọi format
    this.extractData();        // biến đổi: bắt buộc phải override
    this.parseData();          // biến đổi: bắt buộc phải override
    this.analyzeData();        // bất biến: giống nhau
    this.generateReport();     // bất biến: giống nhau
    this.closeFile();          // bất biến: giống nhau
  }
 
  // Bước bất biến — implement ở đây, không override
  private openFile(filePath: string): void {
    console.log(`📂 Mở file: ${filePath}`);
  }
 
  private analyzeData(): void {
    console.log("📊 Phân tích data đã trích xuất...");
  }
 
  private generateReport(): void {
    console.log("📄 Tạo báo cáo...");
  }
 
  private closeFile(): void {
    console.log("🔒 Đóng file.");
  }
 
  // Bước biến đổi — subclass BẮT BUỘC implement
  protected abstract extractData(): void;
  protected abstract parseData(): void;
 
  // Hook method — override tùy chọn, mặc định rỗng
  protected afterParse(): void {
    // Subclass có thể override để thêm xử lý sau parse
  }
}
 
// Concrete class — chỉ implement phần thay đổi
class CSVDataMiner extends DataMiner {
  protected extractData(): void {
    console.log("📋 Trích xuất rows từ file CSV");
  }
 
  protected parseData(): void {
    console.log("🔍 Parse CSV: tách theo dấu phẩy, map header");
  }
}
 
class JSONDataMiner extends DataMiner {
  protected extractData(): void {
    console.log("📋 Trích xuất JSON object từ file");
  }
 
  protected parseData(): void {
    console.log("🔍 Parse JSON: deserialize nested object");
  }
 
  // Class này dùng hook tùy chọn
  protected afterParse(): void {
    console.log("🔗 JSON: Resolve $ref references...");
  }
}
 
class XMLDataMiner extends DataMiner {
  protected extractData(): void {
    console.log("📋 Trích xuất XML node từ file");
  }
 
  protected parseData(): void {
    console.log("🔍 Parse XML: xây dựng DOM tree");
  }
}
 
// Usage — cùng interface, behavior bên trong khác nhau
console.log("=== CSV Mining ===");
new CSVDataMiner().mine("sales_2026.csv");
 
console.log("\n=== JSON Mining ===");
new JSONDataMiner().mine("products.json");
 
console.log("\n=== XML Mining ===");
new XMLDataMiner().mine("config.xml");

Output:

=== CSV Mining ===
📂 Mở file: sales_2026.csv
📋 Trích xuất rows từ file CSV
🔍 Parse CSV: tách theo dấu phẩy, map header
📊 Phân tích data đã trích xuất...
📄 Tạo báo cáo...
🔒 Đóng file.
 
=== JSON Mining ===
📂 Mở file: products.json
📋 Trích xuất JSON object từ file
🔍 Parse JSON: deserialize nested object
📊 Phân tích data đã trích xuất...
📄 Tạo báo cáo...
🔒 Đóng file.

Ví Dụ 2: Pha Đồ Uống

Ví dụ kinh điển của Gang of Four — pha trà vs pha cà phê:

abstract class HotBeverage {
  // Template method — công thức cố định
  public prepare(): void {
    this.boilWater();
    this.brew();
    this.pourInCup();
    if (this.customerWantsCondiments()) {  // hook kiểm soát bước tùy chọn
      this.addCondiments();
    }
  }
 
  // Bước cố định
  private boilWater(): void {
    console.log("🔥 Đun sôi nước");
  }
 
  private pourInCup(): void {
    console.log("☕ Rót vào ly");
  }
 
  // Bước biến đổi — subclass tự điền
  protected abstract brew(): void;
  protected abstract addCondiments(): void;
 
  // Hook — subclass có thể override để nói "không cần gia vị"
  protected customerWantsCondiments(): boolean {
    return true; // mặc định: có
  }
}
 
class Tea extends HotBeverage {
  protected brew(): void {
    console.log("🍃 Ngâm túi trà (3 phút)");
  }
 
  protected addCondiments(): void {
    console.log("🍋 Thêm lát chanh");
  }
}
 
class Coffee extends HotBeverage {
  protected brew(): void {
    console.log("☕ Lọc cà phê qua phin");
  }
 
  protected addCondiments(): void {
    console.log("🥛 Thêm đường và sữa");
  }
}
 
class BlackCoffee extends HotBeverage {
  protected brew(): void {
    console.log("☕ Lọc cà phê qua phin");
  }
 
  protected addCondiments(): void {
    // Không bao giờ được gọi
  }
 
  // Override hook — cà phê đen không cần gia vị
  protected customerWantsCondiments(): boolean {
    return false;
  }
}
 
console.log("--- Pha Trà ---");
new Tea().prepare();
 
console.log("\n--- Pha Cà Phê ---");
new Coffee().prepare();
 
console.log("\n--- Pha Cà Phê Đen ---");
new BlackCoffee().prepare();
// Không có bước "Thêm" — hook trả về false

Ví Dụ 3: Quy Trình Xử Lý Đơn Hàng

abstract class OrderProcessor {
  // Template method — pipeline xử lý
  public processOrder(orderId: string): void {
    console.log(`\n🛒 Xử lý đơn hàng #${orderId}`);
    if (!this.validateOrder(orderId)) {
      console.log("❌ Validate đơn hàng thất bại. Hủy.");
      return;
    }
    this.processPayment(orderId);
    this.fulfillOrder(orderId);
    this.sendNotification(orderId);
    this.logOrder(orderId);
  }
 
  // Abstract — mỗi kênh xử lý theo cách riêng
  protected abstract validateOrder(orderId: string): boolean;
  protected abstract processPayment(orderId: string): void;
  protected abstract fulfillOrder(orderId: string): void;
 
  // Bước bất biến (giống nhau với mọi kênh)
  private sendNotification(orderId: string): void {
    console.log(`📧 Gửi email xác nhận cho đơn hàng #${orderId}`);
  }
 
  private logOrder(orderId: string): void {
    console.log(`📝 Ghi log đơn hàng #${orderId} vào audit trail`);
  }
}
 
class OnlineOrderProcessor extends OrderProcessor {
  protected validateOrder(orderId: string): boolean {
    console.log("🔍 Validate đơn online: kiểm tra tồn kho + phát hiện gian lận");
    return true;
  }
 
  protected processPayment(orderId: string): void {
    console.log("💳 Xử lý thanh toán online qua Stripe");
  }
 
  protected fulfillOrder(orderId: string): void {
    console.log("📦 Tạo nhãn vận chuyển, lên lịch lấy hàng");
  }
}
 
class InStoreOrderProcessor extends OrderProcessor {
  protected validateOrder(orderId: string): boolean {
    console.log("🔍 Validate đơn tại cửa hàng: kiểm tra hàng trên kệ");
    return true;
  }
 
  protected processPayment(orderId: string): void {
    console.log("🏪 Xử lý thanh toán tại POS terminal");
  }
 
  protected fulfillOrder(orderId: string): void {
    console.log("🛍️ Đơn hàng sẵn sàng để lấy ngay");
  }
}
 
new OnlineOrderProcessor().processOrder("ORD-1001");
new InStoreOrderProcessor().processOrder("ORD-1002");

Python Implementation

from abc import ABC, abstractmethod
 
class ReportGenerator(ABC):
    """Abstract base định nghĩa algorithm tạo báo cáo."""
 
    # Template method — theo quy ước là final (Python không enforce nhưng nên hiểu vậy)
    def generate(self, data: dict) -> str:
        header = self._create_header(data)
        body = self._create_body(data)
        footer = self._create_footer()
        extras = self._add_extras(data)   # hook
        return "\n".join(filter(None, [header, body, extras, footer]))
 
    # Bước bất biến
    def _create_footer(self) -> str:
        return "--- Hết Báo Cáo ---"
 
    # Abstract step — subclass bắt buộc implement
    @abstractmethod
    def _create_header(self, data: dict) -> str:
        pass
 
    @abstractmethod
    def _create_body(self, data: dict) -> str:
        pass
 
    # Hook — enhancement tùy chọn
    def _add_extras(self, data: dict) -> str:
        return ""  # Mặc định: không thêm gì
 
 
class PDFReportGenerator(ReportGenerator):
    def _create_header(self, data: dict) -> str:
        return f"[PDF] BÁO CÁO: {data['title'].upper()}\nNgày tạo: {data['date']}"
 
    def _create_body(self, data: dict) -> str:
        lines = [f"  • {k}: {v}" for k, v in data.get("metrics", {}).items()]
        return "[PDF Body]\n" + "\n".join(lines)
 
    def _add_extras(self, data: dict) -> str:
        return "[PDF] Biểu đồ và đồ thị đính kèm"
 
 
class HTMLReportGenerator(ReportGenerator):
    def _create_header(self, data: dict) -> str:
        return f"<h1>{data['title']}</h1><p>Ngày: {data['date']}</p>"
 
    def _create_body(self, data: dict) -> str:
        rows = "".join(f"<tr><td>{k}</td><td>{v}</td></tr>"
                       for k, v in data.get("metrics", {}).items())
        return f"<table>{rows}</table>"
 
 
class MarkdownReportGenerator(ReportGenerator):
    def _create_header(self, data: dict) -> str:
        return f"# {data['title']}\n**Ngày:** {data['date']}"
 
    def _create_body(self, data: dict) -> str:
        lines = [f"| {k} | {v} |" for k, v in data.get("metrics", {}).items()]
        return "| Chỉ số | Giá trị |\n|--------|--------|\n" + "\n".join(lines)
 
 
# Usage — cùng template, output format khác nhau
report_data = {
    "title": "Báo Cáo Doanh Thu Tháng",
    "date": "2026-03-05",
    "metrics": {
        "Doanh thu": "$45,200",
        "Đơn hàng": 312,
        "Khách hàng mới": 87,
    }
}
 
generators = [PDFReportGenerator(), HTMLReportGenerator(), MarkdownReportGenerator()]
 
for generator in generators:
    print(f"\n{'='*40}")
    print(generator.generate(report_data))

Java Implementation

// Template Method — Skeleton vòng đời trò chơi
 
public abstract class Game {
    // Template method — final đảm bảo subclass không thay đổi được thứ tự game loop
    public final void play() {
        initialize();
        startPlay();
        while (!isDone()) {
            takeTurn();
        }
        endPlay();
        announceWinner();
    }
 
    // Abstract step — subclass bắt buộc implement behavior riêng của game
    protected abstract void initialize();
    protected abstract void startPlay();
    protected abstract void takeTurn();
    protected abstract boolean isDone();
    protected abstract void endPlay();
 
    // Bước bất biến — thông báo người chiến thắng luôn có cùng cấu trúc
    private void announceWinner() {
        System.out.println("🏆 Game over! " + getWinnerName() + " thắng!");
    }
 
    // Hook — subclass cung cấp tên người chiến thắng
    protected String getWinnerName() {
        return "Người chơi 1"; // mặc định
    }
}
 
public class Chess extends Game {
    private int turnCount = 0;
 
    @Override
    protected void initialize() {
        System.out.println("♟️ Cờ vua: Xếp bàn cờ với 32 quân");
    }
 
    @Override
    protected void startPlay() {
        System.out.println("♟️ Cờ vua: Bên trắng đi trước");
    }
 
    @Override
    protected void takeTurn() {
        turnCount++;
        System.out.println("♟️ Cờ vua: Lượt " + turnCount + " — người chơi thực hiện nước đi");
    }
 
    @Override
    protected boolean isDone() {
        return turnCount >= 3; // demo đơn giản: 3 lượt
    }
 
    @Override
    protected void endPlay() {
        System.out.println("♟️ Cờ vua: Phát hiện Chiếu hết");
    }
 
    @Override
    protected String getWinnerName() {
        return "Bên Trắng";
    }
}
 
public class TicTacToe extends Game {
    private int moveCount = 0;
 
    @Override
    protected void initialize() {
        System.out.println("❌ TicTacToe: Vẽ lưới 3x3");
    }
 
    @Override
    protected void startPlay() {
        System.out.println("❌ TicTacToe: X đi trước");
    }
 
    @Override
    protected void takeTurn() {
        moveCount++;
        System.out.println("❌ TicTacToe: Nước " + moveCount);
    }
 
    @Override
    protected boolean isDone() {
        return moveCount >= 3;
    }
 
    @Override
    protected void endPlay() {
        System.out.println("❌ TicTacToe: Ba ô thẳng hàng!");
    }
}
 
// Main
Game chess = new Chess();
chess.play();
 
System.out.println();
 
Game ttt = new TicTacToe();
ttt.play();

Strategy vs Template Method — So Sánh Trực Diện

Cả hai đều xử lý algorithm, nhưng cơ chế hoàn toàn khác:

Chiều So SánhStrategyTemplate Method
Cơ chếObject composition (giữ strategy object)Class inheritance (kế thừa base class)
Chọn algorithmLúc runtime — swap strategy objectLúc compile — do subclass quyết định
Biến đổi ở đâuToàn bộ algorithmTừng bước (step) của algorithm
Ai điều khiểnClient — gọi context.execute()Base class — gọi template method
Open/ClosedThêm algorithm = thêm class strategy mớiThêm biến thể = tạo subclass mới
Linh hoạtLinh hoạt hơn — strategy thay được lúc runtimeÍt linh hoạt hơn — locked với một subclass
Hollywood PrincipleKhông — context gọi strategyCó — "Đừng gọi chúng tôi, chúng tôi sẽ gọi bạn"
Phù hợp nhấtAlgorithm khác nhau hoàn toàn cùng mục đíchCùng luồng algorithm, chỉ khác ở từng bước

Câu hỏi then chốt: Bạn có thể swap behavior lúc runtime sau khi object đã được tạo không?

  • Có → Strategy (inject một strategy object khác)
  • Không, behavior được baked-in khi chọn class → Template Method (dùng subclass khác)

Kết Hợp Strategy với Template Method

Trong hệ thống thực, hai pattern này thường dùng chung. Template Method định nghĩa toàn bộ workflow; từng bước bên trong ủy quyền cho Strategy object để chọn algorithm chi tiết:

// Template Method định nghĩa thứ tự
// Strategy xử lý sub-algorithm biến đổi
 
interface SortAlgorithm {
  sort<T>(data: T[]): T[];
}
 
abstract class DataProcessor {
  constructor(protected sortStrategy: SortAlgorithm) {}
 
  // Template method — processing pipeline
  public process(data: number[]): void {
    const validated = this.validate(data);           // abstract
    const sorted = this.sortStrategy.sort(validated); // Strategy!
    const result = this.transform(sorted);           // abstract
    this.output(result);                             // bất biến
  }
 
  protected abstract validate(data: number[]): number[];
  protected abstract transform(data: number[]): number[];
 
  private output(data: number[]): void {
    console.log("Kết quả:", data.join(", "));
  }
}
 
class FilterProcessor extends DataProcessor {
  protected validate(data: number[]): number[] {
    return data.filter(n => n > 0); // loại số âm
  }
 
  protected transform(data: number[]): number[] {
    return data.map(n => n * 2); // nhân đôi mỗi giá trị
  }
}
 
// Usage
const processor = new FilterProcessor(new QuickSortStrategy());
processor.process([5, -1, 3, 0, 8, -2, 1]);
// Kết quả: 2, 6, 10, 16

Use Case Thực Tế

Strategy Xuất Hiện Ở Đâu

Hệ thống authentication:

interface AuthStrategy {
  authenticate(credentials: Record<string, string>): Promise<boolean>;
}
// JWTStrategy, OAuth2Strategy, APIKeyStrategy, BasicAuthStrategy

Logging backend:

interface LogStrategy {
  log(level: string, message: string): void;
}
// ConsoleLogStrategy, FileLogStrategy, ElasticSearchStrategy, DatadogStrategy

Caching policy:

interface CacheStrategy {
  get(key: string): any;
  set(key: string, value: any, ttl: number): void;
}
// InMemoryCache, RedisCache, NoOpCache (cho testing)

File storage:

interface StorageStrategy {
  upload(file: Buffer, path: string): Promise<string>;
}
// LocalStorageStrategy, S3Strategy, AzureBlobStrategy, GCSStrategy

Template Method Xuất Hiện Ở Đâu

Spring Framework — AbstractController, JdbcTemplate, HibernateTemplate đều dùng Template Method. Framework định nghĩa workflow (bắt đầu transaction → thực thi → commit/rollback), bạn chỉ override bước query/update.

JUnit test lifecycle — @BeforeEach, @Test, @AfterEach là Template Method: test runner điều khiển chuỗi setup → test → teardown, bạn implement từng phần.

React component lifecycle — componentDidMount, componentDidUpdate, componentWillUnmount là Template Method: React điều khiển lifecycle, bạn override phần cần.

Angular HTTP Interceptors — định nghĩa template: mỗi request đi qua intercept → transform → next.handle. Bạn implement bước transform.


Những Pitfall Thường Gặp

Pitfall Của Strategy

Pitfall 1: Thổi phồng strategy interface

// ❌ Ép tất cả strategy implement mọi thứ
interface OverloadedStrategy {
  sort(data: any[]): any[];
  validate(data: any[]): boolean;
  transform(data: any[]): any[];
  // ...
}
 
// ✅ Giữ strategy interface tập trung vào MỘT algorithm
interface SortStrategy {
  sort<T>(data: T[]): T[];
}

Pitfall 2: Dùng Strategy khi một function đơn giản là đủ

// ❌ Overkill cho operation một method đơn giản
class AddTaxStrategy implements PriceStrategy {
  calculate(price: number): number { return price * 1.1; }
}
 
// ✅ Truyền function trực tiếp
function applyPricing(price: number, strategy: (p: number) => number) {
  return strategy(price);
}
applyPricing(100, price => price * 1.1);

Pitfall 3: Cho phép strategy null

// ❌ Crash lúc runtime nếu strategy chưa được set
class Context {
  private strategy?: Strategy;  // Có thể undefined!
  execute() { this.strategy!.run(); }
}
 
// ✅ Dùng Null Object Pattern hoặc enforce qua constructor
class Context {
  constructor(private strategy: Strategy) {}  // Bắt buộc khi khởi tạo
  execute() { this.strategy.run(); }
}

Pitfall Của Template Method

Pitfall 1: Khai báo quá nhiều abstract method

// ❌ Quá nhiều abstract step = gánh nặng cho subclass
abstract class Bloated {
  template() { this.a(); this.b(); this.c(); this.d(); this.e(); }
  abstract a(): void;
  abstract b(): void;
  abstract c(): void;
  abstract d(): void;
  abstract e(): void;
}
 
// ✅ Giữ abstract method tối giản; biến phần còn lại thành hook với default
abstract class Lean {
  template() { this.a(); this.b(); this.c(); }
  abstract a(): void;     // bắt buộc override
  protected b(): void {}  // hook tùy chọn
  private c(): void { console.log("bất biến"); } // luôn như nhau
}

Pitfall 2: Gọi abstract method trong constructor

// ❌ Nguy hiểm — field của subclass chưa được khởi tạo trong Java/TypeScript
abstract class Bad {
  constructor() {
    this.init(); // init() của subclass có thể reference field chưa khởi tạo!
  }
  abstract init(): void;
}
 
// ✅ Gọi abstract method từ template method, không phải constructor
abstract class Good {
  public run(): void {
    this.init(); // Gọi sau khi construction — an toàn
    this.execute();
  }
  protected abstract init(): void;
  protected abstract execute(): void;
}

Pitfall 3: Override chính template method

Toàn bộ ý nghĩa của Template Method là skeleton không thay đổi được. Nếu subclass override đúng template method, chúng phá vỡ contract.

// ✅ Đánh dấu template method là final (Java); TypeScript dùng quy ước + linting
abstract class AbstractProcessor {
  // "final" trong Java; TypeScript dùng readonly arrow function
  public readonly process = (data: string): void => {
    this.preProcess(data);
    this.execute(data);
    this.postProcess(data);
  };
 
  protected abstract execute(data: string): void;
  protected preProcess(data: string): void {}  // hook
  protected postProcess(data: string): void {} // hook
}

Tổng Kết

Cả Strategy và Template Method đều là behavioral pattern để quản lý sự biến đổi của algorithm — nhưng mỗi cái có chỗ đứng riêng.

Strategy Pattern — chọn algorithm qua composition:

✅ Tách biến thể algorithm ra các class riêng sau một interface chung
✅ Swap algorithm lúc runtime — không cần động đến context
✅ Loại bỏ conditional branching (if/else, switch theo type)
✅ Mỗi strategy test được độc lập
✅ Thêm algorithm mới không cần sửa code hiện có (Open/Closed)

Template Method Pattern — skeleton algorithm qua inheritance:

✅ Định nghĩa cấu trúc algorithm một lần trong base class
✅ Subclass chỉ override bước biến đổi — không trùng lặp code
✅ Hook method cung cấp điểm customization tùy chọn
✅ Hollywood Principle: base class điều khiển luồng, subclass điền chi tiết
✅ Đảm bảo algorithm thực thi đúng thứ tự

Hướng dẫn quyết định:

  • Cần thay behavior lúc runtime? → Strategy
  • Cấu trúc algorithm cố định, chỉ bước bên trong thay đổi? → Template Method
  • Cần cả hai? → Kết hợp: Template Method cho skeleton, Strategy cho bước biến đổi

Bước Tiếp Theo

Còn đúng một bài nữa trong series này — bộ ba behavioral pattern cuối cùng:

➡️ Tiếp theo: Observer, Command và State Patterns — event-driven behavior, undo/redo và state machine. Sau đó, series OOP & Design Patterns sẽ hoàn tất!

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.