prototype pattern

- Published on
- /11 mins read/
Trong nhóm các mẫu thiết kế khởi tạo (Creational Design Patterns), Prototype Pattern giải quyết một bài toán rất đặc thù: Tạo ra một đối tượng mới bằng cách nhân bản (cloning) một đối tượng mẫu có sẵn, thay vì khởi tạo từ đầu bằng từ khóa new.
Mẫu thiết kế này đặc biệt hữu dụng khi chi phí tạo một instance mới quá đắt đỏ: ví dụ như đối tượng cần parse một file cấu hình XML/JSON khổng lồ, tính toán cây cú pháp (AST), load metadata từ database, hoặc thiết lập một cỗ máy trạng thái (State Machine) phức tạp.
Tuy nhiên, trong thế giới Java, Prototype Pattern lại gắn liền với một trong những "vết nhơ thiết kế" tồi tệ nhất của ngôn ngữ: Interface Cloneable và phương thức Object.clone().
Bài viết này sẽ phân tích tận gốc rễ cơ chế cấp phát bộ nhớ của JVM, chỉ rõ tại sao các kiến trúc sư phần mềm cấm sử dụng Cloneable, và so sánh 4 giải pháp nhân bản đối tượng chuẩn mực trong các hệ thống Enterprise phân tán.
# cơ chế bộ nhớ: shallow copy vs deep copy
Cốt lõi của mọi lỗi tiềm ẩn trong Prototype Pattern bắt nguồn từ sự khác biệt giữa Shallow Copy (Sao chép nông) và Deep Copy (Sao chép sâu) trên bộ nhớ Heap của JVM.
# shallow copy (sao chép nông)
- JVM chỉ cấp phát vùng nhớ mới cho đối tượng ngoài cùng và sao chép từng bit dữ liệu (bitwise copy).
- Các trường nguyên thủy (
int,boolean,double) được sao chép giá trị độc lập. - Hiểm họa: Các tham chiếu đối tượng (như
List,Map,Date, custom Objects) chỉ được copy địa chỉ con trỏ (Memory Address). Cả đối tượng gốc và đối tượng nhân bản đều cùng trỏ vào một vùng nhớ chung. - Hậu quả: Khi clone thay đổi dữ liệu trong list (
cloned.getRoles().clear()), đối tượng prototype gốc cũng bị biến đổi dữ liệu ngoài ý muốn! Đây là nguồn cơn của vô số lỗi concurrency race conditions và dữ liệu bị đầu độc (data corruption).
# deep copy (sao chép sâu)
- Toàn bộ cây đối tượng (Object Graph) bao gồm mọi nút con, cháu, chắt đều được cấp phát vùng nhớ mới hoàn toàn trên Heap.
- Đối tượng gốc và đối tượng nhân bản độc lập 100%. Mọi thay đổi trên bản clone không bao giờ ảnh hưởng đến bản gốc.
# vì sao cloneable là một thiết kế thất bại trong JDK?
Trong cuốn sách kinh điển Effective Java (Item 13), tác giả Joshua Bloch (người thiết kế nhiều phần cốt lõi của Java Collections Framework) đã khẳng định:
Interface
Cloneablelà một lỗi thiết kế nghiêm trọng (seriously flawed) và không nên được sử dụng trong các hệ thống mới.
# những khuyết tật kiến trúc của cloneable
- Interface không có phương thức (Broken Abstraction):
java.lang.Cloneablelà một Marker Interface rỗng, nó không hề chứa methodclone()! Phương thứcclone()thực chất lại nằm ởjava.lang.Objectvới access modifier làprotected. Việc implementCloneablechỉ nhằm mục đích duy nhất: báo choObject.clone()biết không được ném ra ngoại lệCloneNotSupportedException. - Vượt mặt Constructor (Bypasses Constructors): Khi gọi
super.clone(), JVM cấp phát bộ nhớ bằng cơ chế native C++ mà hoàn toàn không chạy qua bất kỳ Constructor nào. Điều này phá vỡ nguyên lý hướng đối tượng: các quy tắc kiểm tra tính hợp lệ (invariants validation) nằm trong constructor bị bỏ qua hoàn toàn. - Phá vỡ tính bất biến của trường
final: Phương thứcclone()không thể gán lại giá trị cho các trường được đánh dấufinal. Nếu class của bạn có một trườngprivate final List<String> permissions, bạn không thể thực hiện deep copy trường đó trong phương thứcclone()mà không dùng Reflection để hack qua cờ final! - Ném ra Checked Exception lỗi thời:
CloneNotSupportedExceptionbuộc lập trình viên phải viết khốitry-catchrác rưởi hoặc ném ngoại lệ lên chữ ký hàm, gây ô nhiễm API design.
# bốn giải pháp deep cloning chuẩn enterprise
Để triển khai Prototype Pattern an toàn và hiệu năng cao trong môi trường Production, các kiến trúc sư phần mềm sử dụng 4 kỹ thuật sau:
# copy constructor & copy Factory (khuyến nghị hàng đầu)
Đây là giải pháp được khuyến khích mạnh mẽ nhất trong Effective Java. Nó không yêu cầu interface đặc biệt, an toàn tuyệt đối với các trường final, và kiểm soát sâu từng thuộc tính mutable:
public class SecurityPolicy {
private final String policyId;
private final String department;
private final Set<String> allowedScopes;
private final AuditMetadata metadata;
// Standard constructor
public SecurityPolicy(String policyId, String department, Set<String> allowedScopes, AuditMetadata metadata) {
this.policyId = Objects.requireNonNull(policyId);
this.department = Objects.requireNonNull(department);
this.allowedScopes = new HashSet<>(allowedScopes);
this.metadata = metadata;
}
// 1. Copy Constructor: Deep copy an toàn tuyệt đối
public SecurityPolicy(SecurityPolicy source) {
this.policyId = UUID.randomUUID().toString(); // Cấp phát ID mới cho clone
this.department = source.department;
// Deep copy Collection
this.allowedScopes = new HashSet<>(source.allowedScopes);
// Deep copy nested object
this.metadata = new AuditMetadata(source.metadata);
}
// 2. Copy Factory (Alternative)
public static SecurityPolicy newInstance(SecurityPolicy source) {
return new SecurityPolicy(source);
}
public Set<String> getAllowedScopes() {
return Collections.unmodifiableSet(allowedScopes);
}
}# deep copy qua JSON serialization (jackson / fastjson2)
Khi đối tượng là một cây phân cấp sâu (Deep Object Tree) gồm hàng chục model lồng nhau, việc viết Copy Constructor thủ công sẽ tốn hàng nghìn dòng boilerplate code và dễ bỏ sót trường mới thêm.
Sử dụng Jackson ObjectMapper là giải pháp cực kỳ phổ biến trong các hệ thống Spring Boot:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public final class DeepCloner {
private static final ObjectMapper MAPPER = new ObjectMapper()
.findAndRegisterModules(); // Support Java 8 Date/Time, Records
private DeepCloner() {}
@SuppressWarnings("unchecked")
public static <T> T deepClone(T source, Class<T> clazz) {
if (source == null) return null;
try {
// Serialize to byte array or string, then deserialize back to fresh instance
byte[] bytes = MAPPER.writeValueAsBytes(source);
return MAPPER.readValue(bytes, clazz);
} catch (Exception e) {
throw new IllegalStateException("Thất bại khi thực hiện deep clone qua Jackson", e);
}
}
}Nhược điểm: Tốn CPU do phải thực hiện Reflection, serialize String/Bytes và deserialize. Không phù hợp cho hot-path triệu TPS.
# high-performance binary serialization (kryo)
Đối với các ứng dụng yêu cầu throughput cao (ví dụ: Apache Spark, Game Engine, High-Frequency Trading), Kryo là thư viện binary serialization có tốc độ nhanh gấp 10-20 lần so với Jackson hoặc Java Native Serialization:
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.util.Pool;
public class KryoCloner {
// Kryo không thread-safe, sử dụng Pool để tái sử dụng instance an toàn
private static final Pool<Kryo> KRYO_POOL = new Pool<>(true, false, 32) {
@Override
protected Kryo create() {
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(false);
return kryo;
}
};
public static <T> T clone(T object) {
Kryo kryo = KRYO_POOL.obtain();
try {
return kryo.copy(object); // Kryo native memory deep copy!
} finally {
KRYO_POOL.free(kryo);
}
}
}# modern Java Records & wither pattern (Java 17+)
Với sự xuất hiện của Java Records (JEP 395), mọi dữ liệu đều mang tính chất bất biến (Immutable). Thay vì clone một mutable object rồi mutate giá trị, ta sử dụng mô hình Wither: Tạo ra bản copy mới với chỉ một vài trường được thay đổi:
public record DeploymentProfile(
String environment,
int cpuCores,
int memoryGb,
List<String> featureFlags
) {
// Compact constructor bảo vệ tính bất biến
public DeploymentProfile {
featureFlags = List.copyOf(featureFlags); // Immutable defensive copy
}
// Wither method để nhân bản profile với tham số thay đổi
public DeploymentProfile withEnvironment(String newEnv) {
return new DeploymentProfile(newEnv, this.cpuCores, this.memoryGb, this.featureFlags);
}
public DeploymentProfile withScale(int newCores, int newMemGb) {
return new DeploymentProfile(this.environment, newCores, newMemGb, this.featureFlags);
}
}# ma trận đánh giá so sánh các giải pháp
| Tiêu chí | Object.clone() (Legacy) | Copy Constructor | Jackson JSON | Kryo Binary | Java Record Withers |
|---|---|---|---|---|---|
| Tính an toàn bộ nhớ | 🔴 Nguy hiểm (Shallow) | 🟢 Tuyệt đối | 🟢 Tuyệt đối | 🟢 Tuyệt đối | 🟢 Tuyệt đối |
| Tốc độ thực thi | ⚡ Rất nhanh (Native) | ⚡ Rất nhanh (JVM Inlined) | 🐢 Chậm (Reflection + I/O) | 🚀 Cực nhanh | ⚡ Siêu nhanh |
Hỗ trợ final field | 🔴 Không | 🟢 Hoàn toàn | 🟡 Cần constructor/setter | 🟢 Hoàn toàn | 🟢 Mặc định 100% |
| Chi phí bảo trì code | 🔴 Rất cao | 🟡 Cần viết code copy | 🟢 0 dòng boilerplate | 🟢 0 dòng boilerplate | 🟢 Cực thấp |
| Xử lý Cyclic Graph | 🔴 StackOverflowError | 🔴 Cần xử lý thủ công | 🔴 Cần @JsonIdentityInfo | 🟢 Tự động xử lý | ⚪ Không có cycle |
# case study thực tế: Prototype registry cho dynamic rule engine
Trong các hệ thống FinTech xử lý gian lận (Fraud Detection), mỗi luồng giao dịch cần một bộ Evaluation Context chứa hàng chục bảng tham số rủi ro đã được tiền xử lý (pre-loaded). Việc load lại các rules này từ Database tốn 50-100ms.
Sử dụng Prototype Registry Pattern:
@Component
public class FraudRuleEngineRegistry {
private final Map<String, RuleContext> prototypeCache = new ConcurrentHashMap<>();
@PostConstruct
public void warmupPrototypes() {
// Tốn 500ms để parse các rule phức tạp một lần duy nhất lúc khởi động
RuleContext highRiskRule = compileComplexRuleMatrix("HIGH_RISK_DOMESTIC");
prototypeCache.put("HIGH_RISK_DOMESTIC", highRiskRule);
}
public RuleContext createEngineForTransaction(String ruleCode, String transactionId) {
RuleContext prototype = prototypeCache.get(ruleCode);
if (prototype == null) {
throw new IllegalArgumentException("Không tìm thấy prototype cho rule: " + ruleCode);
}
// Nhân bản sâu cực nhanh mà không cần gọi DB / Compiler
RuleContext cloned = KryoCloner.clone(prototype);
cloned.bindTransactionId(transactionId);
return cloned;
}
}# production checklist
- Tuyệt đối không dùng
Cloneable: Loại bỏ mọi khai báoimplements Cloneablevà@Override clone()khỏi codebase doanh nghiệp. - Ưu tiên Copy Constructor: Đối với các class có số lượng thuộc tính vừa phải và cần tối ưu tốc độ nano-giây.
- Phòng thủ Collection: Trong Copy Constructor, luôn tạo collection mới (
new ArrayList<>(source.list)) hoặc dùng unmodifiable wrappers (Collections.unmodifiableList(...)). - Sử dụng Kryo cho High-Load: Nếu hệ thống cần nhân bản hàng trăm nghìn đối tượng/giây mà cấu trúc object graph phức tạp.
- Chuyển dịch sang Immutable Records: Với Java 17+, tận dụng Records và Wither methods để đạt được tính toàn vẹn dữ liệu hoàn hảo.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # cơ chế bộ nhớ: shallow copy vs deep copy
- # shallow copy (sao chép nông)
- # deep copy (sao chép sâu)
- # vì sao cloneable là một thiết kế thất bại trong JDK?
- # những khuyết tật kiến trúc của cloneable
- # bốn giải pháp deep cloning chuẩn enterprise
- # copy constructor & copy Factory (khuyến nghị hàng đầu)
- # deep copy qua JSON serialization (jackson / fastjson2)
- # high-performance binary serialization (kryo)
- # modern Java Records & wither pattern (Java 17+)
- # ma trận đánh giá so sánh các giải pháp
- # case study thực tế: Prototype registry cho dynamic rule engine
- # production checklist