Prototype Pattern เป็นรูปแบบการออกแบบในกลุ่ม Creational Design Patterns ที่สร้างออบเจ็กต์ใหม่ด้วยการคัดลอกสถานะจากออบเจ็กต์ต้นแบบ แทนการให้โค้ดฝั่งผู้ใช้ประกอบรายละเอียดทั้งหมดขึ้นมาใหม่ทุกครั้ง

แนวคิดนี้เหมาะกับการทำสำเนาเอกสาร รายงาน ตัวละครในเกม หรือชุดการตั้งค่าที่มีข้อมูลพื้นฐานร่วมกัน แล้วปรับรายละเอียดเฉพาะของสำเนาแต่ละชุด

Report copy = template.copy();
copy.setTitle("รายงานใหม่");

คำถามสำคัญในการใช้แพตเทิร์นนี้คือ ข้อมูลส่วนใดต้องคัดลอก และส่วนใดตั้งใจใช้ร่วมกัน การได้ออบเจ็กต์ใหม่ไม่ได้รับประกันว่าข้อมูลภายในทุกระดับจะแยกจากต้นฉบับ

บทความนี้ใช้ Java 17 ขึ้นไป ตัวอย่างสมบูรณ์ทั้งสามโปรแกรมทดสอบกับ OpenJDK 17.0.20 โดยไม่ใช้ไลบรารีภายนอก

#สิ่งที่จะได้เรียนรู้

  • อธิบายบทบาทของ Prototype, Concrete Prototype, Client และ Registry
  • แยกความต่างระหว่างการกำหนดตัวแปรอ้างอิง, Shallow Copy และ Deep Copy
  • สร้าง copy() ด้วย Copy Constructor ที่กำหนดนโยบายชัดเจน
  • ทำ Registry สำหรับเก็บต้นแบบและสร้างสำเนา
  • เข้าใจ Object.clone() และ Cloneable ของ Java
  • ตรวจสอบว่าการแก้สำเนาไม่เปลี่ยนต้นแบบโดยไม่ตั้งใจ

#1. ปัญหาที่ Prototype ช่วยแก้

สมมติระบบรายงานมีรูปแบบมาตรฐาน ซึ่งกำหนดหัวข้อ เนื้อหาเริ่มต้น และโครงสร้างส่วนต่าง ๆ ไว้แล้ว หากสร้างทุกฉบับด้วยการเขียนขั้นตอนตั้งค่าเดิมซ้ำ โค้ดฝั่งผู้ใช้จะต้องรู้รายละเอียดของรายงานมากขึ้น และอาจตั้งค่าไม่ครบ

วิธีใช้ Prototype คือเตรียมรายงานต้นแบบหนึ่งฉบับ แล้วให้ต้นแบบรับผิดชอบการสร้างสำเนาของตัวเอง ผู้ใช้จึงเลือกต้นแบบ คัดลอก และเปลี่ยนเฉพาะส่วนที่ต้องการ

ประโยชน์หลักคือการนำ สถานะที่เตรียมไว้แล้ว มาใช้ซ้ำ และรวมกฎการคัดลอกไว้ในจุดที่เข้าใจโครงสร้างของออบเจ็กต์

การคัดลอกอาจช่วยลดการตั้งค่าที่มีต้นทุนสูง แต่ไม่ได้เร็วกว่า constructor เสมอไป โดยเฉพาะเมื่อมีข้อมูลภายในขนาดใหญ่ที่ต้องทำ Deep Copy ควรวัดประสิทธิภาพกับงานจริงหากความเร็วเป็นเป้าหมาย

#2. องค์ประกอบของแพตเทิร์น

องค์ประกอบ หน้าที่ ตัวอย่างในบทความ
Prototype กำหนดสัญญาการสร้างสำเนา Prototype<T> และ copy()
Concrete Prototype รู้ว่าต้องคัดลอกสถานะภายในอย่างไร Report
Client ขอสำเนาโดยเรียกผ่านสัญญาของต้นแบบ PrototypeDemo.main()
Prototype Registry เก็บต้นแบบตามชื่อและคืนสำเนา ReportRegistry

Registry เป็นองค์ประกอบเสริม ใช้เมื่อมีหลายต้นแบบให้เลือก แม้ไม่มี Registry ก็ใช้ Prototype ได้

classDiagram
    class Prototype~T~ {
        <<interface>>
        +copy() T
    }
    class Report {
        -String title
        -List sections
        +copy() Report
    }
    class Section {
        -String text
    }
    class ReportRegistry {
        -Map templates
        +register(key, template)
        +create(key) Report
    }
    class PrototypeDemo {
        +main(args)
    }
    Prototype <|.. Report
    Report "1" *-- "0..*" Section
    ReportRegistry o-- Report : stores snapshots
    PrototypeDemo ..> Prototype : requests a copy
    PrototypeDemo ..> ReportRegistry : selects a template

#3. การอ้างอิงร่วมกันไม่ใช่การคัดลอก

คำสั่งต่อไปนี้สร้างตัวแปรอีกตัวที่อ้างถึงออบเจ็กต์เดิม ไม่ได้สร้างรายงานใหม่

Report same = template;
System.out.println(same == template); // true

หากเรียก same.setTitle(...) ก็จะเปลี่ยนรายงานที่ template อ้างถึงด้วย

ส่วนการคัดลอกตามสัญญาที่เราออกแบบในบทความต้องได้ออบเจ็กต์คนละตัว

Report copy = template.copy();
System.out.println(copy == template); // false

อย่างไรก็ตาม ต้องตรวจสอบข้อมูลภายในต่อด้วย เพราะออบเจ็กต์ชั้นนอกอาจแยกกัน ขณะที่ List หรือสมาชิกภายในยังเป็นตัวเดิม

#4. Shallow Copy และ Deep Copy

วิธี ออบเจ็กต์ชั้นนอก ข้อมูลภายในที่แก้ไขได้ ผลที่ต้องพิจารณา
b = a ตัวเดิม ตัวเดิม ทั้งสองตัวแปรอ้างถึงออบเจ็กต์เดียวกัน
Shallow Copy ตัวใหม่ คัดลอกค่า reference จึงอาจใช้ตัวเดิมร่วมกัน แก้ข้อมูลที่ใช้ร่วมกันแล้วอาจเห็นการเปลี่ยนแปลงทั้งสองฝั่ง
Deep Copy ตามนโยบาย ตัวใหม่ สร้างสำเนาของข้อมูลที่ต้องแยกให้ครบตามขอบเขตที่กำหนด การเปลี่ยนข้อมูลส่วนที่แยกแล้วไม่กระทบต้นฉบับ

Deep Copy ไม่จำเป็นต้องสร้างสำเนาของทุกสิ่งโดยไม่มีข้อยกเว้น ข้อมูล immutable เช่น String สามารถใช้ร่วมกันได้ ส่วนข้อมูล mutable เช่น Section ในตัวอย่างนี้ต้องคัดลอกหากต้องการแก้ไขอย่างอิสระ

สำหรับโครงสร้างข้อมูลที่มีวงจรหรือมีหลายเส้นทางอ้างถึงออบเจ็กต์เดียวกัน ต้องกำหนดเพิ่มเติมว่าจะรักษาความสัมพันธ์เหล่านั้นอย่างไร ตัวอย่างในบทความเป็นโครงสร้างรายงานอย่างง่ายที่ไม่มีวงจร

#กับดักของการคัดลอก List

List<Section> copy = new ArrayList<>(original);

บรรทัดนี้สร้าง List ใหม่ และนำสมาชิกจาก List เดิมมาใส่ แต่ไม่ได้สร้าง Section ใหม่ให้แต่ละสมาชิก การเปลี่ยนข้อความใน Section จึงอาจกระทบข้อมูลที่ทั้งสอง List ใช้ร่วมกัน [3]

การคัดลอก container กับการคัดลอกสมาชิกเป็นคนละเรื่อง หากสมาชิกเป็น immutable การใช้สมาชิกเดิมร่วมกันอาจตรงกับความต้องการอยู่แล้ว

#5. เวิร์กช็อป: คัดลอกรายงานด้วย Copy Constructor

#เป้าหมาย

เราจะสร้างรายงานต้นแบบ แล้วคัดลอกเพื่อเปลี่ยนชื่อรายงาน เปลี่ยนข้อความในส่วนแรก และเพิ่มส่วนใหม่ โดยตรวจสอบว่ารายงานต้นฉบับยังมีข้อมูลเดิม

ตัวอย่างใช้ copy() เป็นสัญญาของ Prototype และใช้ Copy Constructor เป็นกลไกภายใน จึงไม่ต้องพึ่ง Cloneable

#นโยบายการคัดลอก

ข้อมูล วิธีจัดการ เหตุผล
title: String ใช้ค่า reference เดิมได้ในตอนคัดลอก String เป็น immutable; การเปลี่ยนชื่อเป็นการกำหนด reference ใหม่
sections: List<Section> สร้าง List ใหม่ แต่ละรายงานต้องเพิ่มหรือลบสมาชิกได้อิสระ
แต่ละ Section สร้าง Section ใหม่ ข้อความภายในถูกเปลี่ยนได้ผ่านเมธอดของรายงาน
Section.text: String ใช้ค่า reference เดิมได้ในตอนคัดลอก ข้อความเป็น immutable

ตัว constructor ปกติจะคัดลอก Section ที่รับเข้ามาด้วย เพื่อให้รายงานเป็นเจ้าของข้อมูล mutable ของตัวเอง ไม่เก็บสมาชิกจากภายนอกไว้ให้แก้ไขย้อนกลับได้โดยไม่ตั้งใจ

#โค้ดสมบูรณ์: PrototypeDemo.java

ทุกคลาสอยู่ในไฟล์เดียวเพื่อให้ทดลองได้ทันที

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class PrototypeDemo {
    interface Prototype<T> {
        T copy();
    }

    static final class Section {
        private String text;

        Section(String text) {
            setText(text);
        }

        Section(Section source) {
            this(source.text);
        }

        void setText(String text) {
            this.text = Objects.requireNonNull(text);
        }

        @Override
        public String toString() {
            return text;
        }
    }

    static final class Report implements Prototype<Report> {
        private String title;
        private final List<Section> sections;

        Report(String title, List<Section> sections) {
            setTitle(title);
            this.sections = new ArrayList<>();
            for (Section section : Objects.requireNonNull(sections)) {
                this.sections.add(new Section(Objects.requireNonNull(section)));
            }
        }

        // Copy constructor: separates the list AND its mutable elements.
        private Report(Report source) {
            this(source.title, source.sections);
        }

        @Override
        public Report copy() {
            return new Report(this);
        }

        void setTitle(String title) {
            this.title = Objects.requireNonNull(title);
        }

        void updateSection(int index, String text) {
            sections.get(index).setText(text);
        }

        void addSection(String text) {
            sections.add(new Section(text));
        }

        @Override
        public String toString() {
            return title + " " + sections;
        }
    }

    static final class ReportRegistry {
        private final Map<String, Report> templates = new HashMap<>();

        void register(String key, Report template) {
            // Keep a snapshot so callers cannot change the stored template.
            templates.put(Objects.requireNonNull(key),
                    Objects.requireNonNull(template).copy());
        }

        Report create(String key) {
            Report template = templates.get(Objects.requireNonNull(key));
            if (template == null) {
                throw new IllegalArgumentException("Unknown template: " + key);
            }
            return template.copy();
        }
    }

    private static void check(boolean condition, String message) {
        if (!condition) {
            throw new AssertionError(message);
        }
    }

    public static void main(String[] args) {
        Report template = new Report("Template",
                List.of(new Section("Introduction"), new Section("Conclusion")));

        Prototype<Report> prototype = template;
        Report copy = prototype.copy();
        copy.setTitle("Java Report");
        copy.updateSection(0, "Prototype Pattern");
        copy.addSection("References");

        System.out.println("Original: " + template);
        System.out.println("Copy: " + copy);
        System.out.println("Different report: " + (template != copy));
        System.out.println("Different list: " + (template.sections != copy.sections));
        System.out.println("Different section: "
                + (template.sections.get(0) != copy.sections.get(0)));

        check(template != copy, "Report must be a new object");
        check(template.sections != copy.sections, "List must be independent");
        check(template.sections.get(0) != copy.sections.get(0),
                "Mutable sections must be independent");
        check(template.title.equals("Template"), "Original title changed");
        check(template.sections.get(0).text.equals("Introduction"),
                "Original section changed");
        check(template.sections.size() == 2, "Original list changed");

        ReportRegistry registry = new ReportRegistry();
        registry.register("basic", template);
        template.updateSection(0, "Changed after registration");
        Report first = registry.create("basic");
        first.updateSection(0, "First user only");
        Report second = registry.create("basic");

        System.out.println("Registry copy: " + second);
        check(second.sections.get(0).text.equals("Introduction"),
                "Registry template was modified");
        check(first.sections.get(0) != second.sections.get(0),
                "Registry copies share a mutable section");

        try {
            registry.create("missing");
            throw new AssertionError("Missing key must be rejected");
        } catch (IllegalArgumentException expected) {
            check(expected.getMessage().equals("Unknown template: missing"),
                    "Unexpected error message");
        }
        System.out.println("All checks passed.");
    }
}

#คอมไพล์และรัน

ติดตั้ง JDK 17 ขึ้นไป แล้วบันทึกโค้ดข้างต้นเป็น PrototypeDemo.java จากนั้นรันในไดเรกทอรีเดียวกับไฟล์

javac -encoding UTF-8 PrototypeDemo.java
java PrototypeDemo

ผลลัพธ์:

Original: Template [Introduction, Conclusion]
Copy: Java Report [Prototype Pattern, Conclusion, References]
Different report: true
Different list: true
Different section: true
Registry copy: Template [Introduction, Conclusion]
All checks passed.

#อ่านกลไกการทำงาน

  1. Prototype<Report> ประกาศความสามารถ copy() ทำให้ Client ขอสำเนาผ่านสัญญานี้ได้
  2. Report.copy() เรียก new Report(this) เพื่อส่งตัวเองเข้า Copy Constructor
  3. Copy Constructor ใช้ constructor หลักสร้าง List ใหม่และคัดลอกทุก Section
  4. การเปลี่ยนชื่อ เปลี่ยนข้อความ และเพิ่มส่วนใน copy จึงไม่แก้รายงานต้นฉบับ
  5. เมธอด check() ตรวจเงื่อนไขสำคัญ และโยน AssertionError หากพฤติกรรมผิด โดยไม่ต้องเปิดตัวเลือก -ea

ตัวอย่างประกาศ Report เป็น final เพื่อให้ขอบเขตการคัดลอกชัดเจน หากเปิดให้สืบทอด ต้องออกแบบให้แต่ละชนิดย่อยคัดลอกสถานะของตัวเองครบ และรักษาชนิดที่ต้องการเมื่อ Client เรียก copy()

การใช้ new ภายใน copy() เป็นเรื่องปกติของแนวทางนี้ สิ่งที่เปลี่ยนคือ Client ไม่ต้องรู้ขั้นตอนประกอบข้อมูลภายในซ้ำเอง

#6. Prototype Registry: เลือกต้นแบบแล้วขอสำเนา

ReportRegistry ในโปรแกรมมีสองจุดที่คัดลอกข้อมูล:

  • register() เก็บสำเนาของต้นแบบ เพื่อให้การแก้รายงานที่ส่งเข้ามาภายหลังไม่เปลี่ยนต้นแบบที่เก็บไว้
  • create() คืนสำเนาใหม่ทุกครั้ง เพื่อให้ผู้ใช้แต่ละคนแก้รายงานของตนได้อิสระ

โปรแกรมจงใจแก้ template หลังลงทะเบียน และแก้รายงาน first หลังรับสำเนา จากนั้นตรวจว่ารายงาน second ยังได้ข้อความ Introduction ที่ลงทะเบียนไว้เดิม

หากคืนต้นแบบที่อยู่ใน Map โดยตรง ผู้เรียกอาจแก้ข้อมูลต้นแบบ และทำให้รายงานที่สร้างในครั้งถัดไปผิดไปด้วย

Registry นี้เป็นตัวอย่างสำหรับการทำงานใน thread เดียว หากใช้ร่วมกันระหว่างหลาย thread ต้องออกแบบการเข้าถึงและการเผยแพร่สถานะให้เหมาะสม การเปลี่ยน Map อย่างเดียวไม่ได้ทำให้ต้นแบบ mutable ทั้งระบบปลอดภัยต่อการเข้าถึงพร้อมกันโดยอัตโนมัติ

#7. ทดลอง Shallow Copy ให้เห็นผลจริง

บันทึกเป็น ShallowCopyDemo.java ตัวอย่างนี้แยก List แล้ว แต่จงใจใช้สมาชิกเดิมร่วมกัน

import java.util.ArrayList;
import java.util.List;

public class ShallowCopyDemo {
    static final class Section {
        String text;

        Section(String text) {
            this.text = text;
        }
    }

    public static void main(String[] args) {
        List<Section> original = new ArrayList<>();
        original.add(new Section("Introduction"));

        // A new list, but the same Section object.
        List<Section> copy = new ArrayList<>(original);
        copy.get(0).text = "Changed through copy";
        copy.add(new Section("Copy only"));

        System.out.println("Different list: " + (original != copy));
        System.out.println("Same section: " + (original.get(0) == copy.get(0)));
        System.out.println("Original text: " + original.get(0).text);
        System.out.println("Original size: " + original.size());
        System.out.println("Copy size: " + copy.size());

        if (original == copy || original.get(0) != copy.get(0)
                || !original.get(0).text.equals("Changed through copy")
                || original.size() != 1 || copy.size() != 2) {
            throw new AssertionError("Unexpected shallow copy behavior");
        }
    }
}
javac -encoding UTF-8 ShallowCopyDemo.java
java ShallowCopyDemo

ผลลัพธ์:

Different list: true
Same section: true
Original text: Changed through copy
Original size: 1
Copy size: 2

การเพิ่มสมาชิกส่งผลเฉพาะ List ของสำเนา แต่การแก้ copy.get(0).text เปลี่ยน Section ตัวเดียวกับที่ต้นฉบับใช้อยู่ นี่คือเหตุผลที่การตรวจเพียง original != copy ยังไม่พอ

#8. อีกแนวทางใน Java: Object.clone() และ Cloneable

Java มี Object.clone() ซึ่งมีระดับการเข้าถึงเป็น protected และกลไกของ Object.clone() จะคัดลอกค่าของฟิลด์ โดยไม่คัดลอกออบเจ็กต์ที่ฟิลด์ reference อ้างถึงต่อให้โดยอัตโนมัติ จึงเป็น Shallow Copy [1]

Cloneable เป็น marker interface และไม่มีเมธอด clone() อยู่ในตัวมันเอง หากใช้กลไก Object.clone() กับออบเจ็กต์ที่ไม่รองรับ Cloneable จะเกิด CloneNotSupportedException [2]

ตัวอย่างต่อไปนี้มีเพียง String และ int จึงใช้ผลลัพธ์ของ Shallow Copy ได้โดยไม่ต้องแยกออบเจ็กต์ mutable ภายใน

#โค้ดสมบูรณ์: CloneableDemo.java

public class CloneableDemo {
    static final class Badge implements Cloneable {
        private final String label;
        private final int level;

        Badge(String label, int level) {
            this.label = label;
            this.level = level;
        }

        @Override
        public Badge clone() {
            try {
                return (Badge) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new AssertionError(e);
            }
        }
    }

    public static void main(String[] args) {
        Badge original = new Badge("Java", 1);
        Badge copy = original.clone();
        System.out.println("Different badge: " + (original != copy));
        System.out.println("Same label: " + (original.label == copy.label));
        System.out.println("Copied level: " + copy.level);
        if (original == copy || original.label != copy.label
                || original.level != copy.level) {
            throw new AssertionError("Unexpected clone behavior");
        }
    }
}
javac -encoding UTF-8 CloneableDemo.java
java CloneableDemo

ผลลัพธ์:

Different badge: true
Same label: true
Copied level: 1

Same label: true ไม่เป็นปัญหาในกรณีนี้ เพราะ String แก้ไขเนื้อหาเดิมไม่ได้ หากเพิ่มฟิลด์อย่าง List<Section> ต้องทบทวนการคัดลอกใหม่ ไม่สามารถถือว่า super.clone() จะแยกข้อมูลเหล่านั้นให้แล้ว

การจับ exception แล้วโยน AssertionError ในตัวอย่างอาศัยเงื่อนไขว่า Badge เป็น final และประกาศ implements Cloneable ไว้แล้ว จึงถือว่าข้อยกเว้นนี้เป็นเงื่อนไขที่ไม่ควรเกิดในคลาสตัวอย่าง

#เลือกกลไกคัดลอกอย่างไร

แนวทาง ลักษณะ เหมาะกับ
copy() + Copy Constructor เขียนกฎคัดลอกและ validation ได้ตรงไปตรงมา คลาสที่เราควบคุมและต้องกำหนดนโยบายการคัดลอกเอง
clone() + Cloneable ใช้กลไกคัดลอกฟิลด์ของ Object เป็นจุดเริ่มต้น API ที่ออกแบบให้รองรับ clone() อยู่แล้ว
เมธอดสร้างสำเนา เช่น from() ตั้งชื่อและกำหนดชนิดผลลัพธ์ได้ตามงาน งานแปลงข้อมูลหรือสำเนาที่เปลี่ยนรูปแบบ

บทความเลือก copy() สำหรับตัวอย่างหลัก เพราะมองเห็นกฎที่ต้องการแยก List และสมาชิกได้ชัดเจน ทั้งนี้ Prototype Pattern เป็นแนวคิดการออกแบบ ส่วน Cloneable เป็นกลไกเฉพาะของ Java การใช้แพตเทิร์นไม่จำเป็นต้องใช้กลไกนี้เสมอไป

#9. เทียบกับ Factory, Builder และ Singleton

รูปแบบ คำถามที่เน้น ตัวอย่าง
Prototype จะสร้างสำเนาจากสถานะที่มีอยู่ได้อย่างไร คัดลอกรายงานต้นแบบแล้วเปลี่ยนชื่อ
Factory Method จะให้ชนิดย่อยกำหนดออบเจ็กต์ที่สร้างได้อย่างไร คลาสย่อยเลือกชนิดเอกสารที่คืนจากเมธอดสร้าง
Builder จะประกอบออบเจ็กต์ทีละขั้นอย่างไร ตั้งหัวเรื่อง รูปแบบ และเนื้อหา แล้วเรียก build()
Singleton จะควบคุมให้มี instance เดียวในขอบเขตที่ออกแบบไว้อย่างไร เก็บบริการที่ตั้งใจให้มี instance ร่วมเพียงตัวเดียว

สามารถใช้แพตเทิร์นร่วมกันได้ เช่น ใช้ Builder เตรียมต้นแบบ แล้วเก็บใน Registry เพื่อให้ผู้ใช้สร้างสำเนาด้วย Prototype

#10. เหมาะกับงานใด และมีข้อจำกัดอะไร

#กรณีที่เหมาะ

  • มีต้นแบบที่กำหนดสถานะเริ่มต้นไว้แล้ว และต้องสร้างออบเจ็กต์ที่คล้ายกันหลายชุด
  • ผู้ใช้เลือกต้นแบบขณะโปรแกรมทำงาน เช่น รูปแบบรายงานหลายประเภท
  • ต้องการรวมกฎคัดลอกข้อมูลไว้ในคลาสที่เป็นเจ้าของสถานะ
  • มีรายละเอียดเริ่มต้นจำนวนมากที่ไม่ต้องการให้ Client ตั้งค่าซ้ำเอง

#ข้อจำกัด

  • ต้องดูแลเมธอดคัดลอกเมื่อเพิ่มหรือเปลี่ยนฟิลด์
  • โครงสร้างข้อมูลที่มีวงจรต้องมีวิธีติดตามออบเจ็กต์ที่คัดลอกแล้ว เพื่อไม่วนซ้ำไม่จบ
  • หากต้นฉบับอ้างถึงออบเจ็กต์เดียวกันหลายจุด ต้องกำหนดว่าจะรักษาความเป็นออบเจ็กต์ร่วมกันในสำเนาหรือแยกออก
  • ข้อมูล mutable ขนาดใหญ่ทำให้ Deep Copy ใช้ทั้งเวลาและหน่วยความจำเพิ่มขึ้น
  • สถานะที่เปลี่ยนพร้อมกันระหว่างคัดลอกอาจทำให้ได้ snapshot ไม่สอดคล้องกัน หากไม่มีการประสานการเข้าถึง

หากออบเจ็กต์เรียบง่าย สร้างใหม่ได้ชัดเจน และไม่มีสถานะต้นแบบที่ต้องนำกลับมาใช้ การเรียก constructor โดยตรงอาจเพียงพอ

#11. กำหนดนโยบายก่อนใช้กับระบบจริง

ประเภทข้อมูล สิ่งที่ควรตัดสินใจ
ID, UUID, เลขเอกสาร สำเนาต้องได้ตัวตนใหม่หรือเป็น snapshot ของตัวตนเดิม
วันสร้าง, สถานะอนุมัติ คงเดิม รีเซ็ต หรือสร้างค่าใหม่
List/Map และสมาชิก mutable จะแยกเฉพาะ container หรือแยกสมาชิกด้วย
ค่า immutable ใช้ร่วมกันได้หรือมีเหตุผลทางโดเมนให้สร้างใหม่
ไฟล์, socket, database connection คัดลอกเฉพาะข้อมูลอ้างอิง หรือจัดการทรัพยากรใหม่อย่างชัดเจน
แคชและค่าที่คำนวณได้ คัดลอกผลเดิม ล้างค่า หรือคำนวณใหม่

อย่าเพิ่มฟิลด์ใหม่แล้วลืมพิจารณา Copy Constructor โดยเฉพาะฟิลด์ที่เป็นออบเจ็กต์ mutable การคอมไพล์ผ่านไม่ได้รับประกันว่ากฎการคัดลอกยังครบถ้วน

final List<Section> ป้องกันการเปลี่ยน reference ของฟิลด์หลังสร้าง แต่ไม่ได้ทำให้ List หรือ Section ภายในเป็น immutable

#12. แบบฝึกปฏิบัติ

  1. เพิ่ม Style ที่มีสีและขนาดตัวอักษรซึ่งเปลี่ยนค่าได้ แล้วปรับ Report.copy() ให้สำเนามี Style ของตัวเอง
  2. เพิ่ม UUID id และกำหนดให้การคัดลอกได้ ID ใหม่ พร้อมตรวจว่าต้นฉบับยังใช้ ID เดิม
  3. ลงทะเบียนต้นแบบรายงานสองแบบ แล้วสร้างสำเนาจากแต่ละชื่อ
  4. ทดลองเปลี่ยนการคัดลอกสมาชิกเป็น new ArrayList<>(source.sections) แล้วสังเกตว่าเงื่อนไขใดใน check() ตรวจพบปัญหา
  5. เพิ่มเมธอดลบส่วนรายงาน และตรวจว่าลบจากสำเนาแล้วจำนวนส่วนของต้นฉบับยังเท่าเดิม

#13. ผลการตรวจสอบโค้ดประกอบบทความ

ตัวอย่างสมบูรณ์ทั้งสามโปรแกรมคอมไพล์ด้วย UTF-8 และ -Xlint:all บน OpenJDK 17.0.20 โดยไม่มีคำเตือน และรันตรวจพฤติกรรมดังนี้:

  • รายงาน สำเนา List และสมาชิก mutable เป็นคนละออบเจ็กต์ในกรณี Deep Copy
  • การเปลี่ยนชื่อ เปลี่ยนข้อความ และเพิ่มสมาชิกในสำเนาไม่เปลี่ยนต้นฉบับ
  • Registry แยกทั้งต้นแบบที่รับลงทะเบียนและสำเนาที่คืนให้ผู้เรียก
  • การขอต้นแบบที่ไม่มีชื่ออยู่ใน Registry เกิดข้อผิดพลาดตามที่กำหนด
  • ตัวอย่าง Shallow Copy แสดงการใช้สมาชิกตัวเดิมร่วมกัน แม้ List เป็นคนละตัว
  • ตัวอย่าง clone() สร้าง Badge ใหม่และใช้ข้อความ immutable เดิมร่วมกัน

การตรวจเหล่านี้ครอบคลุมโครงสร้างตัวอย่าง ยังไม่ได้วัดประสิทธิภาพหรือทดสอบการเข้าถึงหลาย thread

#เอกสารอ้างอิง

เอกสาร Java API ต่อไปนี้ใช้ตรวจสอบพฤติกรรมของกลไกและ collection ส่วนโค้ดเวิร์กช็อปเป็นตัวอย่างที่เขียนขึ้นประกอบบทความ

  1. Oracle Java SE 21 — Object.clone() — การคัดลอกฟิลด์และขอบเขตของ Shallow Copy
  2. Oracle Java SE 21 — Cloneable — marker interface และข้อกำหนดของ Object.clone()
  3. Oracle Java SE 21 — ArrayList — constructor ที่รับ Collection และ clone() ซึ่งไม่คัดลอกตัวสมาชิก

API ที่ใช้ในตัวอย่างมีอยู่ใน Java 17 แม้ลิงก์อ้างอิงจะเป็นชุดเอกสาร Java SE 21