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 dùng vẫn chỉ thấy IProductStore, 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ụ
IProductStore và Product giữ như bài Truy vấn với EF Core. Ví dụ dùng
MemoryProductStore thay cho DbProductStore để chạy được không cần
database:
IProductStore store =
new LoggingProductStore(new MemoryProductStore());
var products = await store.InStockAsync();
public class LoggingProductStore : IProductStore
{
private readonly IProductStore _inner;
public LoggingProductStore(IProductStore inner)
{
_inner = inner;
}
public async Task<List<Product>> InStockAsync()
{
Console.WriteLine("Bắt đầu đọc sản phẩm");
var result = await _inner.InStockAsync();
Console.WriteLine(
$"Xong, {result.Count} sản phẩm");
return result;
}
public Task<bool> SetStockAsync(int id, int stock)
{
return _inner.SetStockAsync(id, stock);
}
}
public class MemoryProductStore : IProductStore
{
public Task<List<Product>> InStockAsync()
{
var list = new List<Product>
{
new Product { Id = 1, Name = "Bút bi" }
};
return Task.FromResult(list);
}
public Task<bool> SetStockAsync(int id, int stock)
{
return Task.FromResult(true);
}
}
// Có sẵn trong ShopApi từ khoá ASP.NET Core
public interface IProductStore
{
Task<List<Product>> InStockAsync();
Task<bool> SetStockAsync(int id, int stock);
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
public int Stock { get; set; }
}LoggingProductStoreimplementIProductStorevà giữ_innercũng làIProductStore.InStockAsyncin log, gọi_inner, in log tiếp.SetStockAsyncchuyển thẳng cho_inner.- Bọc
DbProductStorehayFakeProductStoređều được, vì decorator chỉ biết interface. Nơi dùng không phải sửa.
Thử ngay
Sửa LoggingProductStore để nhận thêm một cái tên và in tên đó trong hai dòng
log, rồi bọc hai lớp:
IProductStore inner = new LoggingProductStore(
"B", new MemoryProductStore());
IProductStore store =
new LoggingProductStore("A", inner);
await store.InStockAsync();
public class LoggingProductStore : IProductStore
{
private readonly string _name;
private readonly IProductStore _inner;
public LoggingProductStore(
string name, IProductStore inner)
{
_name = name;
_inner = inner;
}
public async Task<List<Product>> InStockAsync()
{
Console.WriteLine(_name + ": bắt đầu");
var result = await _inner.InStockAsync();
Console.WriteLine(
$"{_name}: xong, {result.Count} sản phẩm");
return result;
}
public Task<bool> SetStockAsync(int id, int stock)
{
return _inner.SetStockAsync(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. Middleware trong bài Middleware và pipeline của khoá ASP.NET Core cũng chạy theo thứ tự này.
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
public class LoggingDbProductStore : DbProductStore
{
public LoggingDbProductStore(ShopDbContext db)
: base(db)
{
}
}// ĐÚNG — bọc bất kỳ IProductStore nào
public class LoggingProductStore : IProductStore
{
private readonly IProductStore _inner;
public LoggingProductStore(IProductStore inner)
{
_inner = inner;
}
public Task<List<Product>> InStockAsync()
{
return _inner.InStockAsync();
}
public Task<bool> SetStockAsync(int id, int stock)
{
return _inner.SetStockAsync(id, stock);
}
}Đây là 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 dùng đề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 quyền.
Tự kiểm tra
0/3 câuMuốn cache kết quả InStockAsync 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 InStockAsync thì lớp nào chạy phần "trước" đầu tiên?