TungDaDev's Blog

prototype pattern

Design prototype.webp
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 Cloneable là 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

  1. Interface không có phương thức (Broken Abstraction): java.lang.Cloneable là một Marker Interface rỗng, nó không hề chứa method clone()! Phương thức clone() thực chất lại nằm ở java.lang.Object với access modifier là protected. Việc implement Cloneable chỉ nhằm mục đích duy nhất: báo cho Object.clone() biết không được ném ra ngoại lệ CloneNotSupportedException.
  2. 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.
  3. Phá vỡ tính bất biến của trường final: Phương thức clone() không thể gán lại giá trị cho các trường được đánh dấu final. Nếu class của bạn có một trường private final List<String> permissions, bạn không thể thực hiện deep copy trường đó trong phương thức clone() mà không dùng Reflection để hack qua cờ final!
  4. Ném ra Checked Exception lỗi thời: CloneNotSupportedException buộc lập trình viên phải viết khối try-catch rá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 ConstructorJackson JSONKryo BinaryJava 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

  1. Tuyệt đối không dùng Cloneable: Loại bỏ mọi khai báo implements Cloneable và @Override clone() khỏi codebase doanh nghiệp.
  2. Ư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.
  3. 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(...)).
  4. 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.
  5. 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 😎 👍🏻 🚀 🔥.

← Previous postbuilder pattern
Next post →stack & queue