Prototype Pattern (Mẫu Prototype)
Định nghĩa (Definition)
Mẫu Prototype tạo đối tượng mới bằng cách sao chép (Clone) một đối tượng hiện có (nguyên mẫu — prototype) thay vì tạo mới từ đầu. Điều này hữu ích khi việc tạo đối tượng tốn kém (Expensive Creation), hoặc khi cần bản sao của các đối tượng được cấu hình lúc runtime.
Ví dụ Coffee Shop
Một quán cà phê có menu các template cà phê (Coffee Template) — công thức cơ bản đã được cấu hình sẵn. Khi khách hàng đặt hàng, barista sao chép template và tùy chỉnh bản sao. Template gốc được giữ nguyên cho các đơn hàng sau.
Cấu trúc (Structure)
Triển khai (Implementation)
public interface ICoffeePrototype
{
ICoffeePrototype Clone();
}
public class CoffeeTemplate : ICoffeePrototype
{
public string Name { get; set; } = "";
public double BasePrice { get; set; }
public List<string> Ingredients { get; set; } = new();
public CupSize Size { get; set; } = CupSize.Medium;
public Dictionary<string, double> Extras { get; set; } = new();
// Shallow copy (Sao chép nông) — reference được chia sẻ!
public ICoffeePrototype Clone() => (ICoffeePrototype)MemberwiseClone();
// Deep copy (Sao chép sâu) — mọi thứ được nhân bản
public CoffeeTemplate DeepClone()
{
return new CoffeeTemplate
{
Name = Name,
BasePrice = BasePrice,
Ingredients = new List<string>(Ingredients),
Size = Size,
Extras = new Dictionary<string, double>(Extras)
};
}
public double TotalPrice => BasePrice + Extras.Values.Sum();
public override string ToString() =>
$"{Name} ({Size}) — ${TotalPrice:F2} [{string.Join(", ", Ingredients)}]" +
(Extras.Count > 0 ? $" + {string.Join(", ", Extras.Keys)}" : "");
}
Sử dụng — Template Menu
// Tạo prototype template (chính là "menu")
var latteTemplate = new CoffeeTemplate
{
Name = "Latte",
BasePrice = 4.00,
Ingredients = new() { "Espresso Shot", "Steamed Milk" },
Size = CupSize.Medium,
};
var mochaTemplate = new CoffeeTemplate
{
Name = "Mocha",
BasePrice = 4.50,
Ingredients = new() { "Espresso Shot", "Steamed Milk", "Chocolate Syrup" },
Extras = new() { { "Whipped Cream", 0.50 } },
};
// Clone và tùy chỉnh cho mỗi đơn hàng
var order1 = latteTemplate.DeepClone();
order1.Size = CupSize.Large;
order1.Extras["Oat Milk"] = 0.60;
var order2 = mochaTemplate.DeepClone();
order2.Ingredients.Add("Caramel Drizzle");
order2.Extras["Extra Shot"] = 0.75;
var order3 = latteTemplate.DeepClone(); // bản sao mới, không bị ảnh hưởng bởi order1
Console.WriteLine(order1); // Latte (Large) — $4.60 [Espresso Shot, Steamed Milk] + Oat Milk
Console.WriteLine(order2); // Mocha (Medium) — $5.75 [Espresso Shot, Steamed Milk, Chocolate Syrup, Caramel Drizzle] + Whipped Cream, Extra Shot
Console.WriteLine(order3); // Latte (Medium) — $4.00 [Espresso Shot, Steamed Milk]
Nguy hiểm của Shallow Copy (Sao chép nông)
var template = new CoffeeTemplate
{
Name = "Latte",
Ingredients = new() { "Espresso Shot", "Steamed Milk" },
};
// Shallow copy — danh sách Ingredients được CHIA SẺ
var shallowCopy = (CoffeeTemplate)template.Clone();
shallowCopy.Ingredients.Add("Vanilla Syrup");
// Template gốc đã bị ô nhiễm!
Console.WriteLine(template.Ingredients.Count); // 3 — BUG!
Console.WriteLine(shallowCopy.Ingredients.Count); // 3
// Deep copy — danh sách Ingredients ĐỘC LẬP
var deepCopy = template.DeepClone();
deepCopy.Ingredients.Add("Caramel");
// Gốc an toàn
Console.WriteLine(template.Ingredients.Count); // 3 (đã bị ô nhiễm từ trước)
Console.WriteLine(deepCopy.Ingredients.Count); // 4
MemberwiseClone() sao chép reference, không phải object mà chúng trỏ đến. List<T>, Dictionary<K,V>, array, và custom object đều được chia sẻ giữa bản gốc và bản sao. Luôn triển khai deep copy cho các property kiểu reference type (Reference-Type Properties).
Các cách Clone trong .NET (.NET Cloning Approaches)
| Cách tiếp cận | Độ sâu sao chép (Copy Depth) | Ghi chú |
|---|---|---|
MemberwiseClone() | Shallow (Nông) | Method protected trên object — chỉ truy cập được bên trong class |
ICloneable.Clone() | Phụ thuộc vào triển khai | Không rõ ràng (Ambiguous) — không xác định shallow hay deep. Tránh trong public API. |
| Deep copy thủ công | Deep (Sâu) | Đáng tin cậy nhất — sao chép từng field một cách rõ ràng |
| Serialization (JSON/Binary) | Deep (Sâu) | Tổng quát nhưng chậm hơn — serialize rồi deserialize |
record với with | Shallow (nhưng deep cho value type) | C# hiện đại — tốt nhất cho dữ liệu bất biến (Immutable Data) |
Deep Clone dựa trên Serialization
using System.Text.Json;
public static class CloningExtensions
{
public static T DeepClone<T>(this T source) where T : class
{
var json = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(json)!;
}
}
// Usage
var order = latteTemplate.DeepClone();
Prototype dựa trên Record
public record CoffeeOrder(
string Name,
double Price,
CupSize Size = CupSize.Medium,
string MilkType = "Whole"
)
{
// Expression 'with' tạo shallow copy với các sửa đổi
public CoffeeOrder Customize(CupSize? size = null, string? milk = null)
=> this with
{
Size = size ?? Size,
MilkType = milk ?? MilkType
};
}
// Usage
var baseLatte = new CoffeeOrder("Latte", 4.00);
var customized = baseLatte.Customize(CupSize.Large, "Oat");
// baseLatte không thay đổi: Latte (Medium, Whole)
// customized là mới: Latte (Large, Oat)
Khi nào sử dụng (When to Use)
- Khởi tạo đối tượng tốn kém (Expensive Creation) — khởi tạo nặng, gọi DB, tính toán phức tạp
- Cần nhiều đối tượng tương tự chỉ khác biệt một chút (Similar Objects)
- Đối tượng được cấu hình lúc runtime (Runtime-Configured) và muốn bảo tồn cấu hình đó
- Muốn tránh xây dựng hệ thống factory song song (Parallel Factory Hierarchy)
Khi nào KHÔNG sử dụng (When NOT to Use)
- Đối tượng đơn giản và rẻ để tạo — chỉ dùng constructor
- Tất cả field là value type —
MemberwiseClone()hoạt động, nhưng copy constructor rõ ràng hơn - Cần deep copy cho đồ thị đối tượng phức tạp (Complex Object Graph) — serialization-based cloning có thể chậm
- Hệ thống phân cấp class (Class Hierarchy) thay đổi thường xuyên — bảo trì
Clone()qua nhiều subclass rất tẻ nhạt
Điểm chính (Key Takeaways)
- Prototype sao chép đối tượng hiện có thay vì tạo mới từ đầu
- Shallow copy (Sao chép nông) chia sẻ reference-type field — hầu như luôn là bug cho mutable object
- Deep copy (Sao chép sâu) nhân bản mọi thứ — triển khai thủ công hoặc dùng serialization
- Trong C# hiện đại,
recordvớiwithexpression cung cấp cơ chế prototype tích hợp sẵn, gọn gàng ICloneablekhông rõ ràng (shallow hay deep?) — tránh dùng trong public API; ưu tiên methodDeepClone()được đặt tên rõ ràng
Câu hỏi phỏng vấn (Interview Questions)
Q: Khác biệt giữa shallow copy (sao chép nông) và deep copy (sao chép sâu) là gì? Shallow copy nhân bản object nhưng chia sẻ reference đến nested object. Deep copy nhân bản mọi thứ đệ quy (Recursively) — bản sao hoàn toàn độc lập.
Q: Tại sao ICloneable được coi là có vấn đề?
Method Clone() không xác định shallow hay deep copy. Người gọi không thể biết họ nhận được gì. Ưu tiên method DeepClone() / ShallowClone() rõ ràng.
Q: Khi nào Prototype tốt hơn Factory Method? Khi việc tạo đối tượng tốn kém (Expensive Creation) và có một instance đã cấu hình để sao chép. Prototype tránh lặp lại thiết lập tốn kém. Factory Method tạo mới từ đầu mỗi lần.
Chủ đề liên quan (Related Topics)
- Builder — xây dựng đối tượng phức tạp từng bước
- Structs & Records —
recordtype vớiwithcho prototype hiện đại - Collections — sao chép collection (reference vs value semantics)