- แนวคิดพื้นฐาน
- ภาพรวมทั้ง 8 เทคนิค
- 1. Move Method — ย้ายเมธอดไปยังเจ้าของพฤติกรรม
- 2. Move Field — ย้ายข้อมูลไปยังเจ้าของที่เหมาะสม
- 3. Extract Class — แยกคลาสที่ทำหลายหน้าที่
- 4. Inline Class — รวมคลาสที่ไม่มีบทบาทเพียงพอ
- 5. Hide Delegate — ซ่อนโครงสร้างการส่งต่องาน
- 6. Remove Middle Man — ลดตัวกลางที่ส่งต่อมากเกินไป
- 7. Introduce Foreign Method — เติมเมธอดเมื่อแก้คลาสต้นทางไม่ได้
- 8. Introduce Local Extension — รวมความสามารถเพิ่มเติมเป็นชนิดใหม่
- เลือกเทคนิคที่มีทิศทางตรงข้ามอย่างไร
- ความสัมพันธ์กับ Code Smells
- ขั้นตอนทำ Refactoring ในโครงการจริง
- กรณีตรวจสอบพฤติกรรมสำหรับตัวอย่าง
- แบบฝึกหัด
- ข้อคิดสำหรับการออกแบบ
- แหล่งอ้างอิง
จัดข้อมูลและพฤติกรรมให้อยู่กับออบเจ็กต์ที่มีความรับผิดชอบเหมาะสม ผ่าน 8 เทคนิค พร้อมตัวอย่างก่อน–หลัง

#แนวคิดพื้นฐาน
เมื่อระบบเติบโต ปัญหามักไม่ได้อยู่ที่อัลกอริทึมเพียงอย่างเดียว แต่อยู่ที่ ข้อมูลและเมธอดอยู่ผิดคลาส เช่น Order คำนวณค่าขนส่งจากข้อมูลของ Shipment แทบทั้งหมด หรือคลาสพนักงานดูแลทั้งข้อมูลบุคคลและรูปแบบเบอร์โทรศัพท์
Moving Features between Objects เป็นกลุ่มเทคนิคที่ปรับตำแหน่งของสมาชิกคลาสและขอบเขตความรับผิดชอบ โดยรักษาพฤติกรรมที่ผู้ใช้ระบบสังเกตได้ไว้ เป้าหมายคือทำให้สิ่งที่เกี่ยวข้องกันอยู่ด้วยกัน และลดการพึ่งพารายละเอียดภายในของคลาสอื่น [1]
คำศัพท์สำคัญ:
- Feature: ในบริบทนี้หมายถึงสมาชิกหรือความสามารถ เช่น field และ method
- Cohesion: ความสอดคล้องของข้อมูลและพฤติกรรมภายในคลาส
- Coupling: ระดับการพึ่งพาระหว่างส่วนต่าง ๆ ของระบบ
- Delegation: ออบเจ็กต์หนึ่งรับคำขอแล้วส่งงานให้กับอีกออบเจ็กต์
- Client: โค้ดที่เรียกใช้ออบเจ็กต์ ไม่จำเป็นต้องเป็น frontend
อย่าตัดสินจากจำนวนครั้งที่เมธอดถูกเรียกเพียงอย่างเดียว ให้พิจารณาว่าเมธอดใช้ข้อมูลของใคร กฎนั้นเป็นความรับผิดชอบของใคร และเมื่อข้อกำหนดเปลี่ยนควรแก้คลาสใด
#ภาพรวมทั้ง 8 เทคนิค
| เทคนิค | สัญญาณที่ควรพิจารณา | การปรับโครงสร้าง |
|---|---|---|
| Move Method | เมธอดพึ่งข้อมูลของคลาสอื่นเป็นหลัก | ย้ายพฤติกรรมไปยังคลาสที่เหมาะสม |
| Move Field | ข้อมูลอยู่ห่างจากผู้รับผิดชอบใช้ข้อมูล | ย้าย field และปรับจุดอ้างอิง |
| Extract Class | คลาสเดียวรับผิดชอบหลายเรื่อง | แยกข้อมูลและพฤติกรรมชุดหนึ่งเป็นคลาสใหม่ |
| Inline Class | คลาสมีบทบาทน้อยจนไม่คุ้มกับความซับซ้อน | รวมสมาชิกเข้าอีกคลาสแล้วลบคลาสเดิม |
| Hide Delegate | Client ต้องรู้โครงสร้างออบเจ็กต์ภายใน | เพิ่มเมธอดที่ซ่อนการส่งต่องาน |
| Remove Middle Man | คลาสมีเมธอดส่งต่องานจำนวนมากโดยไม่เพิ่มคุณค่า | ให้ client เข้าถึง delegate โดยตรงในจุดที่เหมาะสม |
| Introduce Foreign Method | ต้องเพิ่มความสามารถเล็กน้อย แต่แก้คลาสต้นทางไม่ได้ | เพิ่ม helper ใน client โดยรับออบเจ็กต์ต้นทางเป็นพารามิเตอร์ |
| Introduce Local Extension | ต้องเพิ่มหลายความสามารถให้คลาสที่แก้ไม่ได้ | สร้าง wrapper หรือ subclass เพื่อรวมความสามารถ |
ตัวอย่าง Java ต่อไปนี้เขียนขึ้นใหม่เพื่อการสอน แต่ละหัวข้อเป็นตัวอย่างอิสระ และโค้ดก่อน–หลังเป็นคนละเวอร์ชัน ห้ามวางคลาสชื่อซ้ำไว้ใน package เดียวกันพร้อมกัน ตัวอย่างสมมติว่าอาร์กิวเมนต์ที่จำเป็นไม่เป็น
nullและข้อมูลผ่านการตรวจสอบแล้ว
#1. Move Method — ย้ายเมธอดไปยังเจ้าของพฤติกรรม
ปัญหา: เมธอดในคลาสหนึ่งใช้ข้อมูลของอีกคลาสเป็นส่วนใหญ่ จนความรับผิดชอบกระจายผิดที่
วิธีแก้: สร้างเมธอดในคลาสปลายทาง ย้าย logic แล้วปรับผู้เรียก เมธอดเดิมอาจคงไว้เป็นตัวส่งต่องานระหว่างเปลี่ยนผ่าน [2]
#ก่อนปรับ
Order รู้กฎค่าขนส่ง ทั้งที่ข้อมูลที่ใช้ทั้งหมดอยู่ใน Shipment ตัวเลขค่าขนส่งในตัวอย่างมีหน่วยเป็นสตางค์
class Shipment {
private final int weightGrams;
private final boolean express;
Shipment(int weightGrams, boolean express) {
this.weightGrams = weightGrams;
this.express = express;
}
int getWeightGrams() { return weightGrams; }
boolean isExpress() { return express; }
}
class Order {
private final Shipment shipment;
Order(Shipment shipment) {
this.shipment = shipment;
}
long shippingFeeSatang() {
long base = shipment.getWeightGrams() > 5_000 ? 20_000 : 10_000;
return base + (shipment.isExpress() ? 5_000 : 0);
}
}
#หลังปรับ
class Shipment {
private final int weightGrams;
private final boolean express;
Shipment(int weightGrams, boolean express) {
this.weightGrams = weightGrams;
this.express = express;
}
long shippingFeeSatang() {
long base = weightGrams > 5_000 ? 20_000 : 10_000;
return base + (express ? 5_000 : 0);
}
}
class Order {
private final Shipment shipment;
Order(Shipment shipment) {
this.shipment = shipment;
}
long shippingFeeSatang() {
return shipment.shippingFeeSatang();
}
}
ผลที่ได้: กฎขนส่งอยู่ใกล้ข้อมูลขนส่ง และผู้เรียก Order.shippingFeeSatang() ยังใช้รูปแบบเดิมได้ เช่น พัสดุ 6,000 กรัมแบบด่วนยังได้ 25,000 สตางค์
ข้อควรระวัง: หากเมธอดอ้างสมาชิกของคลาสเดิมจำนวนมาก อาจต้องแยกบางส่วนก่อนย้าย ตรวจการ override, interface และ visibility ด้วย ไม่ควรเปิด field เป็น public เพื่อให้ย้ายโค้ดได้ง่ายขึ้น
#2. Move Field — ย้ายข้อมูลไปยังเจ้าของที่เหมาะสม
ปัญหา: Field อยู่ในคลาสหนึ่ง แต่กฎที่ใช้ข้อมูลนั้นอยู่ในอีกคลาส
วิธีแก้: สร้าง field ที่ปลายทาง เปลี่ยนจุดอ่านและเขียน แล้วลบ field เดิมเมื่อเปลี่ยนครบ [3]
#ก่อนปรับ
สมมติว่าระบบนี้หนึ่งคำสั่งซื้อมีหนึ่งการจัดส่ง และวันจัดส่งเป็นความรับผิดชอบของ Shipment
import java.time.LocalDate;
class Order {
private final LocalDate deliveryDate;
Order(LocalDate deliveryDate) {
this.deliveryDate = deliveryDate;
}
LocalDate getDeliveryDate() {
return deliveryDate;
}
}
class Shipment {
boolean isLate(Order order, LocalDate today) {
return today.isAfter(order.getDeliveryDate());
}
}
#หลังปรับ
import java.time.LocalDate;
class Shipment {
private final LocalDate deliveryDate;
Shipment(LocalDate deliveryDate) {
this.deliveryDate = deliveryDate;
}
LocalDate getDeliveryDate() {
return deliveryDate;
}
boolean isLate(LocalDate today) {
return today.isAfter(deliveryDate);
}
}
class Order {
private final Shipment shipment;
Order(LocalDate deliveryDate) {
this.shipment = new Shipment(deliveryDate);
}
LocalDate getDeliveryDate() {
return shipment.getDeliveryDate();
}
}
ผลที่ได้: Shipment ตรวจความล่าช้าจากข้อมูลของตัวเอง โดยกำหนดเดิมว่าวันครบกำหนดยังไม่ถือว่าช้า เมธอดอ่านวันจัดส่งผ่าน Order ยังคงไว้ได้
ข้อควรระวัง: การย้าย field ใน source code ไม่ได้หมายความว่าต้องย้ายคอลัมน์ฐานข้อมูลทันที ตรวจ mapping ของ ORM, JSON, constructor และอายุของข้อมูลด้วย หากคำสั่งซื้อหนึ่งรายการจัดส่งได้หลายครั้ง ต้องออกแบบความสัมพันธ์ให้ตรงกับโดเมนก่อน
#3. Extract Class — แยกคลาสที่ทำหลายหน้าที่
ปัญหา: คลาสเดียวมีข้อมูลและพฤติกรรมหลายชุดที่เปลี่ยนด้วยเหตุผลต่างกัน
วิธีแก้: แยกความรับผิดชอบชุดหนึ่งไปยังคลาสใหม่ โดยมักใช้ Move Field และ Move Method ร่วมกัน [4]
#ก่อนปรับ
class Employee {
private final String name;
private final String areaCode;
private final String number;
Employee(String name, String areaCode, String number) {
this.name = name;
this.areaCode = areaCode;
this.number = number;
}
String getName() { return name; }
String telephoneNumber() {
return "(" + areaCode + ") " + number;
}
}
#หลังปรับ
class TelephoneNumber {
private final String areaCode;
private final String number;
TelephoneNumber(String areaCode, String number) {
this.areaCode = areaCode;
this.number = number;
}
String formatted() {
return "(" + areaCode + ") " + number;
}
}
class Employee {
private final String name;
private final TelephoneNumber telephone;
Employee(String name, String areaCode, String number) {
this.name = name;
this.telephone = new TelephoneNumber(areaCode, number);
}
String getName() { return name; }
String telephoneNumber() {
return telephone.formatted();
}
}
ผลที่ได้: กฎแสดงเบอร์โทรศัพท์มีที่อยู่ชัดเจน ขณะที่ Employee ดูแลข้อมูลพนักงาน รูปแบบผลลัพธ์เดิม เช่น (02) 1234567 ไม่เปลี่ยน
ข้อควรระวัง: อย่าแยกทุก field เป็นคลาสโดยอัตโนมัติ ให้มีเหตุผลจากความหมาย กฎ หรือแนวโน้มการเปลี่ยนแปลงที่เห็นได้จริง ตัวอย่างนี้แสดงการย้ายกฎจัดรูปแบบ ไม่ได้แสดงการตรวจสอบเบอร์โทรศัพท์ครบทุกประเทศ
#4. Inline Class — รวมคลาสที่ไม่มีบทบาทเพียงพอ
ปัญหา: คลาสหนึ่งเหลือหน้าที่น้อยมาก การเปิดไฟล์และติดตามความสัมพันธ์กลับเพิ่มภาระในการอ่านโค้ด
วิธีแก้: ย้ายสมาชิกไปอีกคลาส เปลี่ยนผู้ใช้งาน แล้วลบคลาสที่ไม่จำเป็น [5]
#ก่อนปรับ
สมมติว่า CustomerLabel ใช้ภายใน Customer เท่านั้น ไม่มี validation หรือความหมายเชิงโดเมนอิสระ
class CustomerLabel {
private final String text;
CustomerLabel(String text) { this.text = text; }
String text() { return text; }
}
class Customer {
private final CustomerLabel label;
Customer(String label) {
this.label = new CustomerLabel(label);
}
String label() { return label.text(); }
}
#หลังปรับ
class Customer {
private final String label;
Customer(String label) {
this.label = label;
}
String label() { return label; }
}
ผลที่ได้: ลดชั้นที่ไม่เพิ่มความหมาย และ Customer.label() ยังคืนข้อความเดิม
ข้อควรระวัง: คลาสขนาดเล็กอาจมีคุณค่าจาก type safety, validation, domain boundary หรือการใช้งานร่วมกัน อย่าใช้จำนวนบรรทัดเป็นเกณฑ์ตัดสินเพียงอย่างเดียว โดยเฉพาะ Value Object
#5. Hide Delegate — ซ่อนโครงสร้างการส่งต่องาน
ปัญหา: Client ต้องรู้ว่า Employee มี Department และต้องไปขอชื่อผู้จัดการจาก Department
วิธีแก้: ให้ Employee เปิดเมธอดที่ตอบความต้องการของ client แล้วส่งงานภายในเอง [6]
#ก่อนปรับ
class Department {
private final String managerName;
Department(String managerName) { this.managerName = managerName; }
String getManagerName() { return managerName; }
}
class Employee {
private final Department department;
Employee(Department department) { this.department = department; }
Department getDepartment() { return department; }
}
class Directory {
String managerOf(Employee employee) {
return employee.getDepartment().getManagerName();
}
}
#หลังปรับ
class Department {
private final String managerName;
Department(String managerName) { this.managerName = managerName; }
String getManagerName() { return managerName; }
}
class Employee {
private final Department department;
Employee(Department department) { this.department = department; }
String getManagerName() {
return department.getManagerName();
}
}
class Directory {
String managerOf(Employee employee) {
return employee.getManagerName();
}
}
ผลที่ได้: Directory ไม่ต้องอ้างชนิด Department หากโครงสร้างภายในเปลี่ยน การปรับโค้ดอาจจำกัดอยู่ใน Employee
ข้อควรระวัง: ไม่ใช่ทุก method chain เป็นปัญหา เช่น fluent API หรือ Stream API อาจออกแบบมาให้ต่อกันอยู่แล้ว และไม่ควรเพิ่มเมธอดส่งต่อทุกความสามารถจนคลาสกลายเป็น Middle Man
#6. Remove Middle Man — ลดตัวกลางที่ส่งต่อมากเกินไป
ปัญหา: คลาสมีเมธอดส่งต่อจำนวนมาก และต้องเพิ่มเมธอดใหม่ทุกครั้งที่ delegate เพิ่มความสามารถ
วิธีแก้: เปิดการเข้าถึง delegate ที่เหมาะสม เปลี่ยน client ให้เรียก delegate และลบเมธอดส่งต่อที่ไม่จำเป็น ไม่จำเป็นต้องลบคลาสตัวกลางทั้งหมด [7]
#ก่อนปรับ
class Department {
String getName() { return "Engineering"; }
String getManagerName() { return "Nida"; }
String getOfficeCode() { return "B-204"; }
}
class Employee {
private final Department department = new Department();
String departmentName() { return department.getName(); }
String managerName() { return department.getManagerName(); }
String officeCode() { return department.getOfficeCode(); }
}
class DepartmentView {
String title(Employee employee) {
return employee.departmentName() + " / " + employee.officeCode();
}
}
#หลังปรับ
class Department {
String getName() { return "Engineering"; }
String getManagerName() { return "Nida"; }
String getOfficeCode() { return "B-204"; }
}
class Employee {
private final Department department = new Department();
Department getDepartment() {
return department;
}
}
class DepartmentView {
String title(Employee employee) {
Department department = employee.getDepartment();
return department.getName() + " / " + department.getOfficeCode();
}
}
ผลที่ได้: Employee ไม่ต้องสะท้อน API ของ Department ทุกเมธอด เหมาะเมื่อ client นี้มีหน้าที่ทำงานกับข้อมูลแผนกโดยตรงอยู่แล้ว
ข้อควรระวัง: Client จะพึ่ง Department มากขึ้น หาก delegate มีเมธอดแก้ไขข้อมูล อาจต้องเปิดผ่าน interface สำหรับอ่านอย่างเดียวเพื่อรักษากฎของระบบ อย่าลบตัวกลางที่ตรวจสิทธิ์ ตรวจข้อมูล หรือรักษา transaction โดยไม่รักษาพฤติกรรมเหล่านั้น
#7. Introduce Foreign Method — เติมเมธอดเมื่อแก้คลาสต้นทางไม่ได้
ปัญหา: ต้องการความสามารถเพิ่มเติมจากคลาสของไลบรารี แต่แก้ source ของคลาสนั้นไม่ได้
วิธีแก้: เพิ่มเมธอดใน client โดยรับออบเจ็กต์ของคลาสต้นทางเป็นอาร์กิวเมนต์ เหมาะกับความสามารถจำนวนเล็กน้อย [8]
ตัวอย่าง: LocalDate มี plusDays() แต่แอปต้องการ “วันทำงานถัดไป” ตามนโยบายจันทร์–ศุกร์ จึงต้องเพิ่มกฎของแอปเอง
#ก่อนปรับ
import java.time.DayOfWeek;
import java.time.LocalDate;
class Report {
LocalDate nextRunDate(LocalDate previousEnd) {
LocalDate next = previousEnd.plusDays(1);
while (next.getDayOfWeek() == DayOfWeek.SATURDAY
|| next.getDayOfWeek() == DayOfWeek.SUNDAY) {
next = next.plusDays(1);
}
return next;
}
}
#หลังปรับ
import java.time.DayOfWeek;
import java.time.LocalDate;
class Report {
LocalDate nextRunDate(LocalDate previousEnd) {
return nextBusinessDay(previousEnd);
}
// Foreign method: extends date behavior locally in this client.
private static LocalDate nextBusinessDay(LocalDate date) {
LocalDate next = date.plusDays(1);
while (next.getDayOfWeek() == DayOfWeek.SATURDAY
|| next.getDayOfWeek() == DayOfWeek.SUNDAY) {
next = next.plusDays(1);
}
return next;
}
}
ผลที่ได้: จุดใช้งานอ่านเจตนาได้ทันที และเมธอดรับวันที่ที่ต้องดำเนินการอย่างชัดเจน เช่น วันศุกร์ 2026-10-16 ได้วันจันทร์ 2026-10-19 ทั้งสองเวอร์ชัน
Foreign Method อาจมีรูปร่างเหมือน Extract Method แต่เหตุผลในการออกแบบคือเพิ่มพฤติกรรมที่สัมพันธ์กับชนิดข้อมูลภายนอกซึ่งเราแก้ไขไม่ได้ คำว่า foreign ไม่ได้หมายถึงการเรียก API ต่างระบบ
ข้อควรระวัง: ตัวอย่างนับเฉพาะเสาร์–อาทิตย์เป็นวันหยุด ไม่รวมวันหยุดราชการ และไม่เปลี่ยนค่า LocalDate เดิม หากต้องใช้ helper เดียวกันหลาย client ควรรวมกฎไว้จุดเดียว แทนการคัดลอกไปทุกคลาส
#8. Introduce Local Extension — รวมความสามารถเพิ่มเติมเป็นชนิดใหม่
ปัญหา: ต้องเพิ่มหลายเมธอดให้คลาสภายนอก การเก็บ helper กระจายใน client เริ่มดูแลยาก
วิธีแก้: สร้าง subclass หากสืบทอดได้และเหมาะสม หรือสร้าง wrapper ที่เก็บออบเจ็กต์เดิมแล้วเพิ่มความสามารถ [9]
LocalDate เป็น final และ immutable จึงใช้ wrapper ในตัวอย่างนี้ ไม่สามารถประกาศ class BusinessDate extends LocalDate ได้ [10]
#ก่อนปรับ
import java.time.DayOfWeek;
import java.time.LocalDate;
class SchedulingService {
boolean isBusinessDay(LocalDate date) {
return date.getDayOfWeek() != DayOfWeek.SATURDAY
&& date.getDayOfWeek() != DayOfWeek.SUNDAY;
}
LocalDate nextBusinessDay(LocalDate date) {
LocalDate next = date.plusDays(1);
while (!isBusinessDay(next)) {
next = next.plusDays(1);
}
return next;
}
}
#หลังปรับ
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Objects;
final class BusinessDate {
private final LocalDate value;
BusinessDate(LocalDate value) {
this.value = Objects.requireNonNull(value);
}
boolean isBusinessDay() {
DayOfWeek day = value.getDayOfWeek();
return day != DayOfWeek.SATURDAY && day != DayOfWeek.SUNDAY;
}
BusinessDate nextBusinessDay() {
BusinessDate next = new BusinessDate(value.plusDays(1));
while (!next.isBusinessDay()) {
next = new BusinessDate(next.value.plusDays(1));
}
return next;
}
LocalDate toLocalDate() {
return value;
}
}
class SchedulingService {
boolean isBusinessDay(LocalDate date) {
return new BusinessDate(date).isBusinessDay();
}
LocalDate nextBusinessDay(LocalDate date) {
return new BusinessDate(date).nextBusinessDay().toLocalDate();
}
}
ผลที่ได้: ความสามารถเรื่องวันทำงานรวมอยู่ใน BusinessDate โดย API ของ SchedulingService ที่รับและคืน LocalDate ยังใช้ได้
ข้อควรระวัง: Wrapper ไม่ใช่ชนิดเดียวกับคลาสต้นทาง ต้องแปลงอย่างชัดเจนเมื่อส่งเข้า API ที่ต้องการ LocalDate ตัวอย่างนี้มีไว้สาธิตการห่อ ไม่ได้ติดตั้ง equals() และ hashCode() สำหรับการใช้เป็น Value Object ใน collection และยังไม่รวมปฏิทินวันหยุด
#เลือกเทคนิคที่มีทิศทางตรงข้ามอย่างไร
#Extract Class กับ Inline Class
| ประเด็น | Extract Class | Inline Class |
|---|---|---|
| ปัญหาหลัก | คลาสรับผิดชอบมากเกินไป | แยกคลาสโดยไม่เกิดคุณค่าเพียงพอ |
| การเปลี่ยน | แยกความรับผิดชอบ | รวมความรับผิดชอบที่สัมพันธ์กัน |
| คำถามตัดสิน | ชุดนี้มีเหตุผลเปลี่ยนแยกจากส่วนที่เหลือหรือไม่ | ชนิดนี้มีกฎหรือความหมายที่ต้องรักษาแยกหรือไม่ |
การเลือกขึ้นกับโครงสร้างและความต้องการปัจจุบัน เป้าหมายไม่ใช่จำนวนคลาสที่มากหรือน้อยที่สุด
#Hide Delegate กับ Remove Middle Man
| ประเด็น | Hide Delegate | Remove Middle Man |
|---|---|---|
| สิ่งที่ต้องการลด | Client รู้โครงสร้างภายในมากเกินไป | ภาระการส่งต่อที่ไม่เพิ่มคุณค่า |
| สิ่งที่เพิ่ม | เมธอดในออบเจ็กต์ที่ client รู้จัก | การพึ่ง delegate ของ client |
| เหมาะเมื่อ | ต้องซ่อนความสัมพันธ์ภายใน | Delegate เป็น abstraction ที่ client ควรใช้อยู่แล้ว |
เมธอดส่งต่อหนึ่งบรรทัดมีคุณค่าได้หากซ่อนขอบเขตที่มีแนวโน้มเปลี่ยน ไม่ควรลบทิ้งเพียงเพราะสั้น
#Introduce Foreign Method กับ Introduce Local Extension
| ประเด็น | Foreign Method | Local Extension |
|---|---|---|
| ขอบเขตเริ่มต้น | ความสามารถเฉพาะจุดจำนวนเล็กน้อย | หลายความสามารถที่เกี่ยวข้องกัน |
| ตำแหน่ง logic | Helper ใน client | Wrapper หรือ subclass |
| แนวโน้มดูแล | เรียบง่ายเมื่อใช้เฉพาะจุด | รวมกฎและนำกลับใช้ได้สะดวกขึ้น |
จำนวนเมธอดเป็นสัญญาณประกอบ ไม่ใช่เกณฑ์ตายตัว ต้องดูความซ้ำและขอบเขตความรับผิดชอบด้วย
#ความสัมพันธ์กับ Code Smells
ตารางนี้เป็นแนวทางวิเคราะห์ ไม่ใช่การจับคู่แบบบังคับ
| สัญญาณในโค้ด | เทคนิคที่ควรพิจารณา | สิ่งที่ต้องตรวจเพิ่ม |
|---|---|---|
| Feature Envy | Move Method, Move Field | กฎเป็นของคลาสปลายทางจริงหรือไม่ |
| Large Class, Divergent Change | Extract Class | แบ่งความรับผิดชอบตามเหตุผลการเปลี่ยนได้หรือไม่ |
| Lazy Class | Inline Class | คลาสมีคุณค่าด้านชนิดข้อมูลหรือขอบเขตหรือไม่ |
| Message Chains | Hide Delegate | เป็นการเปิดโครงสร้างภายใน หรือ fluent API ที่ตั้งใจออกแบบ |
| Middle Man | Remove Middle Man | ตัวกลางมี validation, policy หรือ transaction หรือไม่ |
| Incomplete Library Class | Foreign Method, Local Extension | API เดิมมีความสามารถนั้นแล้วหรือยัง |
#ขั้นตอนทำ Refactoring ในโครงการจริง
- กำหนดพฤติกรรมเดิม: ระบุ input, output, side effect และข้อผิดพลาดที่เป็นส่วนหนึ่งของสัญญา
- ตรวจผู้ใช้งาน: ค้นหาจุดเรียก เมธอดที่ override, interface, reflection และ serialization ที่เกี่ยวข้อง
- กำหนดเจ้าของ: อธิบายให้ได้ว่าทำไมข้อมูลหรือพฤติกรรมควรอยู่ในคลาสปลายทาง
- ย้ายทีละส่วน: เริ่มจากสมาชิกที่เป็น private เมื่อเหมาะสม เก็บ forwarding method ชั่วคราวได้
- ตรวจพฤติกรรม: รันทดสอบที่ครอบคลุมเส้นทางที่ย้าย และตรวจการประกอบระบบเมื่อข้ามขอบเขตโมดูล
- ปรับผู้เรียก: เปลี่ยน call site และ dependency อย่างเป็นลำดับ
- ลบสิ่งที่หมดหน้าที่: ลบสมาชิกเดิมเมื่อยืนยันว่าไม่มีผู้ใช้ที่ต้องรองรับ แล้วทดสอบอีกครั้ง
หากเป็น public API ที่มีผู้ใช้ภายนอก ควรคงเมธอดเดิมเป็นตัวส่งต่อและวางแผน deprecation ตามนโยบายเวอร์ชันของโครงการ
#กรณีตรวจสอบพฤติกรรมสำหรับตัวอย่าง
ตารางนี้เป็นข้อเสนอสำหรับนำไปเขียน characterization tests ในเวิร์กช็อป ไม่ใช่รายงานผลการรันทดสอบ
| หัวข้อ | กรณีตัวอย่าง | ผลที่ควรเหมือนกันก่อน–หลัง |
|---|---|---|
| Move Method | น้ำหนัก 5,000 กรัม ไม่ด่วน | 10,000 สตางค์ |
| Move Method | น้ำหนัก 5,001 กรัม แบบด่วน | 25,000 สตางค์ |
| Move Field | วันนี้เท่ากับวันครบกำหนด | false จาก isLate |
| Move Field | วันนี้หลังวันครบกำหนดหนึ่งวัน | true จาก isLate |
| Extract Class | รหัสพื้นที่ 02, เบอร์ 1234567 |
(02) 1234567 |
| Inline Class | ป้ายข้อความ VIP |
VIP |
| Hide Delegate | แผนกมีผู้จัดการชื่อ Nida |
Nida |
| Remove Middle Man | แผนกในตัวอย่าง | Engineering / B-204 |
| Foreign Method, Local Extension | ศุกร์ 2026-10-16 |
จันทร์ 2026-10-19 |
| Foreign Method, Local Extension | เสาร์ 2026-10-17 |
จันทร์ 2026-10-19 |
| Foreign Method, Local Extension | จันทร์ 2026-10-19 |
อังคาร 2026-10-20 |
#แบบฝึกหัด
Invoiceคำนวณส่วนลดโดยอ่านข้อมูลจากCustomerเกือบทั้งหมด ควรย้ายเมธอดหรือแยกนโยบายส่วนลด เพราะเหตุใด?Userมีข้อมูลบัญชีและที่อยู่จัดส่งหลาย field เสนอขอบเขตการทำ Extract Class และจุดที่ควรรักษา API เดิม- Client เรียก
order.getCustomer().getAddress().getPostalCode()ควรซ่อนการเข้าถึงจุดใด และ client มีหน้าที่อะไร? - Service มีเมธอดส่งต่อไป SDK จำนวนมาก ตรวจอย่างไรว่าควร Remove Middle Man หรือรักษา wrapper ไว้?
- เพิ่มการรองรับวันหยุดราชการในตัวอย่างวันทำงาน โดยอธิบายว่าเหตุใดการเพิ่มนโยบายนี้ควรแยกจากขั้นตอน refactoring ที่รักษาพฤติกรรมเดิม
#ข้อคิดสำหรับการออกแบบ
คำถามที่ใช้ได้กับทั้ง 8 เทคนิคคือ “ข้อมูลนี้เป็นของใคร และพฤติกรรมนี้ควรเปลี่ยนไปพร้อมกับอะไร?” ย้ายสมาชิกเมื่อช่วยให้ความรับผิดชอบชัดขึ้น แยกคลาสเมื่อพบขอบเขตที่มีความหมาย และรวมคลาสเมื่อขอบเขตเดิมไม่ช่วยให้อ่านหรือดูแลระบบได้ดีขึ้น
#แหล่งอ้างอิง
เรียบเรียงคำอธิบายภาษาไทยและเขียนตัวอย่าง Java ขึ้นใหม่ โดยใช้ชื่อและแนวคิดของเทคนิคจาก Refactoring.Guru ตรวจสอบแหล่งข้อมูลวันที่ 16 ตุลาคม 2026
- Refactoring.Guru — Moving Features between Objects
- Refactoring.Guru — Move Method
- Refactoring.Guru — Move Field
- Refactoring.Guru — Extract Class
- Refactoring.Guru — Inline Class
- Refactoring.Guru — Hide Delegate
- Refactoring.Guru — Remove Middle Man
- Refactoring.Guru — Introduce Foreign Method
- Refactoring.Guru — Introduce Local Extension
- Oracle — LocalDate API