Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

prototype pattern

Tung Bui Thanh (TungDaDev)11 mins read

Design prototype.webp

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 khá cụ thể: tạo object mới bằng cách nhân bản (cloning) một object mẫu có sẵn, thay vì khởi tạo từ đầu bằng new.

Pattern này hữu ích khi tạo instance mới quá tốn kém, ví dụ object phải parse một file cấu hình XML/JSON rất lớn, dựng cây cú pháp (AST), load metadata từ database, hoặc dựng một State Machine phức tạp.

Có điều, trong Java, Prototype Pattern lại dính tới một trong những thiết kế bị chê nhiều nhất của ngôn ngữ: interface Cloneable và method Object.clone().

Bài này nói về cách JVM cấp phát bộ nhớ khi clone, vì sao nhiều người cấm dùng Cloneable, và so sánh 4 cách nhân bản object thường gặp trong hệ thống enterprise.


# cơ chế bộ nhớ: shallow copy vs deep copy

Phần lớn lỗi tiềm ẩn của Prototype Pattern đến từ chỗ phân biệt Shallow Copy (sao chép nông) với Deep Copy (sao chép sâu) trên 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.
  • Rủi ro: Các tham chiếu đối tượng (như List, Map, Date, custom Objects) chỉ được copy địa chỉ con trỏ (Memory Address). Object gốc và object nhân bản cùng trỏ vào một vùng nhớ.
  • Hậu quả: Khi bản clone sửa dữ liệu trong list (cloned.getRoles().clear()), prototype gốc cũng bị đổi theo ngoài ý muốn. Nếu nhiều thread cùng cầm các bản clone "tưởng là riêng" thì đây còn là chỗ sinh ra race condition và hỏng dữ liệu (data corruption).

Shallow copy không phải lúc nào cũng sai. Nếu các trường tham chiếu đều trỏ tới object bất biến (String, Integer, LocalDate, List.copyOf(...)), dùng chung chúng là hoàn toàn an toàn, lại tiết kiệm bộ nhớ. Chỉ phần mutable mới cần copy sâu.

# deep copy (sao chép sâu)

  • Toàn bộ cây đối tượng (Object Graph), từ nút con tới cháu chắt, đều được cấp phát vùng nhớ mới trên Heap.
  • Bản gốc và bản nhân bản độc lập hoàn toàn. Sửa bản clone không ảnh hưởng bản gốc.

# vì sao cloneable bị coi là thiết kế hỏng trong JDK?

Trong Effective Java (bản 3, Item 13: "Override clone judiciously"), Joshua Bloch (người thiết kế nhiều phần của Java Collections Framework) dành cả một mục để mổ xẻ Cloneable. Kết luận của ông, diễn lại theo ý:

Với chừng ấy vấn đề, interface mới không nên extend Cloneable, class mới không nên implement nó. Cách tốt hơn để copy object là copy constructor hoặc copy factory.

# những lỗi 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 chứa method clone(). Method clone() nằm ở java.lang.Object và là protected. Implement Cloneable chỉ để báo cho Object.clone() biết là đừng ném 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++ và không chạy qua constructor nào. Các kiểm tra tính hợp lệ (invariants validation) nằm trong constructor bị bỏ qua hết, trái với nguyên tắc hướng đối tượng.
  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 deep copy được trường đó trong clone() nếu không dùng Reflection để lách cờ final.
  4. Ném ra Checked Exception lỗi thời: CloneNotSupportedException buộc phải viết try-catch thừa hoặc ném ngoại lệ lên chữ ký method, làm API rối thêm.

# bốn cách deep clone hay dùng

Để dùng Prototype Pattern an toàn và nhanh trên production, thường có 4 kỹ thuật sau:


# copy constructor & copy Factory (nên ưu tiên)

Đây là cách Effective Java khuyên dùng. Không cần interface đặc biệt, xử lý tốt trường final, và kiểm soát được 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ết Copy Constructor bằng tay sẽ tốn hàng nghìn dòng boilerplate và dễ sót trường mới thêm.

Dùng Jackson ObjectMapper là cách rất phổ biến trong các dự án 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 vì phải serialize ra byte rồi parse lại. Không hợp với hot-path cỡ triệu TPS. Ngoài ra Jackson chỉ copy những gì nó "nhìn thấy": trường không có getter hoặc bị @JsonIgnore sẽ mất, trường kiểu interface/đa hình (Object, Animal) sẽ bị đọc lại thành LinkedHashMap hoặc lỗi nếu không bật type info, và class cần constructor mặc định hoặc @JsonCreator. Nên viết unit test so sánh bản gốc với bản clone cho từng model.


# high-performance binary serialization (kryo)

Với ứng dụng cần throughput cao, Kryo (thư viện serialization nhị phân mà Apache Spark dùng làm tuỳ chọn tăng tốc) thường nhanh hơn rõ rệt so với JSON hay Java Serialization. Riêng việc copy object thì Kryo còn có kryo.copy(), copy thẳng trên heap mà không cần ghi ra byte. Mức nhanh hơn bao nhiêu thì tuỳ model, nên benchmark (JMH) với dữ liệu của chính bạn:

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); // Deep copy trực tiếp trên heap qua serializer của Kryo, không cần ghi ra byte
        } finally {
            KRYO_POOL.free(kryo);
        }
    }
}

Một vài điểm cần biết khi dùng Kryo 5: mặc định nó cần class có constructor không tham số (hoặc phải cấu hình StdInstantiatorStrategy), copy class của JDK trên Java 17+ có thể phải --add-opens vì module system chặn reflection, và kryo.copy() mặc định có theo dõi tham chiếu nên xử lý được object graph có vòng.


# modern Java Records & wither pattern (Java 17+)

Với Java Records (JEP 395), dữ liệu là bất biến (Immutable). Thay vì clone một mutable object rồi sửa giá trị, ta dùng mô hình Wither: tạo bản copy mới, chỉ đổi vài trường:

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);
    }
}

# 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⚪ Shallow, không đệ quy🔴 Cần xử lý thủ công🔴 Cần @JsonIdentityInfo🟢 Tự động xử lý⚪ Không có cycle

# case study: Prototype registry cho dynamic rule engine

Giả sử trong một hệ thống chống gian lận (Fraud Detection), mỗi giao dịch cần một Evaluation Context chứa hàng chục bảng tham số rủi ro đã được tiền xử lý. Load lại các rule này từ database mỗi lần thì tốn cỡ vài chục tới cả trăm mili giây, quá chậm cho một giao dịch thanh toán.

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;
    }
}

Một lưu ý về thiết kế: chỉ phần context bị giao dịch ghi vào (transaction ID, điểm rủi ro tích luỹ...) mới thật sự cần clone. Các bảng tham số chỉ đọc nên được làm bất biến và dùng chung giữa mọi bản clone; clone cả chúng chỉ tốn CPU và rác cho GC. Và prototype trong cache phải không bao giờ bị sửa sau khi warmup, nếu không mọi bản clone về sau đều nhiễm thay đổi đó.


# production checklist

  1. Không dùng Cloneable: Xoá mọi implements Cloneable và @Override clone() khỏi codebase.
  2. Ưu tiên Copy Constructor: Hợp với class có vừa phải thuộc tính và cần tối ưu tốc độ 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 bản bất biến (List.copyOf(...)). Collections.unmodifiableList(...) chỉ là một lớp "view" chặn ghi, list gốc bên dưới đổi thì view cũng đổi theo, nên chỉ dùng nó cho getter.
  4. Sử dụng Kryo cho High-Load: Khi cần nhân bản hàng trăm nghìn object mỗi giây mà object graph phức tạp.
  5. Chuyển sang Immutable Records: Với Java 17+, dùng Records và Wither method để giữ dữ liệu toàn vẹn.

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 😎 👍🏻 🚀 🔥.