- ภาพรวมทั้ง 15 เทคนิค
- 1. Self Encapsulate Field — ควบคุมการเข้าถึงภายในคลาส
- 2. Replace Data Value with Object — ย้ายข้อมูลและพฤติกรรมมาอยู่ด้วยกัน
- 3. Change Value to Reference — ใช้วัตถุร่วมสำหรับสิ่งเดียวกัน
- 4. Change Reference to Value — ให้ความเท่ากันขึ้นกับค่า
- 5. Replace Array with Object — ใช้ชื่อ field แทนตำแหน่ง
- 6. Duplicate Observed Data — แยกข้อมูลธุรกิจออกจาก GUI
- 7. Change Unidirectional Association to Bidirectional — เพิ่มทิศทางที่ต้องใช้
- 8. Change Bidirectional Association to Unidirectional — ตัดทิศทางที่ไม่ใช้
- 9. Replace Magic Number with Symbolic Constant — ตั้งชื่อให้ตัวเลขที่มีความหมาย
- 10. Encapsulate Field — ปิดการเข้าถึง field จากภายนอก
- 11. Encapsulate Collection — ให้เจ้าของควบคุมการเปลี่ยนสมาชิก
- 12. Replace Type Code with Class — แทนรหัสด้วยชนิดข้อมูล
- 13. Replace Type Code with Subclasses — แยกพฤติกรรมด้วยคลาสย่อย
- 14. Replace Type Code with State/Strategy — สลับพฤติกรรมผ่านวัตถุประกอบ
- 15. Replace Subclass with Fields — ยุบคลาสย่อยที่ต่างกันแค่ข้อมูล
- คู่เทคนิคที่ต้องตัดสินใจตามบริบท
- ขั้นตอนปรับโครงสร้างโดยรักษาพฤติกรรม
- แบบฝึกหัดสำหรับผู้เรียน
การจัดระเบียบข้อมูลไม่ได้หยุดอยู่ที่การเลือกชนิดตัวแปร แต่รวมถึงการตัดสินใจว่าใครเป็นเจ้าของข้อมูล ใครแก้ไขได้ ข้อมูลมีเอกลักษณ์ของตัวเองหรือเป็นเพียงค่า และพฤติกรรมที่เกี่ยวข้องควรอยู่ตรงไหน

บทความนี้อธิบายเทคนิค Organizing Data ตามรายการของ Refactoring.Guru ด้วยตัวอย่าง Java ที่เขียนขึ้นใหม่ พร้อมข้อพิจารณาในการเลือกใช้ เหมาะสำหรับการเรียน OOP และการปรับโครงสร้างโปรแกรม
Refactoring คือการเปลี่ยนโครงสร้างภายในโดยรักษาพฤติกรรมที่ผู้ใช้สังเกตได้ การเพิ่ม validation ใหม่ การเปลี่ยนกติกาธุรกิจ และการเปลี่ยนความหมายของ equality ควรแยกเป็นงานที่มีข้อกำหนดและการทดสอบชัดเจน
ตัวอย่างแต่ละหัวข้อเป็นอิสระจากกัน ใช้ Java 17 ขึ้นไป บางหัวข้อใช้
recordชื่อคลาสอาจซ้ำข้ามหัวข้อ จึงควรแยกไฟล์ในคนละ package หรือคนละโฟลเดอร์ ตัวอย่างแสดงโครงสร้างหลังปรับและอธิบายโครงสร้างเดิมในข้อความ ไม่ใช่แอปพลิเคชันเต็มรูปแบบ
#ภาพรวมทั้ง 15 เทคนิค
| ลำดับ | เทคนิค | สิ่งที่ต้องการแก้ |
|---|---|---|
| 1 | Self Encapsulate Field | การเข้าถึง private field ภายในคลาสต้องผ่านจุดควบคุม |
| 2 | Replace Data Value with Object | ข้อมูลพื้นฐานเริ่มมีกฎหรือพฤติกรรมของตัวเอง |
| 3 | Change Value to Reference | หลายสำเนาแทนสิ่งเดียวกันที่มี identity |
| 4 | Change Reference to Value | วัตถุเล็กที่ควรเปรียบเทียบด้วยค่า |
| 5 | Replace Array with Object | ใช้ตำแหน่งใน array แทนความหมายของข้อมูลต่างชนิด |
| 6 | Duplicate Observed Data | ข้อมูลธุรกิจผูกอยู่กับ GUI |
| 7 | Unidirectional → Bidirectional | ต้องเดินความสัมพันธ์กลับอีกทิศทาง |
| 8 | Bidirectional → Unidirectional | มีความสัมพันธ์ย้อนกลับที่ไม่จำเป็น |
| 9 | Replace Magic Number with Symbolic Constant | ตัวเลขมีความหมายแต่ไม่มีชื่อ |
| 10 | Encapsulate Field | ภายนอกเข้าถึง field โดยตรง |
| 11 | Encapsulate Collection | ภายนอกแก้ collection ข้ามกติกาของเจ้าของ |
| 12 | Replace Type Code with Class | ใช้ตัวเลขหรือสตริงแทนประเภทข้อมูล |
| 13 | Replace Type Code with Subclasses | ประเภทคงที่กำหนดพฤติกรรมที่แตกต่าง |
| 14 | Replace Type Code with State/Strategy | ต้องเปลี่ยนพฤติกรรมด้วย composition |
| 15 | Replace Subclass with Fields | subclass ต่างกันเพียงค่าคงที่ |
#1. Self Encapsulate Field — ควบคุมการเข้าถึงภายในคลาส
ปัญหา: คลาสมี private field อยู่แล้ว แต่เมธอดภายในอ่านและเขียน field โดยตรงหลายจุด เมื่อจะเปลี่ยนวิธีอ่านหรือเขียนข้อมูลต้องตามแก้ทุกจุด
แนวทาง: เพิ่ม accessor แล้วให้เมธอดภายในเรียกผ่าน accessor นั้น สร้าง setter เฉพาะเมื่อจำเป็น และไม่ต้องเปิดเป็น public เสมอไป
ตัวอย่าง: เดิม reserve() ลดค่า available โดยตรง หลังปรับให้ผ่าน setAvailable() และ getAvailable()
final class Inventory {
private int available;
Inventory(int initial) {
setAvailable(initial);
}
private int getAvailable() {
return available;
}
private void setAvailable(int value) {
available = value;
}
void reserve(int quantity) {
if (quantity <= 0 || quantity > getAvailable()) {
throw new IllegalArgumentException("Invalid quantity");
}
setAvailable(getAvailable() - quantity);
}
}
เลือกใช้เมื่อ: ต้องรวมจุดเข้าถึงข้อมูล เช่น การอ่านค่าแบบ lazy หรือการรักษากฎที่มีอยู่แล้ว หาก accessor เพียงเพิ่มชั้นการเรียกโดยไม่มีประโยชน์ การเข้าถึง private field โดยตรงอาจอ่านง่ายกว่า
ข้อควรระวัง: การเพิ่มกฎใหม่ใน setter อาจเปลี่ยนพฤติกรรมเดิม และควรหลีกเลี่ยงการเรียกเมธอดที่ subclass override ได้จาก constructor
อ้างอิง: Self Encapsulate Field
#2. Replace Data Value with Object — ย้ายข้อมูลและพฤติกรรมมาอยู่ด้วยกัน
ปัญหา: ค่าสตริงหรือตัวเลขเริ่มมีตรรกะเฉพาะ เช่น รหัสสินค้าใช้สร้างชื่อไฟล์ บาร์โค้ด และรูปแบบการแสดงผล แต่ตรรกะกระจายอยู่หลายคลาส
แนวทาง: สร้างชนิดข้อมูลที่มีความหมาย แล้วนำค่ากับพฤติกรรมที่เกี่ยวข้องมาไว้ด้วยกัน
ตัวอย่าง: เดิม Product เก็บ String sku และประกอบชื่อป้ายเอง หลังปรับให้ Sku ดูแลพฤติกรรมดังกล่าว
import java.util.Objects;
record Sku(String value) {
Sku {
Objects.requireNonNull(value);
}
String labelFileName() {
return value + "-label.pdf";
}
}
record Product(Sku sku, String name) {
String labelFileName() {
return sku.labelFileName();
}
}
ผล: เมธอดที่ต้องการ SKU สามารถรับ Sku โดยตรง ลดการส่งสตริงผิดประเภท และรวมกฎไว้ในที่เดียว
ข้อควรระวัง: อย่าห่อ primitive ทุกตัวโดยอัตโนมัติ ควรมีความหมายทางโดเมนหรือกฎที่คุ้มค่ากับชนิดข้อมูลใหม่ ตัวอย่างกำหนดว่า SKU ต้องไม่เป็น null ตั้งแต่ก่อนปรับโครงสร้าง
อ้างอิง: Replace Data Value with Object
#3. Change Value to Reference — ใช้วัตถุร่วมสำหรับสิ่งเดียวกัน
ปัญหา: แต่ละคำสั่งซื้อสร้าง Customer ของตัวเอง แม้เป็นลูกค้ารหัสเดียวกัน เมื่อชื่อเปลี่ยน ข้อมูลในแต่ละสำเนาอาจไม่ตรงกัน
แนวทาง: ใช้แหล่งค้นหาที่คืนวัตถุลูกค้าเดียวกันตาม identity ภายในขอบเขตที่กำหนด
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
final class Customer {
private final String id;
private String name;
Customer(String id, String name) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
}
String id() { return id; }
String name() { return name; }
void rename(String value) { name = Objects.requireNonNull(value); }
}
final class CustomerDirectory {
private final Map<String, Customer> customers = new HashMap<>();
void register(Customer customer) {
if (customers.putIfAbsent(customer.id(), customer) != null) {
throw new IllegalArgumentException("Duplicate customer id");
}
}
Customer get(String id) {
Customer customer = customers.get(id);
if (customer == null) throw new IllegalArgumentException("Unknown customer");
return customer;
}
}
record Purchase(Customer customer) {}
หลังลงทะเบียนลูกค้าหนึ่งครั้ง คำสั่งซื้อทุกใบใน directory เดียวกันเรียก get(id) แทนการ new Customer(...) ซ้ำ การเปลี่ยนชื่อจึงปรากฏผ่าน reference เดียวกัน
ข้อควรระวัง: เป้าหมายคือ identity ไม่ใช่การรวมทุกวัตถุที่มี field เท่ากัน ลูกค้าชื่อเหมือนกันอาจเป็นคนละคน ตัวอย่างเป็น in-memory และใช้เธรดเดียว ระบบจริงต้องกำหนดขอบเขต identity map การบันทึก และ concurrency เพิ่มเติม การเปลี่ยนจากสำเนาเป็น shared state ต้องตรวจสอบว่าตรงกับความหมายของระบบ
อ้างอิง: Change Value to Reference
#4. Change Reference to Value — ให้ความเท่ากันขึ้นกับค่า
ปัญหา: วัตถุขนาดเล็กถูกจัดการด้วย registry หรือ identity ทั้งที่ผู้ใช้สนใจเพียงค่าภายใน เช่น พิกัดบนตาราง
แนวทาง: ทำเป็น value object ที่ไม่เปลี่ยนแปลงค่า และเปรียบเทียบตามส่วนประกอบ
record GridPosition(int row, int column) {
GridPosition moveDown() {
return new GridPosition(row + 1, column);
}
}
new GridPosition(2, 3).equals(new GridPosition(2, 3)) ได้ true และ moveDown() สร้างค่าใหม่โดยไม่แก้ตำแหน่งเดิม record สร้าง equals() และ hashCode() จาก components ให้
ข้อควรระวัง: การใช้ reference ในภาษา Java ไม่ได้ทำให้วัตถุนั้นเป็น reference object ตามความหมายของโดเมน สิ่งที่เปลี่ยนคือความหมายของ identity, equality และการแก้ไขค่า ตรวจสอบการใช้ == และ key ใน collection ด้วย record ที่มี List หรือวัตถุ mutable ภายในไม่ได้ immutable แบบลึกโดยอัตโนมัติ
อ้างอิง: Change Reference to Value
#5. Replace Array with Object — ใช้ชื่อ field แทนตำแหน่ง
ปัญหา: ใช้ Object[] เก็บข้อมูลคนละความหมาย เช่น {"SE101", "OOP", 3} ผู้ใช้ต้องจำว่า index 0, 1 และ 2 หมายถึงอะไรและต้อง cast ให้ถูก
แนวทาง: แทนแต่ละตำแหน่งด้วย field ที่มีชื่อและชนิดชัดเจน
record Course(String code, String title, int credits) {
String displayName() {
return code + " - " + title;
}
}
ใช้ course.credits() แทน (Integer) row[2] ทำให้ compiler ช่วยตรวจชนิดและลดการสลับตำแหน่ง
เลือกใช้เมื่อ: array หนึ่งชุดทำหน้าที่เสมือน record ของข้อมูลหลายความหมาย Array ที่เก็บคะแนนจำนวนมากชนิดเดียวกันยังเหมาะสม ไม่ต้องเปลี่ยนทุก array เป็น object
อ้างอิง: Replace Array with Object
#6. Duplicate Observed Data — แยกข้อมูลธุรกิจออกจาก GUI
ปัญหา: หน้าจอเก็บทั้งข้อมูลและกฎธุรกิจ ทำให้ใช้ข้อมูลเดียวกันกับหลายหน้าจอหรือทดสอบโดยไม่เปิด GUI ได้ยาก
แนวทาง: ให้ domain model เป็นเจ้าของข้อมูลจริง และให้ GUI เก็บค่าแสดงผลที่อัปเดตตาม model ผ่าน notification
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntConsumer;
final class ScoreModel {
private int score;
private final List<IntConsumer> observers = new ArrayList<>();
int score() { return score; }
void subscribe(IntConsumer observer) { observers.add(observer); }
void unsubscribe(IntConsumer observer) { observers.remove(observer); }
void setScore(int value) {
score = value;
for (IntConsumer observer : List.copyOf(observers)) {
observer.accept(value);
}
}
}
final class ScoreView implements AutoCloseable {
private final ScoreModel model;
private String displayedText;
private final IntConsumer listener = this::render;
ScoreView(ScoreModel model) {
this.model = model;
model.subscribe(listener);
render(model.score());
}
void userEntered(int value) { model.setScore(value); }
private void render(int value) { displayedText = Integer.toString(value); }
String displayedText() { return displayedText; }
public void close() { model.unsubscribe(listener); }
}
ทิศทาง: ผู้ใช้แก้หน้าจอ → model เปลี่ยน → แจ้งผู้สังเกต → หน้าจอแสดงค่าใหม่ render() ไม่เรียก setter ของ model จึงไม่เกิดวงจรแจ้งเตือนกลับไปมา
ชื่อเทคนิคไม่ได้ชวนให้ทำสำเนาข้อมูลธุรกิจที่แก้ไขได้อย่างอิสระ สถานะบนหน้าจอเป็นภาพสะท้อนของ model ตัวอย่างใช้ callback และจำลอง view เพื่อให้เห็นหลักการ โดยสมมติทำงานบน UI thread เดียวกัน GUI จริงต้องจัดการเธรดและยกเลิก listener เมื่อปิดหน้าจอ ส่วนเว็บที่ทำงานเป็น request/response ต้องเลือกกลไกซิงก์ตามสถาปัตยกรรม
อ้างอิง: Duplicate Observed Data
#7. Change Unidirectional Association to Bidirectional — เพิ่มทิศทางที่ต้องใช้
ปัญหา: Student รู้ว่าตนอยู่ Classroom ใด แต่ห้องเรียนต้องแสดงนักเรียนทั้งหมดและยังไม่มีทางเข้าถึง
แนวทาง: เพิ่มความสัมพันธ์ย้อนกลับ และกำหนดเมธอดเดียวที่รับผิดชอบทำให้สองด้านสอดคล้องกัน
import java.util.HashSet;
import java.util.Set;
final class Classroom {
private final Set<Student> students = new HashSet<>();
Set<Student> students() { return Set.copyOf(students); }
void attachInternal(Student student) { students.add(student); }
void detachInternal(Student student) { students.remove(student); }
}
final class Student {
private Classroom classroom;
Classroom classroom() { return classroom; }
void moveTo(Classroom next) {
if (classroom == next) return;
if (classroom != null) classroom.detachInternal(this);
classroom = next;
if (next != null) next.attachInternal(this);
}
}
moveTo(null) ถอนนักเรียนออกจากห้องเดิม และย้ายห้องด้วย moveTo(next) ซึ่งลบความสัมพันธ์เดิมก่อนเพิ่มใหม่
ข้อควรระวัง: เมธอดลงท้าย Internal ในตัวอย่างเป็น package-private เพื่อให้เห็นการประสานงาน ควรจำกัดไว้ใน package ของโดเมนและให้ผู้ใช้เรียก moveTo() เท่านั้น ระบบที่มีหลายเธรดหรือฐานข้อมูลต้องทำให้การเปลี่ยนทั้งสองด้านเป็นหนึ่งหน่วยงานที่สอดคล้องกัน
อ้างอิง: Change Unidirectional Association to Bidirectional
#8. Change Bidirectional Association to Unidirectional — ตัดทิศทางที่ไม่ใช้
ปัญหา: Order ถือรายการ OrderLine และแต่ละรายการถือ Order กลับ ทั้งที่ไม่มีกรณีใช้งานให้รายการเข้าถึง order
แนวทาง: ลบ reference ย้อนกลับและโค้ดซิงก์ที่ไม่จำเป็น
import java.util.ArrayList;
import java.util.List;
record OrderLine(String sku, int quantity) {}
final class Order {
private final List<OrderLine> lines = new ArrayList<>();
void addLine(OrderLine line) { lines.add(line); }
List<OrderLine> lines() { return List.copyOf(lines); }
}
เหลือ Order → OrderLine จึงเข้าใจความเป็นเจ้าของง่ายขึ้นและลด coupling
ข้อควรระวัง: ตรวจสอบผู้เรียก การ serialize และการ mapping persistence ก่อนลบ ความสัมพันธ์แบบสองทิศทางไม่ผิดในตัวเอง ควรมีเมื่อรองรับกรณีใช้งานจริง
อ้างอิง: Change Bidirectional Association to Unidirectional
#9. Replace Magic Number with Symbolic Constant — ตั้งชื่อให้ตัวเลขที่มีความหมาย
ปัญหา: ตัวเลขอย่าง 86400 ปรากฏในสูตร โดยผู้อ่านต้องเดาความหมายและหน่วย
แนวทาง: ตั้งชื่อค่าคงที่ตามความหมาย
final class TimeConversion {
private static final long SECONDS_PER_DAY = 86_400L;
static long toSeconds(long days) {
return days * SECONDS_PER_DAY;
}
}
เดิมเป็น days * 86400 หลังปรับสูตรยังเหมือนเดิม แต่ชื่อระบุความสัมพันธ์ของหน่วยชัดเจน
ข้อควรระวัง: ไม่ต้องสร้าง constant ให้ทุก 0 หรือ 1 และไม่ควรรวมตัวเลขที่เท่ากันแต่คนละความหมายเป็น constant เดียว ตัวอย่างหมายถึงช่วงเวลา 24 ชั่วโมง การคำนวณวันตามปฏิทินและ timezone ต้องใช้แบบจำลองเวลาที่เหมาะสม
อ้างอิง: Replace Magic Number with Symbolic Constant
#10. Encapsulate Field — ปิดการเข้าถึง field จากภายนอก
ปัญหา: public String displayName ทำให้ทุกส่วนของโปรแกรมอ่านและเขียนตรงเข้าสู่ representation ของคลาส
แนวทาง: เปลี่ยน field เป็น private และให้เข้าถึงผ่านเมธอดที่จำเป็น
final class UserProfile {
private String displayName;
UserProfile(String displayName) {
this.displayName = displayName;
}
String displayName() { return displayName; }
void rename(String value) { displayName = value; }
}
ผู้ใช้ภายใน package เรียก profile.rename("Naruapon") แทนการกำหนด field โดยตรง หากเป็น API ข้าม package จึงค่อยเปิดคลาสและเมธอดที่ต้องใช้เป็น public
ความต่างจากข้อ 1: Encapsulate Field เปลี่ยนขอบเขตการเข้าถึงจากภายนอก ส่วน Self Encapsulate Field ทำให้เมธอดภายในคลาสใช้ accessor ด้วย ไม่จำเป็นต้องสร้าง setter สำหรับทุก field
อ้างอิง: Encapsulate Field
#11. Encapsulate Collection — ให้เจ้าของควบคุมการเปลี่ยนสมาชิก
ปัญหา: getter คืน mutable list ตัวจริง ผู้เรียกจึงเพิ่ม ลบ หรือ clear ได้โดยข้ามเมธอดของเจ้าของ
แนวทาง: คืนรายการที่ผู้เรียกแก้ไขไม่ได้ และเปิดเมธอดเฉพาะสำหรับการเพิ่มหรือลบ
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
record Book(String isbn, String title) {}
final class ReadingList {
private final List<Book> books = new ArrayList<>();
List<Book> books() { return List.copyOf(books); }
void addBook(Book book) { books.add(Objects.requireNonNull(book)); }
boolean removeBook(Book book) { return books.remove(book); }
}
List.copyOf() คืน snapshot ที่แก้ไขโครงสร้างไม่ได้ เมื่อเจ้าของเพิ่มสมาชิกภายหลัง snapshot เดิมจะไม่เพิ่มตาม ต่างจาก unmodifiable view ที่อาจสะท้อนการเปลี่ยน collection ต้นทาง
ข้อควรระวัง: read-only collection ไม่ได้ทำให้สมาชิกข้างใน immutable โดยอัตโนมัติ ตัวอย่าง Book มี components เป็นสตริงจึงไม่มี mutable member และกำหนดว่า null เป็นค่าที่ไม่รองรับ การเปลี่ยน getter จาก mutable list เป็น snapshot ต้องย้ายผู้เรียกไปใช้เมธอดของเจ้าของและตรวจสอบพฤติกรรมการถือรายการเก่าไว้ด้วย
อ้างอิง: Encapsulate Collection
#12. Replace Type Code with Class — แทนรหัสด้วยชนิดข้อมูล
ปัญหา: ฟิลด์ int categoryCode มีชุดค่าที่อนุญาต แต่ compiler ยังยอมให้ส่งตัวเลขใดก็ได้ ตัวรหัสมีหน้าที่ระบุหมวดหมู่ ไม่ได้เลือกอัลกอริทึมธุรกิจ
แนวทาง: สร้างคลาสสำหรับประเภทนั้นและควบคุมชุดค่าที่สร้างได้
import java.util.Objects;
final class CatalogCategory {
static final CatalogCategory BOOK = new CatalogCategory("BOOK");
static final CatalogCategory COURSE = new CatalogCategory("COURSE");
private final String code;
private CatalogCategory(String code) { this.code = code; }
String code() { return code; }
}
record CatalogItem(String name, CatalogCategory category) {
CatalogItem {
Objects.requireNonNull(category);
}
}
เมธอดที่รับ CatalogCategory จะไม่รับ 123 หรือสตริงผิดหมวดโดยตรง ตัวอย่างนี้ใช้คลาสเพื่อแสดงเทคนิคดั้งเดิม ใน Java ชุดค่าปิดที่เรียบง่ายมักเขียนด้วย enum ได้กระชับกว่า
ข้อควรระวัง: ถ้ารหัสทำให้มี switch เลือกพฤติกรรมซ้ำหลายจุด การห่อรหัสอย่างเดียวอาจยังไม่แก้ปัญหา ควรพิจารณาข้อ 13 หรือ 14 และยังอาจต้องแปลงรหัสตรงขอบเขตฐานข้อมูลหรือ API
อ้างอิง: Replace Type Code with Class
#13. Replace Type Code with Subclasses — แยกพฤติกรรมด้วยคลาสย่อย
ปัญหา: คลาสใช้รหัสชนิดรูปทรง เช่น CIRCLE และ RECTANGLE เพื่อเลือกสูตรพื้นที่ และประเภทไม่เปลี่ยนตลอดอายุวัตถุ
แนวทาง: ให้แต่ละ subclass รับผิดชอบสูตรของตน แล้วเรียกผ่าน polymorphism
abstract class Shape {
abstract double area();
}
final class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
double area() { return Math.PI * radius * radius; }
}
final class Rectangle extends Shape {
private final double width;
private final double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
double area() { return width * height; }
}
ผู้เรียกใช้ shape.area() โดยไม่ต้องรู้ชนิดจริง การสร้างวัตถุจากรหัสภายนอกอาจยังมี factory ที่เลือกชนิดเพียงจุดเดียว
เลือกใช้เมื่อ: ความแตกต่างเป็นพฤติกรรมของชนิดที่คงที่และเหมาะกับ inheritance หากประเภทต้องเปลี่ยนระหว่างรัน หรือมีหลายมิติที่ทำให้ subclass เพิ่มแบบทวีคูณ ให้พิจารณา composition
อ้างอิง: Replace Type Code with Subclasses
#14. Replace Type Code with State/Strategy — สลับพฤติกรรมผ่านวัตถุประกอบ
ปัญหา: คลาสเลือกพฤติกรรมด้วย type code แต่เปลี่ยนประเภทระหว่างใช้งานได้ หรือ inheritance ของคลาสนั้นไม่เหมาะกับการแยกเพิ่ม
แนวทาง: แยกพฤติกรรมเป็นวัตถุและให้ context ถือ reference ไปยังพฤติกรรมนั้น
import java.util.Objects;
interface ShippingStrategy {
long feeSatang();
}
final class StandardShipping implements ShippingStrategy {
public long feeSatang() { return 4_000L; }
}
final class ExpressShipping implements ShippingStrategy {
public long feeSatang() { return 8_000L; }
}
final class Checkout {
private ShippingStrategy shipping;
Checkout(ShippingStrategy shipping) { changeShipping(shipping); }
void changeShipping(ShippingStrategy next) {
shipping = Objects.requireNonNull(next);
}
long shippingFeeSatang() { return shipping.feeSatang(); }
}
เปลี่ยนจากมาตรฐานเป็นด่วนด้วย checkout.changeShipping(new ExpressShipping()) โดยยังเป็น checkout ตัวเดิม ตัวอย่างนี้เป็น Strategy สำหรับเลือกวิธีจัดส่ง ส่วน State เหมาะกับสถานะ เช่น Draft, Paid และ Shipped ที่กำหนดทั้งการกระทำที่อนุญาตและการเปลี่ยนสถานะ
ข้อควรระวัง: การใช้ State ต้องกำหนด transition ที่ถูกต้อง ไม่ควรเปิดให้ผู้เรียกเปลี่ยนไปสถานะใดก็ได้โดยข้ามกฎ สำหรับกติกาง่ายที่ต่างกันเพียงค่าคงที่ การใช้ field อาจพอและไม่จำเป็นต้องสร้างหลาย strategy class
อ้างอิง: Replace Type Code with State/Strategy
#15. Replace Subclass with Fields — ยุบคลาสย่อยที่ต่างกันแค่ข้อมูล
ปัญหา: BasicPlan กับ PremiumPlan มีโครงสร้างเหมือนกัน ทุกเมธอดต่างกันเพียงชื่อแพ็กเกจและค่าบริการที่คืนกลับ ไม่มีอัลกอริทึมหรือ invariant เฉพาะชนิด
แนวทาง: เก็บความแตกต่างใน field ของคลาสเดียว แล้วลบ subclass ที่ไม่จำเป็น
record SubscriptionPlan(String name, long monthlyFeeSatang) {
static SubscriptionPlan basic() {
return new SubscriptionPlan("Basic", 9_900L);
}
static SubscriptionPlan premium() {
return new SubscriptionPlan("Premium", 19_900L);
}
}
ใช้ SubscriptionPlan.basic().monthlyFeeSatang() แทนการสร้าง subclass เพื่อคืนตัวเลขเดียว โรงงานสร้างวัตถุในตัวอย่างเป็นจุดรวมค่าของแพ็กเกจ
ข้อควรระวัง: ถ้าคลาสย่อยมีขั้นตอนประมวลผลหรือข้อจำกัดต่างกันจริง การยุบอาจนำไปสู่เงื่อนไขที่ซับซ้อนกว่าเดิม ตรวจสอบด้วยว่าผู้เรียกอาศัยชนิด subclass ใน signature หรือ instanceof อยู่หรือไม่
อ้างอิง: Replace Subclass with Fields
#คู่เทคนิคที่ต้องตัดสินใจตามบริบท
| ทางเลือก | คำถามที่ควรถาม | แนวทางเลือก |
|---|---|---|
| Self Encapsulate / Encapsulate Field | ต้องควบคุมการใช้จากภายในหรือภายนอก? | ภายในใช้ accessor → Self; ปิด public field → Encapsulate |
| Value → Reference / Reference → Value | สิ่งนี้ต้องมี identity ร่วม หรือเพียงค่าเท่ากัน? | ลูกค้าที่ระบุด้วยรหัส → Reference; พิกัด → Value |
| Uni → Bi / Bi → Uni | มีกรณีใช้งานเดินความสัมพันธ์ย้อนกลับจริงหรือไม่? | เพิ่มเมื่อจำเป็น ลบเมื่อไม่มีผู้ใช้ |
| Type Code → Class / Subclasses / Strategy | เป็นข้อมูล ประเภทคงที่ หรือพฤติกรรมที่สลับได้? | Class หรือ enum / Subclasses / Strategy |
| Subclasses / Fields | ความแตกต่างเป็นอัลกอริทึมหรือค่าคงที่? | อัลกอริทึม → พิจารณา polymorphism; ค่าคงที่ → Fields |
ไม่มีทิศทางใดที่ดีกว่าเสมอ การเพิ่ม abstraction มีต้นทุน จึงควรเริ่มจากปัญหาที่เกิดขึ้นจริง เช่น จุดแก้ไขซ้ำ กฎข้อมูลรั่วไหล หรือความสัมพันธ์ที่รักษาให้ตรงกันยาก
#ขั้นตอนปรับโครงสร้างโดยรักษาพฤติกรรม
- ระบุผู้เรียกและเขียน characterization test สำหรับพฤติกรรมปัจจุบัน โดยเฉพาะกรณีขอบและข้อผิดพลาดที่ผู้ใช้สังเกตได้
- เลือกเทคนิคที่แก้ปัญหานั้นโดยตรง และปรับทีละจุด เช่น เพิ่ม accessor ก่อนย้าย field
- ย้ายผู้เรียกไปยัง API ใหม่ แล้วตรวจสอบว่าไม่มีทางลัดที่ยังแก้ field หรือ collection เดิม
- ตรวจสอบความหมายของ equality, identity, null, หน่วยข้อมูล และการถือ reference ร่วมกัน
- รันการทดสอบที่เกี่ยวข้อง ก่อนลบโค้ดหรือโครงสร้างเดิมที่ไม่ใช้แล้ว
สำหรับระบบที่เชื่อมฐานข้อมูล การเปลี่ยน object model อาจต้องมี migration และการปรับ serialization เพิ่มด้วย ไม่ควรถือว่าการเปลี่ยนคลาส Java จะปรับสัญญาของ API หรือข้อมูลเก่าตามไปเอง
#แบบฝึกหัดสำหรับผู้เรียน
- เปลี่ยน
Object[]ที่เก็บรหัสวิชา ชื่อวิชา และหน่วยกิตให้เป็นชนิดข้อมูลที่มีชื่อ แล้วอธิบายข้อผิดพลาดที่ compiler ช่วยตรวจได้ - ออกแบบ
ReadingListให้เพิ่มและลบหนังสือได้โดยผู้เรียกแก้รายการที่ getter คืนมาไม่ได้ พร้อมอธิบาย snapshot และ view - เลือกแบบจำลองสำหรับ
Customer,GridPositionและSubscriptionPlanว่าควรอาศัย identity หรือ value พร้อมเหตุผล - เพิ่ม view อีกหนึ่งตัวให้
ScoreModelและตรวจสอบว่าทั้งสอง view แสดงค่าใหม่เมื่อ model เปลี่ยน - เปรียบเทียบการใช้ field, subclass และ strategy สำหรับค่าจัดส่ง แล้วระบุเงื่อนไขที่ทำให้แต่ละแบบเหมาะสม