Decorator
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Cần ghi log mỗi lần đọc danh sách sản phẩm để xem vì sao API chậm. Sửa thẳng
DbProductStore thì việc log lẫn vào việc đọc database, và muốn
FakeProductStore có log lại phải sửa thêm class đó. Decorator thêm việc log
bằng một class bọc bên ngoài, không đụng tới class gốc.
Khái niệm
🎁 Decorator: class implement cùng interface với class gốc, giữ object gốc bên trong, và làm thêm việc trước hoặc sau khi chuyển lời gọi cho object đó.
Nơi gọi vẫn chỉ thấy ProductStore, không biết mình đang dùng bản gốc hay
bản đã bọc. Bọc nhiều lớp cũng được, mỗi lớp thêm một việc.
Ví dụ
ProductStore giữ như bài Truy vấn với JPA, Product rút gọn chỉ còn tên.
Ví dụ dùng MemoryProductStore thay cho DbProductStore để chạy bằng
java Main.java, không cần database:
// Main.java
void main() {
ProductStore store = new LoggingProductStore(
new MemoryProductStore());
var products = store.inStock();
}
// LoggingProductStore.java
import java.util.List;
class LoggingProductStore implements ProductStore {
private final ProductStore inner;
public LoggingProductStore(ProductStore inner) {
this.inner = inner;
}
@Override
public List<Product> inStock() {
System.out.println("Bắt đầu đọc sản phẩm");
List<Product> result = inner.inStock();
System.out.println("Xong, %d sản phẩm"
.formatted(result.size()));
return result;
}
@Override
public boolean setStock(long id, int stock) {
return inner.setStock(id, stock);
}
}
// MemoryProductStore.java
import java.util.List;
class MemoryProductStore implements ProductStore {
@Override
public List<Product> inStock() {
var pen = new Product();
pen.setName("Bút bi");
return List.of(pen);
}
@Override
public boolean setStock(long id, int stock) {
return true;
}
}
// ProductStore.java
import java.util.List;
// Có sẵn trong shop-api từ khoá Spring Boot Web API
interface ProductStore {
List<Product> inStock();
boolean setStock(long id, int stock);
}
// Product.java
class Product {
private String name = "";
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
}LoggingProductStoreimplementProductStorevà giữinnercũng làProductStore.inStockin log, gọiinner, in log tiếp.setStockchuyển thẳng choinner.- Bọc
DbProductStorehayFakeProductStoređều được, vì decorator chỉ biết interface. Nơi gọi không phải sửa.
Thử ngay
Giữ các file của ví dụ. Sửa LoggingProductStore để nhận thêm một tên và in
tên đó trong hai dòng log, rồi thay main để bọc hai lớp:
// Main.java
void main() {
ProductStore inner = new LoggingProductStore(
"B", new MemoryProductStore());
ProductStore store =
new LoggingProductStore("A", inner);
store.inStock();
}
// LoggingProductStore.java
import java.util.List;
class LoggingProductStore implements ProductStore {
private final String name;
private final ProductStore inner;
public LoggingProductStore(
String name, ProductStore inner) {
this.name = name;
this.inner = inner;
}
@Override
public List<Product> inStock() {
System.out.println(name + ": bắt đầu");
List<Product> result = inner.inStock();
System.out.println("%s: xong, %d sản phẩm"
.formatted(name, result.size()));
return result;
}
@Override
public boolean setStock(long id, int stock) {
return inner.setStock(id, stock);
}
}Đoán trước khi chạy: bốn dòng log của A và B in ra theo thứ tự nào?
Xem kết quả
A: bắt đầu
B: bắt đầu
B: xong, 1 sản phẩm
A: xong, 1 sản phẩmLời gọi đi từ lớp ngoài vào trong, kết quả đi từ trong ra ngoài.
Lỗi hay gặp
Kế thừa class gốc để thêm log. Class log khi đó chỉ gắn với đúng một class
gốc, muốn log FakeProductStore phải viết thêm class khác.
// SAI — gắn chặt với DbProductStore
// LoggingDbProductStore.java
class LoggingDbProductStore extends DbProductStore {
public LoggingDbProductStore(
ProductRepository products) {
super(products);
}
}// ĐÚNG — bọc bất kỳ ProductStore nào
// LoggingProductStore.java
import java.util.List;
class LoggingProductStore implements ProductStore {
private final ProductStore inner;
public LoggingProductStore(ProductStore inner) {
this.inner = inner;
}
@Override
public List<Product> inStock() {
return inner.inStock();
}
@Override
public boolean setStock(long id, int stock) {
return inner.setStock(id, stock);
}
}Đây là cách dùng composition thay cho kế thừa, như bài Composition của khoá OOP.
Tóm tắt
- Decorator bọc object gốc, cùng interface, thêm việc trước hoặc sau.
- Class gốc và nơi gọi đều không phải sửa.
- Bọc nhiều lớp: lời gọi đi từ ngoài vào, kết quả đi từ trong ra.
- Hay dùng cho log, cache, đo thời gian, kiểm tra quyền. Chính Spring cũng
bọc bean theo cách này để thêm
@Transactional.
Tự kiểm tra
0/3 câuMuốn cache kết quả inStock mà không sửa DbProductStore. Cách nào hợp nhất?
Decorator khác kế thừa ở điểm nào?
Bọc Timing(Logging(Db)). Gọi inStock thì lớp nào chạy phần "trước" đầu tiên?