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:
| Pattern | Tó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
- Đã hoàn thành Decorator và Proxy Patterns
- Quen với Polymorphism và Interfaces
- Thoải mái với Inheritance và Composition
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 đề:
- Vi phạm Open/Closed Principle — muốn thêm phương tiện mới là phải sửa class này
- Mỗi phương tiện mới làm method phình to thêm một đống conditional
- Unit test buộc phải test tất cả các nhánh cùng nhau — không test riêng lẻ được
- 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 bayKhô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ơnPython 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.gzJava 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 fileNhư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ề falseVí 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ánh | Strategy | Template Method |
|---|---|---|
| Cơ chế | Object composition (giữ strategy object) | Class inheritance (kế thừa base class) |
| Chọn algorithm | Lúc runtime — swap strategy object | Lúc compile — do subclass quyết định |
| Biến đổi ở đâu | Toàn bộ algorithm | Từng bước (step) của algorithm |
| Ai điều khiển | Client — gọi context.execute() | Base class — gọi template method |
| Open/Closed | Thêm algorithm = thêm class strategy mới | Thêm biến thể = tạo subclass mới |
| Linh hoạt | Linh hoạt hơn — strategy thay được lúc runtime | Ít linh hoạt hơn — locked với một subclass |
| Hollywood Principle | Không — context gọi strategy | Có — "Đừng gọi chúng tôi, chúng tôi sẽ gọi bạn" |
| Phù hợp nhất | Algorithm khác nhau hoàn toàn cùng mục đích | Cù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, 16Use 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, BasicAuthStrategyLogging backend:
interface LogStrategy {
log(level: string, message: string): void;
}
// ConsoleLogStrategy, FileLogStrategy, ElasticSearchStrategy, DatadogStrategyCaching 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, GCSStrategyTemplate 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.