TungDaDev's Blog

decorator pattern

Decorator pattern.webp
Published on
/9 mins read/

Trong các bài học vỡ lòng, Decorator Pattern thường được gắn liền với ví dụ thêm sữa, thêm đường vào cốc cà phê. Nhưng trong thế giới phần mềm doanh nghiệp, Decorator Pattern chính là linh hồn của toàn bộ thư viện I/O cốt lõi trong Java (BufferedInputStream(GZIPInputStream(FileInputStream))), và là tiền thân trực tiếp của trường phái Lập trình Hướng Khía Cạnh (Aspect-Oriented Programming - AOP). Khi nào bạn nên tự viết một Decorator bằng tay, và khi nào nên để Spring AOP Dynamic Proxy can thiệp tự động? Cạm bẫy 'Self-Invocation' tai tiếng trong Spring có thể được hóa giải bằng Decorator như thế nào?

Bổ sung các hành vi phụ trợ (Cross-Cutting Concerns) như Caching, Ghi vết thời gian (Metric Timing), Retry hay Ghi Log bảo mật mà không làm ô nhiễm mã nguồn nghiệp vụ chính là bài toán then chốt của Clean Architecture.

Bài viết này phân tích ranh giới kiến trúc giữa GoF Decorator truyền thống và Spring AOP Proxies (JDK Dynamic Proxy vs CGLIB), đồng thời xây dựng một Resilient Caching Decorator chuẩn production.


# bản chất kiến trúc: hóa giải "vụ nổ lớp" (class explosion)

Hãy tưởng tượng bạn có một PaymentService cơ bản. Yêu cầu nghiệp vụ phát sinh:

  1. Cần thêm tính năng mã hóa dữ liệu (Encryption).
  2. Cần thêm cơ chế ghi log kiểm toán (Audit Logging).
  3. Cần thêm bộ nhớ đệm (Caching).

Nếu sử dụng phương pháp Kế thừa (Inheritance) truyền thống, bạn sẽ rơi vào thảm họa Vụ Nổ Lớp (Combinatorial Class Explosion):

# nguyên lý cốt lõi của Decorator

  • Triển khai cùng một Interface với đối tượng gốc.
  • Giữ một tham chiếu (private final T delegate) tới đối tượng bên trong.
  • Cho phép bọc lồng nhau tùy ý tại thời điểm Runtime:
    PaymentService service = new LoggingDecorator(new EncryptionDecorator(new CorePaymentService()));

# di sản kinh điển: Java.io package là một Decorator thuần khiết

Ví dụ thực tế vĩ đại nhất của Decorator Pattern trong Java Standard Library chính là gói java.io:

// Nghệ thuật lồng ghép Decorator của JDK:
InputStream is = new BufferedInputStream( // Decorator 2: Thêm bộ đệm RAM 8KB
                    new GZIPInputStream( // Decorator 1: Giải nén luồng Gzip on-the-fly
                        new FileInputStream("data.gz") // Component gốc: Đọc byte thô từ Disk
                    )
                 );

Mỗi lớp Decorator chỉ tập trung giải quyết đúng một trách nhiệm duy nhất (Single Responsibility Principle - SRP):

  • FileInputStream: Chịu trách nhiệm mở kết nối file với hệ điều hành.
  • GZIPInputStream: Chịu trách nhiệm giải thuật nén.
  • BufferedInputStream: Chịu trách nhiệm giảm số lần gọi System Call read() vào Kernel.

# Decorator thủ công vs Spring AOP Proxy: cuộc đối đầu kiến trúc

Trong hệ sinh thái Spring, hầu hết các tác vụ của Decorator được tự động hóa bằng Spring AOP (@Transactional, @Cacheable, @Retryable). Tuy nhiên, hai cách tiếp cận này có sự khác biệt rất lớn về mặt cơ học:

# so sánh chi tiết kỹ thuật

Tiêu chuẩn Kiến trúcManual GoF DecoratorSpring AOP Dynamic Proxy
Cơ chế hoạt độngỦy nhiệm tường minh (Explicit Delegation)Can thiệp động bằng Bytecode (CGLIB / JDK Proxy)
Tính minh bạch (Debugging)Cực cao: Stack trace thẳng hàng, dễ đặt breakpointThấp: Stack trace chứa hàng chục lớp proxy nội bộ của Spring
Cạm bẫy Self-InvocationKHÔNG BỊ ẢNH HƯỞNGBỊ LỖI CHÍ MẠNG: Gọi this.internalMethod() sẽ bỏ qua toàn bộ Proxy!
Boilerplate CodePhải viết class wrapper và override toàn bộ methodsRất ít code: Chỉ cần gắn một annotation (vd: @Cacheable)
Khuyến nghị kiến trúcDùng cho Core SDK, luồng xử lý hiệu năng cao cần kiểm soát chặtDùng cho các tác vụ Cross-cutting toàn cục trong Spring Boot

# cạm bẫy self-invocation trong Spring AOP & cách Decorator hóa giải

Một trong những lỗi kinh điển nhất của lập trình viên Spring Boot:

// LỖI CHÍ MẠNG TRONG SPRING AOP:
@Service
public class OrderService {
 
    public void processOrder(Long orderId) {
        // ... Logic chuẩn bị
        this.chargeCard(orderId); // GỌI NỘI BỘ (SELF-INVOCATION)!
    }
 
    @Transactional // ANNOTATION NÀY HOÀN TOÀN BỊ VÔ HIỆU HÓA!
    public void chargeCard(Long orderId) {
        // Do gọi qua con trỏ 'this', luồng đi trực tiếp trong target object,
        // HOÀN TOÀN BỎ QUA Spring CGLIB Proxy! Transaction KHÔNG ĐƯỢC MỞ!
    }
}

Cách Decorator hóa giải: Vì Decorator là một cấu trúc bọc rõ ràng bên ngoài (outer.operation()), mọi lời gọi ủy nhiệm đều minh bạch và không bao giờ bị hiện tượng đánh mất ngữ cảnh của con trỏ this.


# hiện thực hóa: resilient caching Decorator chuẩn doanh nghiệp

Dưới đây là một ví dụ kiến trúc mẫu mực: Tách biệt hoàn toàn tầng truy vấn cơ sở dữ liệu (CustomerRepository) khỏi tầng Caching Redis và Circuit Breaker bằng cách áp dụng Decorator Pattern kết hợp Spring Bean Configuration:

# interface cốt lõi
public interface CustomerAccountService {
    CustomerAccount getAccount(Long customerId);
}
# component gốc (chỉ tập trung vào SQL database)
@Service("coreCustomerService")
public class CoreCustomerAccountService implements CustomerAccountService {
    private final JdbcTemplate jdbcTemplate;
 
    public CoreCustomerAccountService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
 
    @Override
    public CustomerAccount getAccount(Long customerId) {
        // Query thuần SQL nặng nề
        return jdbcTemplate.queryForObject("SELECT ... WHERE id = ?", new Object[]{customerId}, mapper);
    }
}
# resilient caching Decorator (trang bị Redis cache + fallback)
@Service
@Primary // Đánh dấu là Bean ưu tiên để Controller tự động inject Decorator
public class CachingCustomerAccountDecorator implements CustomerAccountService {
 
    private static final Logger log = LoggerFactory.getLogger(CachingCustomerAccountDecorator.class);
    private final CustomerAccountService delegate; // Đối tượng được bọc
    private final RedisTemplate<String, Object> redisTemplate;
    private final MeterRegistry meterRegistry;
 
    public CachingCustomerAccountDecorator(
            @Qualifier("coreCustomerService") CustomerAccountService delegate,
            RedisTemplate<String, Object> redisTemplate,
            MeterRegistry meterRegistry) {
        this.delegate = delegate;
        this.redisTemplate = redisTemplate;
        this.meterRegistry = meterRegistry;
    }
 
    @Override
    public CustomerAccount getAccount(Long customerId) {
        String cacheKey = "customer:account:" + customerId;
 
        // 1. Kiểm tra Cache L1/L2
        try {
            CustomerAccount cached = (CustomerAccount) redisTemplate.opsForValue().get(cacheKey);
            if (cached != null) {
                meterRegistry.counter("customer.cache.hit").increment();
                return cached;
            }
        } catch (Exception ex) {
            log.warn("Redis sập! Bỏ qua cache, fallback sang Database: {}", ex.getMessage());
        }
 
        meterRegistry.counter("customer.cache.miss").increment();
 
        // 2. Đo thời gian thực thi của đối tượng gốc
        Timer.Sample sample = Timer.start(meterRegistry);
        CustomerAccount account = delegate.getAccount(customerId);
        sample.stop(meterRegistry.timer("customer.db.query.duration"));
 
        // 3. Ghi đệm bất đồng bộ vào Redis
        try {
            redisTemplate.opsForValue().set(cacheKey, account, Duration.ofMinutes(15));
        } catch (Exception ex) {
            log.error("Không thể ghi cache: {}", ex.getMessage());
        }
 
        return account;
    }
}

# giá trị kiến trúc đạt được

  1. CoreCustomerAccountService hoàn toàn sạch sẽ, không có bất kỳ dòng code nào về Redis, Prometheus, hay Fallback.
  2. Nếu ngày mai muốn thay Redis bằng Caffeine Cache hoặc thêm tầng mã hóa AES, ta chỉ cần tạo thêm một Decorator mới bọc bên ngoài mà không làm thay đổi 1 dòng mã Database hiện có (Tuân thủ tuyệt đối OCP & SRP).

# bảng kiểm tra sẵn sàng vận hành (architect's checklist)

  • 1. Giữ Vững Interface Contract: Mọi Decorator bắt buộc phải implements cùng interface với component được bọc để duy trì tính đa hình.
  • 2. Bảo Đảm Trật Tự Lồng Nhau (Decorator Ordering): Xác định rõ thứ tự bọc Decorator (ví dụ: Logging ở ngoài cùng → Caching ở giữa → Retry ở trong → Core Service).
  • 3. Tránh Lạm Dụng Quá Sâu: Hạn chế việc bọc lồng quá 4 - 5 lớp Decorator vì sẽ làm tăng chi phí Method Call Stack và gây khó khăn cho việc truy vết lỗi.
  • 4. Khắc Phục Lỗi Self-Invocation: Nếu phát hiện các method có @Transactional hoặc @Cacheable gọi nội bộ lẫn nhau trong cùng 1 class, hãy tách method đó sang một service khác hoặc dùng cấu trúc Decorator tường minh.
  • 5. Xử Lý Graceful Degradation: Trong các Decorator phụ trợ (Caching, Audit), luôn bọc khối try-catch để đảm bảo lỗi của tầng phụ trợ không đánh sập luồng nghiệp vụ chính của khách hàng.

# lời kết

Decorator Pattern là một trong những minh chứng rực rỡ nhất cho nguyên lý "Ưu tiên Thành phần hơn Kế thừa" (Favor Composition over Inheritance).

Dù bạn tự tay bọc các lớp ủy nhiệm trong kiến trúc Clean Architecture hay sử dụng sức mạnh tự động hóa của Spring AOP Dynamic Proxy, việc thấu hiểu cơ chế bọc lớp, nắm vững ranh giới thực thi và kiểm soát được các rủi ro về mặt hiệu năng chính là thước đo bản lĩnh của một Senior Software Engineer và Software Architect.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.

← Previous postadapter pattern