VieTopik
Tiếng HànTiếng AnhIT
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Kiến trúc và chất lượng codeDesign pattern
Bài 13/17
6 phút

Decorator

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 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;
    }
}
  • LoggingProductStore implement ProductStore và giữ inner cũng là ProductStore.
  • inStock in log, gọi inner, in log tiếp. setStock chuyển thẳng cho inner.
  • Bọc DbProductStore hay FakeProductStore đều được, vì decorator chỉ biết interface. Nơi gọi không phải sửa.
inStock inStock Nơi gọi LoggingProductStore MemoryProductStore
Nơi gọi dùng lớp bọc, lớp bọc chuyển lời gọi vào object gốc

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ẩm

Lờ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âu
Câu 1

Muốn cache kết quả inStock mà không sửa DbProductStore. Cách nào hợp nhất?

Câu 2

Decorator khác kế thừa ở điểm nào?

Câu 3

Bọc Timing(Logging(Db)). Gọi inStock thì lớp nào chạy phần "trước" đầu tiên?

FactoryObserver

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt