Back to blog

Decorator và Proxy Pattern: Mở Rộng và Kiểm Soát Đối Tượng

oopstructural-patternsdesign-patternstypescriptpythonjava
Decorator và Proxy Pattern: Mở Rộng và Kiểm Soát Đối Tượng

Giới Thiệu

Ở bài trước, bạn đã học hai structural pattern bọc đối tượng: Adapter chuyển đổi interface, Facade đơn giản hóa hệ thống con. Giờ mình sẽ xem tiếp hai kiểu bọc nữa — nhưng mục đích hoàn toàn khác:

PatternTóm Tắt
DecoratorThêm hành vi mới cho đối tượng lúc runtime, không cần sửa class
ProxyKiểm soát quyền truy cập — lazy loading, caching, phân quyền, logging

Cả hai đều bọc đối tượng và implement cùng interface. Điểm khác biệt nằm ở mục đích: Decorator mở rộng khả năng của đối tượng. Proxy kiểm soát cách đối tượng được truy cập.

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

✅ Thêm behavior linh hoạt với Decorator — không còn bùng nổ subclass
✅ Xếp chồng nhiều decorator để ghép hành vi phức tạp
✅ Kiểm soát truy cập với Proxy — lazy loading, caching, protection, logging
✅ Triển khai cả hai pattern trong TypeScript, Python và Java
✅ Phân biệt Decorator vs Proxy vs Adapter — ba kiểu bọc, ba mục đích khác nhau
✅ Tránh những lỗi phổ biến khi bọc đối tượng

Điều Kiện Tiên Quyết


Phần 1: Decorator Pattern

Vấn Đề: Bùng Nổ Subclass

Bạn đang xây hệ thống thông báo. Ban đầu chỉ có gửi email:

class EmailNotifier {
  send(message: string): void {
    console.log(`Email: ${message}`);
  }
}

Rồi yêu cầu ngày càng nhiều:

  • Có user muốn thêm SMS
  • Có user muốn thêm Slack
  • Có user muốn email + SMS
  • Có user muốn email + Slack
  • Có user muốn cả ba

Nếu dùng inheritance, bạn cần một subclass cho mỗi tổ hợp:

// ❌ Bùng nổ subclass — 2^n tổ hợp
class EmailNotifier { /* ... */ }
class SMSNotifier extends EmailNotifier { /* ... */ }
class SlackNotifier extends EmailNotifier { /* ... */ }
class EmailAndSMSNotifier extends EmailNotifier { /* ... */ }
class EmailAndSlackNotifier extends EmailNotifier { /* ... */ }
class SMSAndSlackNotifier extends EmailNotifier { /* ... */ }
class EmailSMSAndSlackNotifier extends EmailNotifier { /* ... */ }
// Thêm push notification? Giờ là 2^4 = 16 class ❌

Ba kênh thông báo = 7 class. Bốn kênh = 15 class. Không scale được.

Decorator pattern giải quyết chuyện này: bọc đối tượng bằng behavior bổ sung lúc runtime, xếp chồng bao nhiêu cũng được.


Cấu Trúc Pattern

Thành phần:

  • Component — interface chung cho cả đối tượng gốc và decorator (Notifier)
  • ConcreteComponent — đối tượng gốc được decorate (EmailNotifier)
  • BaseDecorator — giữ reference tới component bên trong, ủy thác mọi lời gọi
  • ConcreteDecorators — kế thừa base decorator, thêm hành vi cụ thể (SMSDecorator, SlackDecorator)

Điểm hay: Vì decorator implement cùng interface với component, bạn có thể xếp chồng vô hạn. Mỗi decorator bọc cái trước nó như búp bê Matryoshka.


TypeScript Implementation

Hệ Thống Thông Báo

// Component interface
interface Notifier {
  send(message: string): void;
}
 
// Concrete component — đối tượng gốc
class EmailNotifier implements Notifier {
  constructor(private email: string) {}
 
  send(message: string): void {
    console.log(`📧 Email to ${this.email}: ${message}`);
  }
}
 
// Base decorator — cùng interface, bọc một component
abstract class NotifierDecorator implements Notifier {
  constructor(protected wrapped: Notifier) {}
 
  send(message: string): void {
    this.wrapped.send(message);
  }
}
 
// Concrete decorator — mỗi cái thêm một behavior
class SMSDecorator extends NotifierDecorator {
  constructor(wrapped: Notifier, private phone: string) {
    super(wrapped);
  }
 
  send(message: string): void {
    super.send(message); // ủy thác cho wrapped
    console.log(`📱 SMS to ${this.phone}: ${message}`);
  }
}
 
class SlackDecorator extends NotifierDecorator {
  constructor(wrapped: Notifier, private channel: string) {
    super(wrapped);
  }
 
  send(message: string): void {
    super.send(message); // ủy thác cho wrapped
    console.log(`💬 Slack #${this.channel}: ${message}`);
  }
}
 
class PushDecorator extends NotifierDecorator {
  constructor(wrapped: Notifier, private deviceId: string) {
    super(wrapped);
  }
 
  send(message: string): void {
    super.send(message); // ủy thác cho wrapped
    console.log(`🔔 Push to ${this.deviceId}: ${message}`);
  }
}
 
// Dùng thế nào — xếp decorator theo preference của user
let notifier: Notifier = new EmailNotifier("[email protected]");
 
// User muốn email + SMS + Slack
notifier = new SMSDecorator(notifier, "+1234567890");
notifier = new SlackDecorator(notifier, "alerts");
 
notifier.send("Server is down!");
// Output:
// 📧 Email to [email protected]: Server is down!
// 📱 SMS to +1234567890: Server is down!
// 💬 Slack #alerts: Server is down!

Khác biệt với Adapter: Adapter thay đổi interface (chuyển A thành B). Decorator giữ nguyên interface và thêm behavior lên trên.


Ví Dụ Coffee Shop (Kinh Điển)

Ví dụ decorator cổ điển nhất — xây order cà phê bằng cách xếp chồng topping:

interface Coffee {
  getCost(): number;
  getDescription(): string;
}
 
class SimpleCoffee implements Coffee {
  getCost(): number { return 5; }
  getDescription(): string { return "Simple coffee"; }
}
 
class Espresso implements Coffee {
  getCost(): number { return 6; }
  getDescription(): string { return "Espresso"; }
}
 
// Base decorator
abstract class CoffeeDecorator implements Coffee {
  constructor(protected coffee: Coffee) {}
  getCost(): number { return this.coffee.getCost(); }
  getDescription(): string { return this.coffee.getDescription(); }
}
 
class MilkDecorator extends CoffeeDecorator {
  getCost(): number { return this.coffee.getCost() + 2; }
  getDescription(): string { return this.coffee.getDescription() + ", milk"; }
}
 
class SugarDecorator extends CoffeeDecorator {
  getCost(): number { return this.coffee.getCost() + 1; }
  getDescription(): string { return this.coffee.getDescription() + ", sugar"; }
}
 
class WhipDecorator extends CoffeeDecorator {
  getCost(): number { return this.coffee.getCost() + 3; }
  getDescription(): string { return this.coffee.getDescription() + ", whipped cream"; }
}
 
// Order: Espresso với sữa và kem tươi
let order: Coffee = new Espresso();
order = new MilkDecorator(order);
order = new WhipDecorator(order);
 
console.log(order.getDescription()); // "Espresso, milk, whipped cream"
console.log(`$${order.getCost()}`);  // "$11"

Python Implementation

from abc import ABC, abstractmethod
 
 
# Component interface
class Notifier(ABC):
    @abstractmethod
    def send(self, message: str) -> None:
        pass
 
 
# Concrete component
class EmailNotifier(Notifier):
    def __init__(self, email: str):
        self._email = email
 
    def send(self, message: str) -> None:
        print(f"📧 Email to {self._email}: {message}")
 
 
# Base decorator
class NotifierDecorator(Notifier):
    def __init__(self, wrapped: Notifier):
        self._wrapped = wrapped
 
    def send(self, message: str) -> None:
        self._wrapped.send(message)
 
 
# Concrete decorator
class SMSDecorator(NotifierDecorator):
    def __init__(self, wrapped: Notifier, phone: str):
        super().__init__(wrapped)
        self._phone = phone
 
    def send(self, message: str) -> None:
        super().send(message)
        print(f"📱 SMS to {self._phone}: {message}")
 
 
class SlackDecorator(NotifierDecorator):
    def __init__(self, wrapped: Notifier, channel: str):
        super().__init__(wrapped)
        self._channel = channel
 
    def send(self, message: str) -> None:
        super().send(message)
        print(f"💬 Slack #{self._channel}: {message}")
 
 
# Sử dụng
notifier: Notifier = EmailNotifier("[email protected]")
notifier = SMSDecorator(notifier, "+1234567890")
notifier = SlackDecorator(notifier, "alerts")
 
notifier.send("Server is down!")
# 📧 Email to [email protected]: Server is down!
# 📱 SMS to +1234567890: Server is down!
# 💬 Slack #alerts: Server is down!

Python Đặc Biệt: Function Decorator

Python có cú pháp decorator tích hợp (@decorator) — thực ra chính là Decorator pattern áp dụng cho function:

import time
import functools
 
 
def timer(func):
    """Decorator đo thời gian thực thi"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper
 
 
def retry(max_attempts: int = 3):
    """Decorator retry khi gặp lỗi"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"Attempt {attempt} failed: {e}. Retrying...")
        return wrapper
    return decorator
 
 
# Xếp chồng decorator — cùng concept với class-based decorator
@timer
@retry(max_attempts=3)
def fetch_data(url: str) -> dict:
    print(f"Fetching {url}...")
    return {"status": "ok"}
 
 
fetch_data("https://api.example.com/data")
# Fetching https://api.example.com/data...
# fetch_data took 0.0001s

Cùng concept, khác cú pháp. @decorator trong Python là syntactic sugar cho Decorator pattern. @timer bọc fetch_data giống hệt cách SMSDecorator bọc EmailNotifier.


Java Implementation

// Component interface
public interface Notifier {
    void send(String message);
}
 
// Concrete component
public class EmailNotifier implements Notifier {
    private final String email;
 
    public EmailNotifier(String email) {
        this.email = email;
    }
 
    @Override
    public void send(String message) {
        System.out.printf("📧 Email to %s: %s%n", email, message);
    }
}
 
// Base decorator
public abstract class NotifierDecorator implements Notifier {
    protected final Notifier wrapped;
 
    public NotifierDecorator(Notifier wrapped) {
        this.wrapped = wrapped;
    }
 
    @Override
    public void send(String message) {
        wrapped.send(message);
    }
}
 
// Concrete decorator
public class SMSDecorator extends NotifierDecorator {
    private final String phone;
 
    public SMSDecorator(Notifier wrapped, String phone) {
        super(wrapped);
        this.phone = phone;
    }
 
    @Override
    public void send(String message) {
        super.send(message);
        System.out.printf("📱 SMS to %s: %s%n", phone, message);
    }
}
 
public class SlackDecorator extends NotifierDecorator {
    private final String channel;
 
    public SlackDecorator(Notifier wrapped, String channel) {
        super(wrapped);
        this.channel = channel;
    }
 
    @Override
    public void send(String message) {
        super.send(message);
        System.out.printf("💬 Slack #%s: %s%n", channel, message);
    }
}
 
// Sử dụng
Notifier notifier = new EmailNotifier("[email protected]");
notifier = new SMSDecorator(notifier, "+1234567890");
notifier = new SlackDecorator(notifier, "alerts");
 
notifier.send("Server is down!");

Java Standard Library: java.io Stream Decorator

Thư viện I/O của Java được xây hoàn toàn trên Decorator pattern:

// Mỗi wrapper thêm một khả năng — xếp chồng Decorator kinh điển
InputStream raw = new FileInputStream("data.gz");              // component gốc
InputStream buffered = new BufferedInputStream(raw);            // + buffering
InputStream decompressed = new GZIPInputStream(buffered);       // + giải nén
Reader reader = new InputStreamReader(decompressed, "UTF-8");   // + decode ký tự
BufferedReader lines = new BufferedReader(reader);              // + đọc theo dòng
 
String line;
while ((line = lines.readLine()) != null) {
    System.out.println(line);
}

Decorator ngoài đời thật. Ai code Java cũng dùng pattern này hàng ngày — BufferedReader, GZIPInputStream, DataInputStream đều là decorator bọc base InputStream.


Ví Dụ Decorator Thực Tế

HTTP Middleware (Express.js / Hono)

Middleware trong web framework chính là Decorator pattern áp dụng cho request handler:

// Mỗi middleware "decorate" request pipeline
interface Handler {
  handle(request: Request): Response;
}
 
class BaseHandler implements Handler {
  handle(request: Request): Response {
    return new Response("OK", { status: 200 });
  }
}
 
class LoggingMiddleware implements Handler {
  constructor(private next: Handler) {}
 
  handle(request: Request): Response {
    console.log(`${request.method} ${request.url}`);
    const start = Date.now();
    const response = this.next.handle(request);
    console.log(`Completed in ${Date.now() - start}ms`);
    return response;
  }
}
 
class AuthMiddleware implements Handler {
  constructor(private next: Handler) {}
 
  handle(request: Request): Response {
    const token = request.headers.get("Authorization");
    if (!token) {
      return new Response("Unauthorized", { status: 401 });
    }
    return this.next.handle(request);
  }
}
 
class RateLimitMiddleware implements Handler {
  private requests = new Map<string, number>();
  constructor(private next: Handler, private limit: number = 100) {}
 
  handle(request: Request): Response {
    const ip = request.headers.get("X-Forwarded-For") ?? "unknown";
    const count = (this.requests.get(ip) ?? 0) + 1;
    this.requests.set(ip, count);
 
    if (count > this.limit) {
      return new Response("Too Many Requests", { status: 429 });
    }
    return this.next.handle(request);
  }
}
 
// Xếp chồng middleware — thứ tự rất quan trọng!
let handler: Handler = new BaseHandler();
handler = new AuthMiddleware(handler);        // check auth trước
handler = new RateLimitMiddleware(handler);   // rate limit trước auth
handler = new LoggingMiddleware(handler);     // log mọi thứ

Phần 2: Proxy Pattern

Vấn Đề: Kiểm Soát Truy Cập Đối Tượng

Bạn có một ReportGenerator query database, xử lý hàng triệu dòng, rồi tạo PDF. Tốn 30 giây mỗi lần:

class ReportGenerator {
  constructor() {
    // Kết nối database, load config...
    console.log("Initializing report generator (30 seconds)...");
  }
 
  generate(reportType: string): Buffer {
    console.log(`Generating ${reportType} report...`);
    // Xử lý nặng
    return Buffer.from("PDF content");
  }
}

Vấn đề khi dùng trực tiếp:

  1. Chi phí khởi tạo — đối tượng được tạo ngay cả khi user chẳng bao giờ yêu cầu report
  2. Không cache — tạo cùng report hai lần là lãng phí
  3. Không phân quyền — ai cũng tạo report gì cũng được
  4. Không log — không biết ai tạo report gì

Proxy pattern bọc đối tượng thật và thêm kiểm soát mà không thay đổi interface.


Cấu Trúc Pattern

Thành phần:

  • Subject — interface chung cho đối tượng thật và proxy (ReportService)
  • RealSubject — đối tượng thật làm công việc nặng (ReportGenerator)
  • Proxy — bọc đối tượng thật, kiểm soát truy cập (ReportProxy)

Các Loại Proxy

Loại ProxyMục ĐíchVí Dụ
Virtual ProxyTrì hoãn khởi tạo đến khi cần (lazy loading)Load ảnh nặng chỉ khi hiển thị
Protection ProxyPhân quyền truy cậpCheck role trước khi cho phép thao tác
Caching ProxyCache kết quả để tránh lặp việc tốn kémCache API response 5 phút
Logging ProxyGhi log thao tác để monitor/debugLog mọi database query

TypeScript Implementation

Virtual Proxy (Lazy Loading)

interface Image {
  display(): void;
  getSize(): { width: number; height: number };
}
 
// Real subject — tạo tốn kém
class HighResImage implements Image {
  private data: Buffer;
 
  constructor(private filename: string) {
    // Load ảnh 50MB từ disk
    console.log(`Loading ${filename} from disk... (slow)`);
    this.data = Buffer.alloc(50 * 1024 * 1024); // 50MB
  }
 
  display(): void {
    console.log(`Displaying ${this.filename} (${this.data.length} bytes)`);
  }
 
  getSize(): { width: number; height: number } {
    return { width: 3840, height: 2160 };
  }
}
 
// Virtual proxy — trì hoãn loading đến khi gọi display()
class ImageProxy implements Image {
  private realImage?: HighResImage;
 
  constructor(private filename: string) {
    // Chưa load gì — chỉ lưu filename
    console.log(`Created proxy for ${filename} (no disk I/O yet)`);
  }
 
  display(): void {
    if (!this.realImage) {
      this.realImage = new HighResImage(this.filename); // load lần đầu
    }
    this.realImage.display();
  }
 
  getSize(): { width: number; height: number } {
    // Trả metadata mà không cần load ảnh
    return { width: 3840, height: 2160 };
  }
}
 
// Tạo 100 proxy tức thì, chỉ load khi hiển thị
const gallery: Image[] = [];
for (let i = 0; i < 100; i++) {
  gallery.push(new ImageProxy(`photo_${i}.jpg`)); // instant — không disk I/O
}
 
// Chỉ ảnh này mới load từ disk
gallery[42].display();

Caching Proxy

interface WeatherService {
  getWeather(city: string): WeatherData;
}
 
interface WeatherData {
  city: string;
  temperature: number;
  humidity: number;
  fetchedAt: Date;
}
 
// Real subject — gọi API bên ngoài (chậm, rate limit)
class OpenWeatherService implements WeatherService {
  getWeather(city: string): WeatherData {
    console.log(`Calling OpenWeather API for ${city}...`);
    return {
      city,
      temperature: 20 + Math.random() * 15,
      humidity: 40 + Math.random() * 40,
      fetchedAt: new Date(),
    };
  }
}
 
// Caching proxy — cache kết quả với TTL tùy chỉnh
class CachedWeatherService implements WeatherService {
  private cache = new Map<string, { data: WeatherData; expiresAt: number }>();
 
  constructor(
    private realService: WeatherService,
    private ttlMs: number = 5 * 60 * 1000 // mặc định 5 phút
  ) {}
 
  getWeather(city: string): WeatherData {
    const cached = this.cache.get(city);
 
    if (cached && Date.now() < cached.expiresAt) {
      console.log(`Cache hit for ${city}`);
      return cached.data;
    }
 
    console.log(`Cache miss for ${city}`);
    const data = this.realService.getWeather(city);
    this.cache.set(city, { data, expiresAt: Date.now() + this.ttlMs });
    return data;
  }
}
 
// Sử dụng
const weather: WeatherService = new CachedWeatherService(
  new OpenWeatherService(),
  5 * 60 * 1000 // cache 5 phút
);
 
weather.getWeather("London"); // Cache miss → gọi API
weather.getWeather("London"); // Cache hit → instant
weather.getWeather("Paris");  // Cache miss → gọi API
weather.getWeather("London"); // Cache hit → instant

Protection Proxy (Phân Quyền)

interface DocumentService {
  read(docId: string): string;
  write(docId: string, content: string): void;
  delete(docId: string): void;
}
 
type Role = "viewer" | "editor" | "admin";
 
// Real subject
class DocumentStore implements DocumentService {
  private docs = new Map<string, string>();
 
  read(docId: string): string {
    return this.docs.get(docId) ?? "Document not found";
  }
 
  write(docId: string, content: string): void {
    this.docs.set(docId, content);
    console.log(`Document ${docId} saved`);
  }
 
  delete(docId: string): void {
    this.docs.delete(docId);
    console.log(`Document ${docId} deleted`);
  }
}
 
// Protection proxy — kiểm tra quyền theo role
class SecureDocumentService implements DocumentService {
  constructor(
    private realService: DocumentService,
    private userRole: Role
  ) {}
 
  read(docId: string): string {
    // Mọi role đều được đọc
    return this.realService.read(docId);
  }
 
  write(docId: string, content: string): void {
    if (this.userRole === "viewer") {
      throw new Error("Access denied: viewer không được ghi document");
    }
    this.realService.write(docId, content);
  }
 
  delete(docId: string): void {
    if (this.userRole !== "admin") {
      throw new Error("Access denied: chỉ admin mới được xóa document");
    }
    this.realService.delete(docId);
  }
}
 
// Sử dụng
const adminDocs: DocumentService = new SecureDocumentService(
  new DocumentStore(), "admin"
);
adminDocs.write("doc1", "Hello");  // ✅ OK
adminDocs.delete("doc1");          // ✅ OK
 
const viewerDocs: DocumentService = new SecureDocumentService(
  new DocumentStore(), "viewer"
);
viewerDocs.read("doc1");           // ✅ OK
viewerDocs.write("doc1", "Hack");  // ❌ throws "Access denied"

Logging Proxy

interface Database {
  query(sql: string): unknown[];
  execute(sql: string): void;
}
 
class PostgresDatabase implements Database {
  query(sql: string): unknown[] {
    return [{ id: 1, name: "Alice" }];
  }
 
  execute(sql: string): void {
    // Thực thi statement
  }
}
 
// Logging proxy — ghi log mọi thao tác
class LoggingDatabase implements Database {
  constructor(private realDb: Database) {}
 
  query(sql: string): unknown[] {
    const start = performance.now();
    const result = this.realDb.query(sql);
    const elapsed = (performance.now() - start).toFixed(2);
 
    console.log(`[QUERY] ${sql} → ${result.length} rows (${elapsed}ms)`);
    return result;
  }
 
  execute(sql: string): void {
    const start = performance.now();
    this.realDb.execute(sql);
    const elapsed = (performance.now() - start).toFixed(2);
 
    console.log(`[EXEC] ${sql} (${elapsed}ms)`);
  }
}
 
// Dùng logging proxy khi develop
const db: Database = new LoggingDatabase(new PostgresDatabase());
db.query("SELECT * FROM users WHERE role = 'admin'");
// [QUERY] SELECT * FROM users WHERE role = 'admin' → 1 rows (0.12ms)

Python Implementation

from abc import ABC, abstractmethod
from functools import lru_cache
import time
 
 
# ---- Virtual Proxy ----
 
class Image(ABC):
    @abstractmethod
    def display(self) -> None:
        pass
 
 
class HighResImage(Image):
    def __init__(self, filename: str):
        self._filename = filename
        self._load_from_disk()
 
    def _load_from_disk(self) -> None:
        print(f"Loading {self._filename} from disk... (slow)")
        time.sleep(0.1)  # giả lập I/O chậm
 
    def display(self) -> None:
        print(f"Displaying {self._filename}")
 
 
class ImageProxy(Image):
    def __init__(self, filename: str):
        self._filename = filename
        self._real_image: HighResImage | None = None
        print(f"Created proxy for {self._filename} (no disk I/O yet)")
 
    def display(self) -> None:
        if self._real_image is None:
            self._real_image = HighResImage(self._filename)
        self._real_image.display()
 
 
# Sử dụng
gallery = [ImageProxy(f"photo_{i}.jpg") for i in range(100)]  # instant
gallery[42].display()  # chỉ ảnh này mới load
 
 
# ---- Caching Proxy ----
 
class WeatherService(ABC):
    @abstractmethod
    def get_weather(self, city: str) -> dict:
        pass
 
 
class OpenWeatherService(WeatherService):
    def get_weather(self, city: str) -> dict:
        print(f"Calling API for {city}...")
        return {"city": city, "temp": 22.5, "humidity": 65}
 
 
class CachedWeatherService(WeatherService):
    def __init__(self, real_service: WeatherService, ttl_seconds: float = 300):
        self._real_service = real_service
        self._ttl = ttl_seconds
        self._cache: dict[str, tuple[dict, float]] = {}
 
    def get_weather(self, city: str) -> dict:
        cached = self._cache.get(city)
        if cached and time.time() < cached[1]:
            print(f"Cache hit for {city}")
            return cached[0]
 
        print(f"Cache miss for {city}")
        data = self._real_service.get_weather(city)
        self._cache[city] = (data, time.time() + self._ttl)
        return data
 
 
# Sử dụng
weather = CachedWeatherService(OpenWeatherService(), ttl_seconds=300)
weather.get_weather("London")  # cache miss → gọi API
weather.get_weather("London")  # cache hit → instant
 
 
# ---- Protection Proxy ----
 
class DocumentService(ABC):
    @abstractmethod
    def read(self, doc_id: str) -> str:
        pass
 
    @abstractmethod
    def write(self, doc_id: str, content: str) -> None:
        pass
 
    @abstractmethod
    def delete(self, doc_id: str) -> None:
        pass
 
 
class DocumentStore(DocumentService):
    def __init__(self):
        self._docs: dict[str, str] = {}
 
    def read(self, doc_id: str) -> str:
        return self._docs.get(doc_id, "Not found")
 
    def write(self, doc_id: str, content: str) -> None:
        self._docs[doc_id] = content
        print(f"Document {doc_id} saved")
 
    def delete(self, doc_id: str) -> None:
        self._docs.pop(doc_id, None)
        print(f"Document {doc_id} deleted")
 
 
class SecureDocumentService(DocumentService):
    def __init__(self, real_service: DocumentService, user_role: str):
        self._real = real_service
        self._role = user_role
 
    def read(self, doc_id: str) -> str:
        return self._real.read(doc_id)
 
    def write(self, doc_id: str, content: str) -> None:
        if self._role == "viewer":
            raise PermissionError("Viewer không được ghi document")
        self._real.write(doc_id, content)
 
    def delete(self, doc_id: str) -> None:
        if self._role != "admin":
            raise PermissionError("Chỉ admin mới được xóa document")
        self._real.delete(doc_id)

Java Implementation

// ---- Virtual Proxy ----
 
public interface Image {
    void display();
}
 
public class HighResImage implements Image {
    private final String filename;
 
    public HighResImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }
 
    private void loadFromDisk() {
        System.out.printf("Loading %s from disk... (slow)%n", filename);
    }
 
    @Override
    public void display() {
        System.out.printf("Displaying %s%n", filename);
    }
}
 
public class ImageProxy implements Image {
    private final String filename;
    private HighResImage realImage;
 
    public ImageProxy(String filename) {
        this.filename = filename;
        System.out.printf("Created proxy for %s (no disk I/O yet)%n", filename);
    }
 
    @Override
    public void display() {
        if (realImage == null) {
            realImage = new HighResImage(filename);
        }
        realImage.display();
    }
}
 
// Sử dụng
List<Image> gallery = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    gallery.add(new ImageProxy("photo_" + i + ".jpg")); // instant
}
gallery.get(42).display(); // chỉ ảnh này mới load
// ---- Caching Proxy ----
 
public interface WeatherService {
    WeatherData getWeather(String city);
}
 
public record WeatherData(String city, double temperature, double humidity) {}
 
public class OpenWeatherService implements WeatherService {
    @Override
    public WeatherData getWeather(String city) {
        System.out.printf("Calling API for %s...%n", city);
        return new WeatherData(city, 22.5, 65);
    }
}
 
public class CachedWeatherService implements WeatherService {
    private final WeatherService realService;
    private final long ttlMillis;
    private final Map<String, CacheEntry> cache = new HashMap<>();
 
    private record CacheEntry(WeatherData data, long expiresAt) {}
 
    public CachedWeatherService(WeatherService realService, long ttlMillis) {
        this.realService = realService;
        this.ttlMillis = ttlMillis;
    }
 
    @Override
    public WeatherData getWeather(String city) {
        CacheEntry cached = cache.get(city);
        if (cached != null && System.currentTimeMillis() < cached.expiresAt()) {
            System.out.printf("Cache hit for %s%n", city);
            return cached.data();
        }
 
        System.out.printf("Cache miss for %s%n", city);
        WeatherData data = realService.getWeather(city);
        cache.put(city, new CacheEntry(data, System.currentTimeMillis() + ttlMillis));
        return data;
    }
}
 
// Sử dụng
WeatherService weather = new CachedWeatherService(
    new OpenWeatherService(),
    5 * 60 * 1000 // 5 phút
);
weather.getWeather("London"); // cache miss
weather.getWeather("London"); // cache hit

Java Standard Library: java.lang.reflect.Proxy

Java có hỗ trợ dynamic proxy tích hợp — tạo proxy runtime mà không cần viết class:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
// Tạo logging proxy cho BẤT KỲ interface nào lúc runtime
public class LoggingHandler implements InvocationHandler {
    private final Object target;
 
    public LoggingHandler(Object target) {
        this.target = target;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.printf("[LOG] %s(%s)%n", method.getName(),
            args != null ? Arrays.toString(args) : "");
 
        long start = System.nanoTime();
        Object result = method.invoke(target, args);
        long elapsed = (System.nanoTime() - start) / 1_000_000;
 
        System.out.printf("[LOG] %s returned in %dms%n", method.getName(), elapsed);
        return result;
    }
}
 
// Sử dụng — bọc bất kỳ interface nào với logging
WeatherService realService = new OpenWeatherService();
WeatherService logged = (WeatherService) Proxy.newProxyInstance(
    WeatherService.class.getClassLoader(),
    new Class<?>[]{ WeatherService.class },
    new LoggingHandler(realService)
);
 
logged.getWeather("London");
// [LOG] getWeather([London])
// Calling API for London...
// [LOG] getWeather returned in 12ms

Decorator vs Proxy vs Adapter

Cả ba pattern đều bọc đối tượng. Đây là cách phân biệt:

Khía CạnhDecoratorProxyAdapter
Mục đíchThêm behavior mớiKiểm soát truy cậpChuyển đổi interface
InterfaceGiữ nguyênGiữ nguyênKhác với đối tượng bọc
Ai tạo đối tượng bọc?Client truyền vàoProxy tự tạo/quản lýClient truyền vào
Số lượng wrapperThường xếp chồng (nhiều)Thường mộtThường một
Khi nào dùngCần thêm/ghép behavior linh hoạtCần lazy loading, cache, auth, logCần tích hợp interface không tương thích
Ví vonGói quà (thêm lớp)Bảo vệ (kiểm soát)Ổ chuyển đổi điện (đổi đầu cắm)

Kết Hợp Nhiều Pattern

Trong thực tế, bạn thường kết hợp cả ba:

// Adapter — chuyển đổi interface bên thứ ba
class StripeAdapter implements PaymentService {
  constructor(private stripe: StripeSDK) {}
  charge(amount: number): Result {
    return this.stripe.createCharge(amount * 100); // convert interface
  }
}
 
// Caching Proxy — cache tỷ giá
class CachedExchangeRate implements ExchangeRateService {
  constructor(private real: ExchangeRateService) {}
  getRate(from: string, to: string): number {
    // check cache, gọi real nếu miss
  }
}
 
// Decorator — thêm retry logic cho bất kỳ payment service nào
class RetryDecorator implements PaymentService {
  constructor(private service: PaymentService, private maxRetries: number) {}
  charge(amount: number): Result {
    for (let i = 0; i < this.maxRetries; i++) {
      try { return this.service.charge(amount); }
      catch { if (i === this.maxRetries - 1) throw; }
    }
  }
}
 
// Ghép tất cả lại
const payment: PaymentService = new RetryDecorator(
  new StripeAdapter(new StripeSDK()),
  3
);

Sai Lầm Thường Gặp

1. Decorator Hỏng Khi Check Kiểu Cụ Thể

// ❌ Client check concrete type — decorator phá vỡ logic này
function process(notifier: Notifier) {
  if (notifier instanceof EmailNotifier) {
    // Sai khi notifier đã bị bọc trong decorator!
  }
}
 
// ✅ Dùng interface, đừng check concrete type
function process(notifier: Notifier) {
  notifier.send("Hello"); // hoạt động với mọi decorator stack
}

2. Quá Nhiều Lớp Decorator = Debug Muốn Khóc

// ❌ 10 lớp chồng — chúc debug vui vẻ
let service = new BaseService();
service = new LoggingDecorator(service);
service = new CachingDecorator(service);
service = new RetryDecorator(service);
service = new TimeoutDecorator(service);
service = new CircuitBreakerDecorator(service);
service = new MetricsDecorator(service);
service = new TracingDecorator(service);
service = new AuthDecorator(service);
service = new ValidationDecorator(service);
service = new RateLimitDecorator(service);
 
// ✅ Nhiều concern vậy thì nên dùng middleware pipeline
const pipeline = new Pipeline([
  new LoggingMiddleware(),
  new AuthMiddleware(),
  new RateLimitMiddleware(),
  new RetryMiddleware(),
]);

3. Proxy Thêm Method Ngoài Interface

// ❌ Proxy thêm method riêng — client không thể swap proxy với real object
class CachedWeatherProxy implements WeatherService {
  getWeather(city: string): WeatherData { /* ... */ }
  clearCache(): void { /* ... */ }  // ❌ không có trong interface
  getCacheStats(): Stats { /* ... */ } // ❌ không có trong interface
}
 
// ✅ Giữ proxy interface giống hệt real object
// Nếu cần quản lý cache thì tách ra interface riêng
interface CacheControl {
  clearCache(): void;
  getCacheStats(): Stats;
}
 
class CachedWeatherProxy implements WeatherService, CacheControl {
  // Client dùng như WeatherService — giống hệt real object
  // Code admin có thể cast sang CacheControl khi cần
}

4. Virtual Proxy Không Thread-Safe

// ❌ Race condition — hai thread tạo real object cùng lúc
class ImageProxy implements Image {
  private realImage?: HighResImage;
 
  display(): void {
    if (!this.realImage) {
      // Hai thread có thể vào đây đồng thời
      this.realImage = new HighResImage(this.filename);
    }
    this.realImage.display();
  }
}
// ✅ Thread-safe lazy initialization trong Java
public class ImageProxy implements Image {
    private volatile HighResImage realImage;
 
    @Override
    public void display() {
        if (realImage == null) {
            synchronized (this) {
                if (realImage == null) { // double-checked locking
                    realImage = new HighResImage(filename);
                }
            }
        }
        realImage.display();
    }
}

Tổng Kết

Decorator và Proxy là hai structural pattern đều bọc đối tượng — nhưng vì lý do khác nhau:

  • Decorator thêm behavior mới linh hoạt — là người mở rộng
  • Proxy kiểm soát truy cập đối tượng — là người gác cổng

Ghi nhớ:

✅ Dùng Decorator khi cần thêm behavior mà không sửa class — xếp chồng nhiều decorator cho tổ hợp linh hoạt
✅ Dùng Proxy cho lazy loading (Virtual), phân quyền (Protection), caching, hoặc logging
✅ Cả hai implement cùng interface với đối tượng bọc — client không phân biệt được
✅ Decorator thường xếp chồng (nhiều lớp); Proxy thường đơn lẻ (một wrapper)
✅ Cú pháp @decorator trong Python là Decorator pattern áp dụng cho function
✅ java.io stream và java.lang.reflect.Proxy là ví dụ thực tế trong standard library


Tiếp Theo

Các bài tiếp theo trong series OOP & Design Patterns:

  • OOP-14: Strategy và Template Method Patterns — behavioral pattern cho thuật toán hoán đổi linh hoạt
  • OOP-15: Observer, Command và State Patterns — behavioral pattern cho xử lý sự kiện và máy trạng thái

Bài Tập Thực Hành

  1. Text Formatting Decorator: Tạo interface TextComponent với render(): string. Dựng decorator cho Bold, Italic, Underline, Color và Link. Ghép chúng để render: text in đậm, màu đỏ, gạch chân, có link. Xác minh bỏ bất kỳ decorator nào vẫn ra output hợp lệ.

  2. API Client Với Nhiều Proxy: Dựng interface ApiClient với get(url): Response và post(url, body): Response. Tạo bốn proxy: (a) CachingProxy cache GET response, (b) LoggingProxy log mọi request, (c) RetryProxy retry request thất bại tối đa 3 lần, (d) AuthProxy gắn bearer token. Xếp chồng đúng thứ tự và giải thích tại sao thứ tự quan trọng.

  3. File System Phân Quyền: Tạo interface FileSystem với read, write, delete và listFiles. Triển khai Protection Proxy với ba role: guest (chỉ đọc + list), user (đọc + ghi + list), admin (toàn quyền). Viết test kiểm tra quyền từng role.

  4. Decorator hay Proxy?: Cho các tình huống sau, chọn pattern và giải thích: (a) Thêm compression cho network socket, (b) Chặn API access cho user không premium, (c) Thêm encryption cho file writer sẵn có, (d) Cache kết quả database query, (e) Thêm transaction support cho repository.


Phần 13 trong series OOP & Design Patterns. Tiếp nối Adapter và Facade Pattern.

📬 Đă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.