- สิ่งที่จะได้เรียนรู้
- 1. ปัญหาที่ Prototype ช่วยแก้
- 2. องค์ประกอบของแพตเทิร์น
- 3. การอ้างอิงร่วมกันไม่ใช่การคัดลอก
- 4. Shallow Copy และ Deep Copy
- 5. เวิร์กช็อป: คัดลอกรายงานด้วย Copy Constructor
- 6. Prototype Registry: เลือกต้นแบบแล้วขอสำเนา
- 7. ทดลอง Shallow Copy ให้เห็นผลจริง
- 8. อีกแนวทางใน Java: Object.clone() และ Cloneable
- 9. เทียบกับ Factory, Builder และ Singleton
- 10. เหมาะกับงานใด และมีข้อจำกัดอะไร
- 11. กำหนดนโยบายก่อนใช้กับระบบจริง
- 12. แบบฝึกปฏิบัติ
- 13. ผลการตรวจสอบโค้ดประกอบบทความ
- เอกสารอ้างอิง
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.
#อ่านกลไกการทำงาน
Prototype<Report>ประกาศความสามารถcopy()ทำให้ Client ขอสำเนาผ่านสัญญานี้ได้Report.copy()เรียกnew Report(this)เพื่อส่งตัวเองเข้า Copy Constructor- Copy Constructor ใช้ constructor หลักสร้าง List ใหม่และคัดลอกทุก
Section - การเปลี่ยนชื่อ เปลี่ยนข้อความ และเพิ่มส่วนใน
copyจึงไม่แก้รายงานต้นฉบับ - เมธอด
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. แบบฝึกปฏิบัติ
- เพิ่ม
Styleที่มีสีและขนาดตัวอักษรซึ่งเปลี่ยนค่าได้ แล้วปรับReport.copy()ให้สำเนามี Style ของตัวเอง - เพิ่ม
UUID idและกำหนดให้การคัดลอกได้ ID ใหม่ พร้อมตรวจว่าต้นฉบับยังใช้ ID เดิม - ลงทะเบียนต้นแบบรายงานสองแบบ แล้วสร้างสำเนาจากแต่ละชื่อ
- ทดลองเปลี่ยนการคัดลอกสมาชิกเป็น
new ArrayList<>(source.sections)แล้วสังเกตว่าเงื่อนไขใดในcheck()ตรวจพบปัญหา - เพิ่มเมธอดลบส่วนรายงาน และตรวจว่าลบจากสำเนาแล้วจำนวนส่วนของต้นฉบับยังเท่าเดิม
#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 ส่วนโค้ดเวิร์กช็อปเป็นตัวอย่างที่เขียนขึ้นประกอบบทความ
- Oracle Java SE 21 — Object.clone() — การคัดลอกฟิลด์และขอบเขตของ Shallow Copy
- Oracle Java SE 21 — Cloneable — marker interface และข้อกำหนดของ
Object.clone() - Oracle Java SE 21 — ArrayList — constructor ที่รับ Collection และ
clone()ซึ่งไม่คัดลอกตัวสมาชิก
API ที่ใช้ในตัวอย่างมีอยู่ใน Java 17 แม้ลิงก์อ้างอิงจะเป็นชุดเอกสาร Java SE 21