Decorator pattern là cách thêm hành vi cho một đối tượng bằng cách bọc nó trong một đối tượng khác cùng giao diện, thay vì sinh ra một lớp con cho mỗi tổ hợp tính năng. Bạn đã dùng nó hằng ngày mà có thể không để ý: new BufferedReader(new InputStreamReader(new FileInputStream(f))) chính là ba lớp bọc xếp chồng. Bài này mổ xẻ Decorator pattern trong java.io, tự cài một decorator đếm byte, rồi áp dụng vào một dịch vụ thanh toán có logging, retry và idempotency. Mọi đoạn code đều đã chạy thật, biên dịch bằng javac --release 17 nên chỉ dùng tính năng và API có từ Java 17.

Series này đã đi qua Adapter pattern: nối API cũ với giao diện mới, một mẫu cũng dựa trên việc bọc đối tượng nhưng đổi giao diện. Nếu muốn nhắc lại nền tảng thiết kế, hãy xem Nguyên lý SOLID trong Java qua ví dụ thực tế, vì Decorator pattern là ví dụ điển hình của Open/Closed và Single Responsibility.

Vấn đề: bùng nổ lớp con khi cộng dồn tính năng

Giả sử hệ thống đơn hàng có interface PaymentGateway với một cổng thanh toán thật. Hôm nay cần ghi log mỗi lần gọi, và Decorator pattern là lời giải gọn nhất cho kiểu yêu cầu cộng dồn này. Tuần sau cần retry khi cổng báo timeout. Tháng sau cần chống trừ tiền hai lần cho cùng một mã đơn. Nếu mỗi nhu cầu là một lớp con của cổng thật, bạn sẽ có LoggingGateway, RetryGateway, rồi LoggingRetryGateway, RetryIdempotentGateway và tiếp tục. Với n tính năng độc lập, số tổ hợp tiềm năng là 2n, và mỗi tổ hợp lại kế thừa cứng từ một cài đặt cụ thể.

Kế thừa còn có hai điểm yếu khác: nó quyết định ở thời điểm biên dịch, và không dùng được khi lớp gốc là final hoặc nằm trong thư viện bạn không sửa được. Decorator pattern giải quyết cả ba điểm yếu đó bằng composition: mỗi tính năng là một lớp nhỏ nhận một đối tượng cùng giao diện, làm việc của mình rồi chuyển tiếp.

Cấu trúc của Decorator pattern

Theo mô tả của refactoring.guru (cũng gọi là Wrapper) và sách GoF, Decorator pattern có bốn vai trò:

Vai tròNhiệm vụVí dụ trong java.ioVí dụ trong bài
ComponentGiao diện chungInputStreamPaymentGateway
Concrete ComponentĐối tượng gốc làm việc thậtFileInputStreamFakeGateway (cổng giả lập)
Base DecoratorGiữ tham chiếu tới Component, chuyển tiếp mọi lời gọiFilterInputStreamGatewayDecorator
Concrete DecoratorThêm hành vi trước hoặc sau khi chuyển tiếpBufferedInputStream, GZIPInputStreamLoggingGateway, RetryGateway

Điểm mấu chốt của Decorator pattern: decorator vừa là một Component (cùng giao diện) vừa có một Component (giữ tham chiếu). Nhờ vậy client không biết, và không cần biết, mình đang cầm đối tượng gốc hay một chồng năm lớp bọc. Client xếp chồng theo tổ hợp mình cần và đối xử với kết quả như mọi Component khác.

Học Decorator pattern từ java.io: chồng decorator quen thuộc

Tài liệu Javadoc của FilterInputStream mô tả đúng vai trò Base Decorator: lớp này "contains some other input stream, which it uses as its basic source of data, possibly transforming the data along the way or providing additional functionality". Mọi phương thức của InputStream được ghi đè để chuyển yêu cầu sang luồng bên trong (trường protected InputStream in). Các lớp con trực tiếp gồm BufferedInputStream, DataInputStream, InflaterInputStream, CheckedInputStream, DigestInputStream, CipherInputStream và vài lớp khác.

Mỗi decorator làm đúng một việc: đệm bộ nhớ, giải nén, tính checksum, giải mã. Muốn đọc file CSV nén gzip theo từng dòng, bạn xếp chồng: FileInputStream (đọc byte từ đĩa) bọc bởi GZIPInputStream (giải nén), bọc bởi InputStreamReader (byte sang ký tự, chỉ định UTF-8), bọc bởi BufferedReader (đọc theo dòng). Thứ tự không tuỳ tiện: giải nén phải đứng trước khi chuyển byte thành ký tự.

Điều thú vị là BufferedInputStream không đổi giao diện, chỉ đổi hiệu năng. Tôi đo thử trên máy 16 GB RAM, mã biên dịch với --release 17 và chạy trên JDK 21, bằng cách đọc từng byte một file ngẫu nhiên 4 MB, bỏ qua hai vòng khởi động và lấy vòng thứ ba:

try (InputStream in = new FileInputStream(big.toFile())) {
    int b; while ((b = in.read()) != -1) sum += b;          // không đệm
}
try (InputStream in = new BufferedInputStream(new FileInputStream(big.toFile()))) {
    int b; while ((b = in.read()) != -1) sum += b;          // có decorator đệm
}
Đọc từng byte 4 MB (lần đo thứ 3): không đệm 1385 ms, có BufferedInputStream 20 ms

Kết quả chỉ mang tính minh hoạ (đo bằng System.nanoTime, không dùng JMH, và biến động theo từng lần chạy: lần chạy lại cho 1451 ms so với 20 ms; ba lần chạy bổ sung trên JDK 21 cho 1387, 1393 và 1367 ms phía không đệm, vẫn 20 ms phía có đệm; tất cả được lưu trong code/s6-07/output-javaio.txt). Nhưng bậc độ lớn rất rõ: mỗi lần read() không đệm là một system call, còn BufferedInputStream đọc một khối lớn rồi phát từng byte từ bộ nhớ. Đây là giá trị thực của Decorator pattern trong thư viện chuẩn: thêm một mối quan tâm (đệm) mà code đọc dữ liệu không phải sửa một dòng.

Tự viết Decorator pattern: đếm byte đi qua luồng

Để thấy cơ chế, hãy kế thừa FilterInputStream và đếm số byte đã đọc. Đây là cách các lớp như CheckedInputStream hoạt động ở mức khái quát.

static class CountingInputStream extends FilterInputStream {
    private long count;
    CountingInputStream(InputStream in) { super(in); }

    @Override public int read() throws IOException {
        int b = super.read();
        if (b >= 0) count++;
        return b;
    }
    @Override public int read(byte[] buf, int off, int len) throws IOException {
        int n = super.read(buf, off, len);
        if (n > 0) count += n;
        return n;
    }
    long count() { return count; }
}

Cần ghi đè cả read() lẫn read(byte[], int, int). Javadoc cảnh báo rằng read(byte[]) của FilterInputStream gọi read(b, 0, b.length) chứ không gọi in.read(b), vì một số lớp con phụ thuộc đúng chiến lược này. Quên ghi đè một trong hai là lỗi phổ biến khi cài Decorator pattern trên luồng I/O khiến bộ đếm đếm thiếu. Chèn decorator vào giữa chuỗi đọc file gzip:

CountingInputStream counting = new CountingInputStream(new FileInputStream(gz.toFile()));
try (BufferedReader r = new BufferedReader(
        new InputStreamReader(new GZIPInputStream(counting), StandardCharsets.UTF_8))) {
    // đọc từng dòng ...
}
System.out.println(counting.count());

Đây là output thật khi chạy java JavaIoDemo.java (file CSV 2.000 đơn hàng giả lập):

Kích thước gốc: 37409 byte, sau gzip: 9726 byte
Dòng đầu: ma_don,khach,tong_tien
Số dòng đọc được: 2001, byte nén đã đọc qua decorator đếm: 9726
Kiểu tĩnh khai báo: InputStream, kiểu thật: BufferedInputStream

Bộ đếm báo đúng 9726 byte, bằng kích thước file nén, vì nó nằm dưới GZIPInputStream: nó thấy dữ liệu nén chứ không thấy dữ liệu đã giải nén. Nếu đặt nó phía trên GZIPInputStream, nó sẽ đếm 37409 byte. Vị trí của decorator trong chồng quyết định nó quan sát tầng nào, một ví dụ nhỏ về việc thứ tự có ý nghĩa.

Lưu ý CountingInputStream ở trên là bản Decorator pattern tối giản: nó chưa xử lý skip(), mark() và reset(). Nếu client gọi skip(n), các byte bị bỏ qua không được đếm; sau reset(), các byte đọc lại bị đếm hai lần. Decorator thật như BufferedInputStream hay CheckedInputStream phải cân nhắc từng phương thức này.

Áp dụng Decorator pattern cho dịch vụ thanh toán

Quay lại bài toán đầu để áp dụng Decorator pattern cho nghiệp vụ. Giao diện và cổng giả lập (hai lần gọi đầu ném lỗi timeout để kiểm tra retry):

record PaymentResult(String orderId, boolean ok, String message) {}

interface PaymentGateway {
    PaymentResult charge(String orderId, long amountVnd);
}

static class FakeGateway implements PaymentGateway {
    final AtomicInteger calls = new AtomicInteger();
    public PaymentResult charge(String orderId, long amountVnd) {
        int n = calls.incrementAndGet();
        if (n <= 2) throw new IllegalStateException("gateway timeout (lần " + n + ")");
        return new PaymentResult(orderId, true, "OK " + amountVnd);
    }
}

Base Decorator chỉ giữ tham chiếu và chuyển tiếp. Các decorator cụ thể kế thừa nó và chỉ viết phần khác biệt:

static abstract class GatewayDecorator implements PaymentGateway {
    protected final PaymentGateway next;
    protected GatewayDecorator(PaymentGateway next) { this.next = Objects.requireNonNull(next); }
    public PaymentResult charge(String orderId, long amountVnd) { return next.charge(orderId, amountVnd); }
}

static class RetryGateway extends GatewayDecorator {
    private final int maxAttempts;
    RetryGateway(PaymentGateway next, int maxAttempts) { super(next); this.maxAttempts = maxAttempts; }
    public PaymentResult charge(String orderId, long amountVnd) {
        RuntimeException last = null;
        for (int i = 1; i <= maxAttempts; i++) {
            try { return super.charge(orderId, amountVnd); }
            catch (RuntimeException e) { last = e; System.out.println("[retry] lần " + i + " thất bại"); }
        }
        throw last;
    }
}

LoggingGateway in log trước và sau lời gọi, IdempotentGateway giữ một Map kết quả theo orderId để cùng một mã đơn không bị trừ tiền hai lần, còn TimingGateway đo thời gian. Dưới đây là hai decorator còn lại của Decorator pattern này, đủ để bạn chép về chạy cùng các đoạn trên (đặt chung trong một lớp, ví dụ ServiceDecoratorDemo, cần import java.util.*):

static class LoggingGateway extends GatewayDecorator {
    LoggingGateway(PaymentGateway next) { super(next); }
    public PaymentResult charge(String orderId, long amountVnd) {
        System.out.println("[log] charge " + orderId + " " + amountVnd);
        try {
            PaymentResult r = super.charge(orderId, amountVnd);
            System.out.println("[log] -> " + r);
            return r;
        } catch (RuntimeException e) {
            System.out.println("[log] !! " + e.getMessage());
            throw e;
        }
    }
}

static class IdempotentGateway extends GatewayDecorator {
    private final Map<String, PaymentResult> done = new HashMap<>();
    IdempotentGateway(PaymentGateway next) { super(next); }
    public PaymentResult charge(String orderId, long amountVnd) {
        PaymentResult cached = done.get(orderId);
        if (cached != null) { System.out.println("[idem] trả kết quả đã lưu cho " + orderId); return cached; }
        PaymentResult r = super.charge(orderId, amountVnd);
        done.put(orderId, r);   // chỉ lưu SAU KHI cổng trả về thành công
        return r;
    }
}

Chú ý dòng chú thích trong IdempotentGateway: kết quả chỉ được lưu sau khi cổng trả về thành công. Điều này quyết định ý nghĩa của decorator, như phần sau sẽ chỉ ra. TimingGateway tương tự LoggingGateway, chỉ ghi System.nanoTime() trước và sau lời gọi. Điểm đáng chú ý khi lắp ráp:

PaymentGateway a = new LoggingGateway(new RetryGateway(new IdempotentGateway(fakeA), 3));
a.charge("DH1001", 250_000);
a.charge("DH1001", 250_000);   // gọi lần hai cùng mã đơn
System.out.println("Số lần chạm cổng thật: " + fakeA.calls.get());
=== Thứ tự A: Logging(Retry(Idempotent(Fake))) ===
[log] charge DH1001 250000
[retry] lần 1 thất bại
[retry] lần 2 thất bại
[log] -> PaymentResult[orderId=DH1001, ok=true, message=OK 250000]
[log] charge DH1001 250000
[idem] trả kết quả đã lưu cho DH1001
[log] -> PaymentResult[orderId=DH1001, ok=true, message=OK 250000]
Số lần chạm cổng thật: 3

Ở thứ tự A, tôi cố ý đặt IdempotentGateway ở trong cùng, sát cổng thật, để minh hoạ rõ việc lưu kết quả đã thành công; đây chưa phải thứ tự khuyến nghị cho Decorator pattern trong dịch vụ thật, phần quy tắc bên dưới nói về thứ tự khác.

Lần gọi đầu đi qua RetryGateway, thất bại hai lần rồi thành công ở lần thứ ba, tổng cộng ba lần chạm cổng thật. Lần gọi thứ hai cùng mã đơn dừng ngay ở IdempotentGateway và không chạm cổng nữa, nên bộ đếm vẫn là 3. Cần đọc đúng điều output này chứng minh: decorator chỉ chặn request trùng mã đơn sau khi lần đầu đã thành công. Ở lần gọi đầu, hai lần timeout đầu không để lại gì trong Map, nên retry vẫn chạm cổng thật đủ ba lần. Chú ý rằng LoggingGateway nằm ngoài cùng nên chỉ thấy một lời gọi mỗi lần, dù bên trong đã retry. Đổi thứ tự các lớp bọc của Decorator pattern sẽ đổi hành vi:

PaymentGateway b = new RetryGateway(new LoggingGateway(fakeB), 3);
b.charge("DH1002", 90_000);
=== Thứ tự B: Retry(Logging(Fake)) — log nằm trong retry ===
[log] charge DH1002 90000
[log] !! gateway timeout (lần 1)
[retry] lần 1 thất bại
[log] charge DH1002 90000
[log] !! gateway timeout (lần 2)
[retry] lần 2 thất bại
[log] charge DH1002 90000
[log] -> PaymentResult[orderId=DH1002, ok=true, message=OK 90000]

Ở thứ tự B, log nằm trong retry nên ghi lại từng lần thử, hữu ích khi cần biết cổng chập chờn thế nào. Ở thứ tự A, log chỉ ghi kết quả cuối, gọn hơn cho audit. Không có thứ tự nào đúng tuyệt đối khi lắp Decorator pattern, nhưng bạn phải chọn có chủ đích. Một quy tắc thực dụng cho thứ tự: logging và đo thời gian đặt ngoài cùng để quan sát đúng điều người dùng thấy; decorator chống trùng request (như IdempotentGateway) đặt ngoài retry để một request trùng mã đơn không kích hoạt cả vòng retry lần nữa. Nhưng thứ tự lắp Decorator pattern này không bảo vệ bạn khỏi trừ tiền hai lần khi retry. Điều đó cần một cơ chế khác, như phần tiếp theo cho thấy.

Timeout không rõ kết quả: cần khoá idempotency ở phía cổng thanh toán

Tình huống nguy hiểm thật là cổng đã xử lý và trừ tiền, rồi mới timeout trước khi trả lời. Client chỉ thấy lỗi và không biết tiền đã bị trừ chưa. Nếu retry gửi lại yêu cầu y hệt, cổng không có cách nào nhận ra đây là yêu cầu cũ. Tôi mô phỏng đúng tình huống đó bằng một cổng giả ném timeout ở lần gọi đầu sau khi đã ghi nhận trừ tiền, và so sánh hai loại cổng: một cổng coi mọi lời gọi là mới, một cổng nhận khoá idempotency (cùng khoá thì trả lại kết quả cũ, giống cách các cổng thanh toán lớn như Stripe cung cấp qua header Idempotency-Key, xem tài liệu Stripe). Cả hai chạy qua cùng chồng IdempotentGateway(RetryGateway(cổng)). Cổng giả được định nghĩa như sau (cũng nằm trong code/s6-07/src/ServiceDecoratorDemo.java):

static class ChargeThenTimeoutGateway implements PaymentGateway {
    final boolean dedupByKey;
    final Map<String, PaymentResult> byKey = new HashMap<>();
    long totalDebited = 0;
    int calls = 0;
    ChargeThenTimeoutGateway(boolean dedupByKey) { this.dedupByKey = dedupByKey; }
    public PaymentResult charge(String idempotencyKey, long amountVnd) {
        calls++;
        PaymentResult r;
        if (dedupByKey && byKey.containsKey(idempotencyKey)) {
            r = byKey.get(idempotencyKey);                 // cùng khoá: không trừ tiền lần nữa
        } else {
            totalDebited += amountVnd;                     // tiền đã bị trừ
            r = new PaymentResult(idempotencyKey, true, "OK " + amountVnd);
            if (dedupByKey) byKey.put(idempotencyKey, r);
        }
        if (calls == 1) throw new IllegalStateException("gateway timeout sau khi đã xử lý (lần 1)");
        return r;
    }
}

Vòng lặp kiểm thử:

for (boolean dedup : new boolean[] { false, true }) {
    ChargeThenTimeoutGateway gw = new ChargeThenTimeoutGateway(dedup);
    PaymentGateway g = new IdempotentGateway(new RetryGateway(gw, 3));
    PaymentResult r = g.charge("DH1004", 500_000);   // mã đơn đóng vai khoá idempotency
    System.out.println("cổng " + (dedup ? "CÓ " : "KHÔNG") + " dedup theo khoá: số lần gọi cổng=" + gw.calls
        + ", tổng tiền bị trừ=" + gw.totalDebited + " đ, kết quả=" + r.message());
}
=== Timeout SAU KHI cổng đã trừ tiền: Idempotent(Retry(cổng)) ===
[retry] lần 1 thất bại
cổng KHÔNG dedup theo khoá: số lần gọi cổng=2, tổng tiền bị trừ=1000000 đ, kết quả=OK 500000
[retry] lần 1 thất bại
cổng CÓ  dedup theo khoá: số lần gọi cổng=2, tổng tiền bị trừ=500000 đ, kết quả=OK 500000

Dù IdempotentGateway có mặt trong chồng, cổng không dedup vẫn trừ 1.000.000 đ cho một đơn 500.000 đ, vì decorator phía client chưa kịp lưu gì khi lần gọi đầu timeout. Chỉ khi cổng nhận và tôn trọng khoá do client gửi xuống, retry mới an toàn: tổng tiền bị trừ là 500.000 đ. Kết luận cho thiết kế Decorator pattern trong thanh toán: retry chỉ an toàn khi thao tác ghi là idempotent ở phía máy chủ, tức khoá phải được gửi tới cổng thanh toán (và sinh một lần cho mỗi đơn, dùng lại ở mọi lần retry). Một decorator phía client như IdempotentGateway chỉ giảm request trùng sau khi đã thành công. Đây là mô phỏng một chiều để minh hoạ, không thay thế việc đọc tài liệu của cổng thanh toán bạn dùng.

Lưu ý IdempotentGateway trong ví dụ dùng HashMap không an toàn đa luồng và không giới hạn kích thước. Đây là code minh hoạ; dịch vụ thật cần lưu khoá idempotency ở kho bền vững (Redis hoặc cơ sở dữ liệu) với thời hạn hết hạn, và xử lý trường hợp hai request cùng mã đơn đến đồng thời.

Decorator trong JDK ngoài java.io: Collections

Các phương thức bọc của Collections cũng là ví dụ của Decorator pattern. Theo Javadoc của Collections, unmodifiableList trả về một "unmodifiable view", synchronizedList trả về danh sách thread-safe "backed by the specified list", checkedList trả về một "dynamically typesafe view". Mỗi phương thức giữ nguyên giao diện List và thêm một mối quan tâm: chỉ đọc, đồng bộ hoá, kiểm tra kiểu lúc chạy. Chạy thử:

List<String> src = new ArrayList<>(List.of("DH1", "DH2"));
List<String> view = Collections.unmodifiableList(src);
src.add("DH3");
System.out.println("view sau khi sửa src: " + view);
try { view.add("DH4"); } catch (UnsupportedOperationException e) { ... }
=== Decorator trong JDK: Collections.unmodifiableList ===
view sau khi sửa src: [DH1, DH2, DH3]
view.add -> UnsupportedOperationException
lớp của view: java.util.Collections$UnmodifiableRandomAccessList

View phản ánh thay đổi của danh sách gốc, một bẫy quen thuộc: unmodifiableList không phải bản sao bất biến, nó chỉ chặn việc sửa qua chính view. Muốn bất biến thật, dùng List.copyOf. Với synchronizedList, Javadoc nhấn mạnh rằng khi duyệt bằng Iterator, Spliterator hay Stream bạn phải tự synchronized trên danh sách trả về, nếu không có thể gặp hành vi không xác định. Lớp bọc kiểu Decorator pattern này đồng bộ hoá từng phương thức riêng lẻ, nhưng không biến chuỗi thao tác nhiều bước thành một đơn vị nguyên tử.

Decorator pattern, Proxy và Adapter: phân biệt thế nào

Ba mẫu này, kể cả Decorator pattern, đều bọc một đối tượng nên dễ nhầm. Bảng sau tổng hợp cách refactoring.guru phân biệt chúng:

MẫuGiao diện so với đối tượng bọcÝ định chínhAi kiểm soát vòng đời đối tượng bên trong
DecoratorGiữ nguyên (hoặc mở rộng)Thêm hành vi, xếp chồng đượcClient tự lắp chồng
ProxyGiữ nguyênKiểm soát truy cập (lazy, quyền, cache, từ xa)Thường chính proxy
AdapterĐổi sang giao diện khácCho hai giao diện không khớp làm việc với nhauClient

Về cấu trúc, Decorator pattern và Proxy gần như giống hệt; khác biệt nằm ở ý định. Bài tiếp theo của series sẽ đào sâu Proxy, kể cả cách JDK và Spring sinh proxy lúc chạy. Với Adapter, tức mẫu khác với Decorator pattern ở chỗ đổi giao diện, hãy đọc lại bài Adapter pattern: nối API cũ với giao diện mới. Một so sánh hữu ích khác là với Strategy: Decorator pattern đổi lớp "vỏ" bên ngoài của đối tượng, còn Strategy đổi thuật toán cốt lõi bên trong.

Khi nào không nên dùng Decorator pattern

Decorator pattern đánh đổi sự linh hoạt bằng độ phức tạp ở chỗ lắp ráp. Các hạn chế được ghi nhận rộng rãi:

  • Khó gỡ một lớp ở giữa chồng. Muốn bỏ retry nhưng giữ logging, bạn phải dựng lại cả chồng.
  • Hành vi phụ thuộc thứ tự. Như thấy ở trên, đổi thứ tự là đổi ngữ nghĩa; khó viết decorator hoàn toàn độc lập với vị trí.
  • Nhiều lớp nhỏ giống nhau. Mỗi decorator thường chỉ vài dòng, nhưng cả hệ thống có thể có hàng chục lớp, và stack trace sâu khi debug.
  • Giao diện quá rộng. Nếu interface có hàng chục phương thức, Base Decorator phải chuyển tiếp từng cái; quên một phương thức là bug thầm lặng (cách khắc phục là dùng default method hoặc proxy động, xem bài sau).
  • Kiểm tra kiểu bị che. Code dùng instanceof hoặc ép kiểu về lớp cụ thể sẽ hỏng khi đối tượng bị bọc.

Nếu bạn chỉ có một tính năng bổ sung cố định, dùng Decorator pattern là thừa; một lớp con hoặc một phương thức thêm vào có thể đơn giản hơn. Nếu tính năng cắt ngang nhiều lớp (logging, transaction, bảo mật), AOP của Spring thường phù hợp hơn việc viết tay hàng chục decorator.

Lỗi thường gặp khi dùng Decorator pattern

  • Quên đóng luồng ngoài cùng. Đóng decorator ngoài cùng sẽ lan truyền close() xuống luồng bên trong; dùng try-with-resources cho biến ngoài cùng. Nếu giữ riêng một tham chiếu tới luồng trong, chú ý đừng đóng hai lần.
  • Quên ghi đè đủ các phương thức đọc/ghi. Với FilterInputStream, ghi đè read() mà quên read(byte[], int, int) khiến logic của bạn bị bỏ qua khi lớp trên đọc theo khối.
  • Dùng chung một decorator có trạng thái giữa nhiều luồng. IdempotentGateway ở trên giữ HashMap; đưa vào môi trường đa luồng phải thay bằng cấu trúc an toàn.
  • Nhầm view với bản sao. Collections.unmodifiableList vẫn thay đổi theo danh sách gốc.
  • Retry thao tác trừ tiền mà cổng không hỗ trợ khoá idempotency. Nếu timeout xảy ra sau khi cổng đã trừ tiền, retry sẽ trừ lần hai (xem output ở trên). Phải gửi khoá idempotency xuống cổng, sinh một lần cho mỗi đơn và dùng lại ở mọi lần thử; decorator lưu kết quả phía client không đủ để chặn lỗi này.

Checklist áp dụng Decorator pattern

  1. Có một interface nhỏ, ổn định để mọi thành phần của Decorator pattern cùng cài đặt.
  2. Mỗi decorator chỉ làm một việc và chuyển tiếp phần còn lại cho đối tượng bên trong.
  3. Viết Base Decorator để tránh lặp code chuyển tiếp trong Decorator pattern.
  4. Ghi lại thứ tự lắp ráp ở một nơi duy nhất (factory hoặc cấu hình), kèm lý do chọn thứ tự.
  5. Viết test cho từng decorator riêng lẻ, và ít nhất một test cho chồng đầy đủ như chạy thật.
  6. Với tính năng cắt ngang toàn ứng dụng, cân nhắc AOP thay vì viết tay Decorator pattern cho từng lớp.

Bài tiếp theo

Decorator pattern và Proxy có cùng hình dạng nhưng khác ý định. Bài kế tiếp trong series: Proxy pattern: JDK dynamic proxy, CGLIB và Spring AOP, nơi chúng ta sinh proxy lúc chạy bằng java.lang.reflect.Proxy và xem Spring dùng nó cho @Transactional ra sao.