Lập trình · Mẫu thiết kế
Mẫu Decorator (Design Pattern) giải thích kèm ví dụ
Trả lời nhanh
Decorator là một mẫu thiết kế cấu trúc (structural), thêm trách nhiệm cho một đối tượng một cách động bằng cách bọc nó trong một đối tượng khác có cùng interface. Mỗi decorator chuyển tiếp lời gọi đến đối tượng được bọc và thêm hành vi riêng trước hoặc sau đó, như ghi log, lưu đệm hay nén dữ liệu. Vì các decorator có thể xếp chồng theo thứ tự tùy ý, chúng là lựa chọn linh hoạt thay cho việc tạo một lớp con cho mọi tổ hợp tính năng.
Mục đích
Gắn thêm trách nhiệm cho một đối tượng một cách động. Decorator là lựa chọn linh hoạt thay cho kế thừa để mở rộng chức năng. Decorator là một trong bảy mẫu cấu trúc trong sách Gang of Four (GoF), nơi nó còn được gọi là Wrapper.
Vấn đề mà mẫu này giải quyết
Hãy tưởng tượng một kho dữ liệu có phương thức get(key). Có nơi gọi muốn ghi log, có nơi muốn cache trong bộ nhớ, có nơi muốn cả hai, và có nơi muốn thử lại với back end mạng chậm. Với kế thừa, bạn sẽ viết LoggingStore, CachingStore, LoggingCachingStore, RetryingCachingStore và cứ thế: một lớp con cho mỗi tổ hợp, và các tổ hợp bị cố định từ lúc biên dịch.
Decorator biến mỗi tính năng thành một lớp bọc nhỏ. Bạn dựng tổ hợp cần thiết lúc chạy bằng cách bọc đối tượng này vào đối tượng kia, và mã dùng kho dữ liệu vẫn thấy cùng một interface dù có bao nhiêu lớp bọc.
Cấu trúc và các thành phần
| Thành phần | Trách nhiệm | Trong ví dụ |
|---|---|---|
| Component | Interface dùng chung cho đối tượng thật và các decorator. | Store |
| ConcreteComponent | Đối tượng gốc thực hiện công việc cốt lõi. | SlowStore |
| Decorator | Giữ tham chiếu đến một Component và cài đặt cùng interface bằng cách chuyển tiếp. | StoreDecorator |
| ConcreteDecorator | Thêm hành vi trước hoặc sau khi chuyển tiếp. | LoggingStore, CachingStore |
Cài đặt bằng TypeScript
interface Store {
get(key: string): string;
}
class SlowStore implements Store {
get(key: string): string { console.log(` (database read: ${key})`); return key.toUpperCase(); }
}
// Base decorator: forwards everything to the wrapped store.
abstract class StoreDecorator implements Store {
constructor(protected inner: Store) {}
get(key: string): string { return this.inner.get(key); }
}
class LoggingStore extends StoreDecorator {
get(key: string): string {
console.log(`get ${key}`);
return super.get(key);
}
}
class CachingStore extends StoreDecorator {
private cache = new Map<string, string>();
get(key: string): string {
if (!this.cache.has(key)) this.cache.set(key, super.get(key));
return this.cache.get(key)!;
}
}
// Stack features in any order at run time.
const store: Store = new LoggingStore(new CachingStore(new SlowStore()));
console.log(store.get("ada"));
console.log(store.get("ada"));
// get ada
// (database read: ada)
// ADA
// get ada
// ADALần gọi thứ hai được ghi log nhưng được trả lời từ cache, nên cơ sở dữ liệu không bị đọc lại. Nếu đổi thứ tự thành new CachingStore(new LoggingStore(...)), chỉ những lần cache miss mới được ghi log. Thứ tự các decorator làm thay đổi hành vi; đây vừa là điểm mạnh vừa là nguồn gây nhầm lẫn thường gặp.
Decorator hàm trong TypeScript
Khi component là một hàm đơn lẻ, decorator có thể là một hàm bậc cao: nhận vào một hàm và trả về hàm mới có cùng chữ ký cùng hành vi bổ sung.
function withTiming<A extends unknown[], R>(name: string, fn: (...args: A) => R) {
return (...args: A): R => {
const start = performance.now();
try {
return fn(...args);
} finally {
console.log(`${name} took ${(performance.now() - start).toFixed(1)} ms`);
}
};
}
const slowSquare = (n: number) => { for (let i = 0; i < 1e6; i++); return n * n; };
const timedSquare = withTiming("square", slowSquare);
console.log(timedSquare(12)); // square took ... ms, then 144Khi nào nên dùng và khi nào không
Dùng Decorator khi:
- Bạn muốn thêm các tính năng như ghi log, lưu đệm, thử lại, kiểm tra hợp lệ, nén hoặc kiểm tra quyền truy cập cho từng đối tượng riêng lẻ mà không ảnh hưởng các đối tượng khác cùng lớp.
- Các tính năng cần được kết hợp tự do, và kế thừa sẽ tạo ra một lớp cho mỗi tổ hợp.
- Bạn không thể hoặc không nên sửa lớp gốc, chẳng hạn vì nó đến từ một thư viện.
Tránh dùng khi:
- Bạn chỉ cần một hành vi bổ sung cố định. Thêm trực tiếp, hoặc dùng một lớp con, sẽ đơn giản hơn.
- Mã phụ thuộc vào danh tính hoặc lớp cụ thể của đối tượng. Đối tượng đã được decorate là một đối tượng khác, nên các phép kiểm tra như
instanceof SlowStorehay so sánh===sẽ thất bại. - Nhiều lớp bọc nhỏ làm việc gỡ lỗi khó khăn. Chồng lớp bọc sâu tạo ra call stack dài và khó biết lớp nào làm gì.
Middleware trong các web framework, như Express hay Koa, là một ý tưởng liên quan: mỗi lớp nhận một request, có thể xử lý nó, rồi chuyển tiếp.
Ứng dụng thực tế
- Java I/O:
new BufferedInputStream(new FileInputStream("data.bin"))bọc một stream trong một stream khác để thêm bộ đệm, và cả hai đều có interfaceInputStream. Chính cuốn sách GoF cũng bàn về stream như một ví dụ điển hình. - Java collections:
Collections.unmodifiableList(list)vàCollections.synchronizedList(list)trả về các lớp bọc thêm hành vi chỉ đọc hoặc khóa đồng bộ quanh một danh sách có sẵn. - Python: các decorator hàm như
functools.lru_cachebọc một hàm và thêm cache, cònfunctools.wrapsgiúp lớp bọc giữ tên và docstring của hàm được bọc.
Các mẫu liên quan
| Mẫu | Khác với Decorator ở điểm nào |
|---|---|
| Adapter | Thay đổi interface của một đối tượng. Decorator giữ nguyên interface và thêm hành vi. |
| Proxy | Có cùng cấu trúc nhưng mục đích khác: kiểm soát truy cập (khởi tạo lười, truy cập từ xa, phân quyền) và thường tự quản lý vòng đời của đối tượng thật. |
| Composite | Cũng dùng composition đệ quy, nhưng composite giữ nhiều phần tử con; decorator bọc đúng một. |
| Strategy | Thay đổi bên trong đối tượng bằng cách hoán đổi thuật toán. Decorator thay đổi bên ngoài bằng cách bọc nó. |
| Chain of Responsibility | Cũng chuyển yêu cầu qua một chuỗi đối tượng, nhưng mỗi handler có thể dừng yêu cầu; decorator thường luôn chuyển tiếp. |
Câu hỏi thường gặp
Mẫu Decorator khác kế thừa ở điểm nào?
Kế thừa thêm hành vi cho cả một lớp lúc biên dịch. Decorator thêm hành vi cho một đối tượng lúc chạy, và nhiều decorator có thể kết hợp theo thứ tự tùy ý.
Mẫu Decorator khác mẫu Proxy ở điểm nào?
Chúng có cùng hình dạng. Decorator thêm tính năng mà client chọn xếp chồng, còn proxy kiểm soát truy cập vào đối tượng thật, thường là tạo nó hoặc quyết định có gọi nó hay không.
Decorator @ của TypeScript có phải là mẫu Decorator không?
Không hẳn. Decorator @ là một tính năng ngôn ngữ sửa đổi lớp và thành viên của chúng khi được định nghĩa. Chúng có thể dùng để cài đặt việc bọc, nhưng mẫu GoF nói về việc bọc đối tượng lúc chạy.
Thứ tự các decorator có quan trọng không?
Có. Mỗi decorator nhận lời gọi trước các đối tượng mà nó bọc, nên cache đặt ngoài logger sẽ che các lời gọi lặp khỏi logger, còn logger đặt ngoài cache sẽ thấy mọi lời gọi.