Tất cả bài viết

Decorator — kế thừa không sai, sai là mày dùng nó để mở rộng hành vi

Kế thừa mở rộng là static và compile-time. Decorator mở rộng là dynamic và runtime. Khi behavior cần thay đổi linh hoạt — Decorator là lựa chọn đúng.

design-patternsdecoratorOOP

Giả sử HMS cần audit log: mỗi lần một doctor cập nhật medical record, hệ thống phải ghi lại ai làm gì, lúc mấy giờ, dữ liệu cũ là gì. Ban đầu, cách đơn giản nhất là thêm logic vào trong MedicalRecordService:

public void updateRecord(UUID recordId, UpdateRecordRequest request) {
    MedicalRecord record = recordRepository.findById(recordId).orElseThrow();
    MedicalRecord oldRecord = record.copy(); // Snapshot trước khi sửa
    
    record.update(request);
    recordRepository.save(record);
    
    // Audit log — nhét vào đây cho tiện
    auditLogRepository.save(AuditLog.of("UPDATE_RECORD", oldRecord, record, userContext.getCurrentUser()));
}

Chạy được. Nhưng bây giờ MedicalRecordService đang làm hai việc: business logic và audit. Khi team quyết định thêm audit vào tất cả các service khác — AppointmentService, PrescriptionService, PatientService — mày sẽ copy đoạn code đó vào mười mấy chỗ. Khi format audit log thay đổi, mày phải tìm và sửa từng nơi một.

Một developer khác trong team sẽ nghĩ: “Dùng kế thừa thôi, tạo một AuditedMedicalRecordService extends MedicalRecordService.” Nghe hợp lý. Nhưng đây là nơi nhiều người đi sai đường.


Tại sao kế thừa không phải câu trả lời cho bài toán này

Kế thừa mô hình hóa quan hệ is-a: Doctor is an Employee, AppointmentConfirmedSender is a NotificationSender. Nó phù hợp khi class con là một phiên bản chuyên biệt hơn của class cha.

Nhưng AuditedMedicalRecordService không phải là một loại MedicalRecordService — nó là MedicalRecordService cộng thêm một hành vi. Đây là quan hệ has-a-behavior. Dùng kế thừa ở đây tạo ra một vấn đề cụ thể: mày bị lock vào compile time. Không thể bật/tắt audit theo runtime config. Không thể add thêm behavior khác (caching, validation) mà không tạo thêm class kế thừa mới. Class hierarchy bắt đầu phình ra: AuditedCachedMedicalRecordService extends AuditedMedicalRecordService… và nó chỉ tệ hơn theo thời gian.


Decorator: wrap behavior, không kế thừa

Decorator hoạt động bằng cách wrap một object trong một object khác có cùng interface. Wrapper thêm behavior trước/sau khi delegate sang object gốc.

public interface MedicalRecordService {
    MedicalRecord updateRecord(UUID recordId, UpdateRecordRequest request);
    MedicalRecord getRecord(UUID recordId);
    // ...
}

// Implementation gốc — chỉ làm business logic
@Service
@Primary
public class MedicalRecordServiceImpl implements MedicalRecordService {
    
    @Override
    public MedicalRecord updateRecord(UUID recordId, UpdateRecordRequest request) {
        MedicalRecord record = recordRepository.findById(recordId).orElseThrow();
        record.update(request);
        return recordRepository.save(record);
    }
}

// Decorator thêm audit — wrap implementation gốc
public class AuditedMedicalRecordService implements MedicalRecordService {
    
    private final MedicalRecordService delegate; // Object gốc được wrap
    private final AuditLogRepository auditLogRepository;
    private final UserContext userContext;
    
    public AuditedMedicalRecordService(MedicalRecordService delegate, 
                                        AuditLogRepository auditLogRepository,
                                        UserContext userContext) {
        this.delegate = delegate;
    }
    
    @Override
    public MedicalRecord updateRecord(UUID recordId, UpdateRecordRequest request) {
        // Lấy snapshot trước khi delegate thực hiện
        MedicalRecord before = delegate.getRecord(recordId);
        
        // Delegate sang implementation gốc
        MedicalRecord after = delegate.updateRecord(recordId, request);
        
        // Thêm behavior: audit log SAU KHI delegate thực hiện
        auditLogRepository.save(AuditLog.builder()
            .action("UPDATE_MEDICAL_RECORD")
            .entityId(recordId)
            .before(objectMapper.writeValueAsString(before))
            .after(objectMapper.writeValueAsString(after))
            .performedBy(userContext.getCurrentUserId())
            .build());
        
        return after;
    }
    
    @Override
    public MedicalRecord getRecord(UUID recordId) {
        // Read không cần audit — delegate thẳng
        return delegate.getRecord(recordId);
    }
}

Trong Spring: @Aspect làm điều này tốt hơn

Thực tế, Spring AOP là cách Decorator pattern được implement phổ biến nhất trong Spring codebase. @Transactional, @Cacheable, @PreAuthorize — tất cả đều là Decorator hoạt động thông qua proxy.

Mày có thể viết custom Aspect cho audit:

@Aspect
@Component
public class AuditAspect {
    
    // Intercept tất cả method có annotation @Audited
    @Around("@annotation(audited)")
    public Object auditMethod(ProceedingJoinPoint joinPoint, Audited audited) throws Throwable {
        Object result = joinPoint.proceed(); // Delegate sang method gốc
        
        auditLogRepository.save(AuditLog.builder()
            .action(audited.action())
            .performedBy(userContext.getCurrentUserId())
            .build());
        
        return result;
    }
}

// Dùng:
@Audited(action = "UPDATE_MEDICAL_RECORD")
public MedicalRecord updateRecord(UUID recordId, UpdateRecordRequest request) {
    // Chỉ business logic, không có audit code
}

Đây là Decorator pattern ở tầng framework — Spring tự tạo proxy wrap method của mày, thêm behavior trước/sau mà không cần mày viết boilerplate wrapper class.


Khi nào dùng Decorator, khi nào dùng kế thừa

Dùng kế thừa khi class con là một phiên bản chuyên biệt hơn: EmergencyAppointment extends Appointment, class con override behavior vì nó có rules khác.

Dùng Decorator khi mày muốn thêm behavior vào một object mà không thay đổi class đó: logging, caching, validation, audit, retry logic. Behavior này độc lập với business logic và có thể được bật/tắt, kết hợp tùy ý.


Takeaway

Mỗi lần mày định viết code kiểu // Thêm X vào trước/sau khi làm Y trong một method — đó là Decorator đang gõ cửa. Câu hỏi: behavior đó có phải là một phần của business logic không? Nếu không — nó nên là một lớp bọc bên ngoài, không phải code nằm lẫn bên trong.


Bài tiếp theo: Proxy — không phải object nào cũng nên được truy cập trực tiếp