จัดข้อมูลและพฤติกรรมให้อยู่กับออบเจ็กต์ที่มีความรับผิดชอบเหมาะสม ผ่าน 8 เทคนิค พร้อมตัวอย่างก่อน–หลัง

info-refactoring-moving-features-between-objects-java

#แนวคิดพื้นฐาน

เมื่อระบบเติบโต ปัญหามักไม่ได้อยู่ที่อัลกอริทึมเพียงอย่างเดียว แต่อยู่ที่ ข้อมูลและเมธอดอยู่ผิดคลาส เช่น 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 ในโครงการจริง

  1. กำหนดพฤติกรรมเดิม: ระบุ input, output, side effect และข้อผิดพลาดที่เป็นส่วนหนึ่งของสัญญา
  2. ตรวจผู้ใช้งาน: ค้นหาจุดเรียก เมธอดที่ override, interface, reflection และ serialization ที่เกี่ยวข้อง
  3. กำหนดเจ้าของ: อธิบายให้ได้ว่าทำไมข้อมูลหรือพฤติกรรมควรอยู่ในคลาสปลายทาง
  4. ย้ายทีละส่วน: เริ่มจากสมาชิกที่เป็น private เมื่อเหมาะสม เก็บ forwarding method ชั่วคราวได้
  5. ตรวจพฤติกรรม: รันทดสอบที่ครอบคลุมเส้นทางที่ย้าย และตรวจการประกอบระบบเมื่อข้ามขอบเขตโมดูล
  6. ปรับผู้เรียก: เปลี่ยน call site และ dependency อย่างเป็นลำดับ
  7. ลบสิ่งที่หมดหน้าที่: ลบสมาชิกเดิมเมื่อยืนยันว่าไม่มีผู้ใช้ที่ต้องรองรับ แล้วทดสอบอีกครั้ง

หากเป็น 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

#แบบฝึกหัด

  1. Invoice คำนวณส่วนลดโดยอ่านข้อมูลจาก Customer เกือบทั้งหมด ควรย้ายเมธอดหรือแยกนโยบายส่วนลด เพราะเหตุใด?
  2. User มีข้อมูลบัญชีและที่อยู่จัดส่งหลาย field เสนอขอบเขตการทำ Extract Class และจุดที่ควรรักษา API เดิม
  3. Client เรียก order.getCustomer().getAddress().getPostalCode() ควรซ่อนการเข้าถึงจุดใด และ client มีหน้าที่อะไร?
  4. Service มีเมธอดส่งต่อไป SDK จำนวนมาก ตรวจอย่างไรว่าควร Remove Middle Man หรือรักษา wrapper ไว้?
  5. เพิ่มการรองรับวันหยุดราชการในตัวอย่างวันทำงาน โดยอธิบายว่าเหตุใดการเพิ่มนโยบายนี้ควรแยกจากขั้นตอน refactoring ที่รักษาพฤติกรรมเดิม

#ข้อคิดสำหรับการออกแบบ

คำถามที่ใช้ได้กับทั้ง 8 เทคนิคคือ “ข้อมูลนี้เป็นของใคร และพฤติกรรมนี้ควรเปลี่ยนไปพร้อมกับอะไร?” ย้ายสมาชิกเมื่อช่วยให้ความรับผิดชอบชัดขึ้น แยกคลาสเมื่อพบขอบเขตที่มีความหมาย และรวมคลาสเมื่อขอบเขตเดิมไม่ช่วยให้อ่านหรือดูแลระบบได้ดีขึ้น

#แหล่งอ้างอิง

เรียบเรียงคำอธิบายภาษาไทยและเขียนตัวอย่าง Java ขึ้นใหม่ โดยใช้ชื่อและแนวคิดของเทคนิคจาก Refactoring.Guru ตรวจสอบแหล่งข้อมูลวันที่ 16 ตุลาคม 2026

  1. Refactoring.Guru — Moving Features between Objects
  2. Refactoring.Guru — Move Method
  3. Refactoring.Guru — Move Field
  4. Refactoring.Guru — Extract Class
  5. Refactoring.Guru — Inline Class
  6. Refactoring.Guru — Hide Delegate
  7. Refactoring.Guru — Remove Middle Man
  8. Refactoring.Guru — Introduce Foreign Method
  9. Refactoring.Guru — Introduce Local Extension
  10. Oracle — LocalDate API