การจัดวางความรับผิดชอบในลำดับชั้นของคลาส ด้วย 12 เทคนิค Refactoring

info-refactoring-dealing-with-generalization-java

#Generalization เกี่ยวข้องกับการออกแบบอย่างไร

ในการออกแบบเชิงวัตถุ Generalization คือการมองหาคุณสมบัติร่วมของชนิดที่เฉพาะเจาะจง แล้วอธิบายคุณสมบัตินั้นผ่านชนิดที่ทั่วไปกว่า เช่น Lecturer และ Researcher อาจมีคุณสมบัติร่วมใน Employee ส่วน interface ใช้อธิบายสัญญาความสามารถที่หลายคลาสรองรับได้

Refactoring หมวด Dealing with Generalization ช่วยปรับโครงสร้างเหล่านี้เมื่อความรับผิดชอบอยู่ผิดระดับ มีโค้ดซ้ำ หรือใช้ inheritance ไม่สอดคล้องกับความสัมพันธ์ในระบบ เทคนิคในหมวดนี้ครอบคลุมการย้ายสมาชิกขึ้นลงในลำดับชั้น การสร้างหรือรวมชนิด และการสลับระหว่าง inheritance กับ delegation [1]

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

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

#ภาพรวมทั้ง 12 เทคนิค

เทคนิค สัญญาณที่ควรพิจารณา การเปลี่ยนแปลงหลัก
Pull Up Field คลาสลูกเก็บข้อมูลชนิดเดียวกันและความหมายเดียวกัน ย้าย field ไปคลาสแม่
Pull Up Method คลาสลูกมีพฤติกรรมเหมือนกัน ย้าย method ไปคลาสแม่
Pull Up Constructor Body constructor ของคลาสลูกเริ่มต้นข้อมูลร่วมซ้ำกัน ย้ายส่วนร่วมไป constructor คลาสแม่
Push Down Method method ในคลาสแม่เหมาะกับคลาสลูกเพียงบางชนิด ย้าย method ลงไปยังชนิดที่ต้องใช้
Push Down Field field ในคลาสแม่ไม่มีความหมายกับคลาสลูกส่วนใหญ่ ย้าย field ลงไปยังชนิดที่เป็นเจ้าของข้อมูล
Extract Subclass คลาสหนึ่งมีข้อมูลหรือพฤติกรรมเฉพาะบางกรณี แยกกรณีนั้นเป็นคลาสลูก
Extract Superclass หลายคลาสมีสถานะและพฤติกรรมร่วมที่สอดคล้องกัน สร้างคลาสแม่แล้วรวมส่วนร่วม
Extract Interface ผู้เรียกต้องการเพียงสัญญาความสามารถบางส่วน สร้าง interface ให้พึ่งพา
Collapse Hierarchy คลาสแม่กับคลาสลูกแทบไม่มีความต่างที่มีประโยชน์ รวมระดับของลำดับชั้น
Form Template Method หลายอัลกอริทึมมีขั้นตอนลำดับเดียวกัน ยกโครงขั้นตอนไปคลาสแม่
Replace Inheritance with Delegation คลาสต้องการใช้ความสามารถของอีกคลาส แต่ความสัมพันธ์แบบชนิดย่อยไม่เหมาะ ถือออบเจ็กต์แล้วส่งต่องาน
Replace Delegation with Inheritance ตัวห่อส่งต่องานเกือบทั้งหมด และเป็นชนิดย่อยได้จริง เปลี่ยนเป็นคลาสลูก

รายละเอียดของแต่ละเทคนิคอ้างอิงรายการ [2]–[13] ส่วนตัวอย่างและข้อเสนอแนะในการเลือกใช้อธิบายเพิ่มเติมสำหรับ Java

#1. Pull Up Field: ย้ายข้อมูลร่วมขึ้นคลาสแม่

ปัญหา: คลาสลูกหลายคลาสประกาศ field ที่มีความหมายเดียวกัน เช่น Lecturer และ Researcher ต่างมี name [2]

แนวทาง: ย้าย field ไปยังคลาสแม่ที่เหมาะสม แล้วลบ field ซ้ำออกจากคลาสลูก

ก่อนปรับ:

class Employee { }

class Lecturer extends Employee {
    protected String name;
}

class Researcher extends Employee {
    protected String name;
}

หลังปรับ:

class Employee {
    protected String name;
}

class Lecturer extends Employee { }
class Researcher extends Employee { }

ทั้งสองคลาสยังมีข้อมูล name ผ่านการสืบทอด แต่ประกาศไว้เพียงแห่งเดียว การย้าย field ไม่ได้ทำให้พนักงานทุกคนใช้ค่าเดียวกัน เพราะ field นี้ยังเป็นข้อมูลประจำแต่ละ instance

ข้อควรระวัง: ชื่อเหมือนกันยังไม่พอ ต้องตรวจชนิดข้อมูล หน่วย และความหมายด้วย ตัวอย่างใช้ protected ให้เห็นการย้ายชัดเจน ในระบบจริงควรพิจารณา private พร้อม constructor หรือ method ที่ควบคุมการเข้าถึง เพื่อจำกัดการผูกติดกับรายละเอียดภายใน

#2. Pull Up Method: ย้ายพฤติกรรมร่วมขึ้นคลาสแม่

ปัญหา: คลาสลูกมี method ทำงานเหมือนกัน เมื่อแก้ไขต้องตามแก้หลายแห่ง [3]

ก่อนปรับ:

class Employee { }

class Lecturer extends Employee {
    public String organization() {
        return "NPRU";
    }
}

class Researcher extends Employee {
    public String organization() {
        return "NPRU";
    }
}

หลังปรับ:

class Employee {
    public String organization() {
        return "NPRU";
    }
}

class Lecturer extends Employee { }
class Researcher extends Employee { }

ผลที่ได้: พฤติกรรมร่วมมีแหล่งแก้ไขเดียว คลาสลูกแต่ละชนิดยังเรียก organization() ได้

ข้อควรระวัง: หาก method อ้าง field หรือ helper เฉพาะคลาสลูก ต้องจัดการ dependency ก่อน อย่าย้ายขึ้นเพียงเพราะชื่อ method เหมือนกัน และต้องยืนยันว่าพฤติกรรมนี้เหมาะกับชนิดลูกทั้งหมดของคลาสแม่นั้น

#3. Pull Up Constructor Body: รวมการเริ่มต้นข้อมูลร่วม

ปัญหา: constructor ของคลาสลูกมีส่วนกำหนดค่าเริ่มต้นหรือ validation ซ้ำกัน [4]

ก่อนปรับ:

class Employee {
    protected String name;
}

class Lecturer extends Employee {
    Lecturer(String name) {
        this.name = name;
    }
}

class Researcher extends Employee {
    Researcher(String name) {
        this.name = name;
    }
}

หลังปรับ:

class Employee {
    private final String name;

    protected Employee(String name) {
        this.name = name;
    }

    public String name() {
        return name;
    }
}

class Lecturer extends Employee {
    Lecturer(String name) {
        super(name);
    }
}

class Researcher extends Employee {
    Researcher(String name) {
        super(name);
    }
}

ตัวอย่างหลังปรับรวมการกำหนด name และปรับการเข้าถึงให้ผ่าน accessor ด้วย หากมีโค้ดอื่นเข้าถึง field เดิมโดยตรง ต้องย้ายผู้เรียกเหล่านั้นมาใช้ accessor

ข้อควรระวัง: constructor ไม่ได้สืบทอดเหมือน method ปกติ คลาสลูกต้องเรียก constructor ที่เหมาะสมของคลาสแม่ ตัวอย่างวาง super(name) เป็นคำสั่งแรกเพื่อใช้ได้กับ Java รุ่นที่ใช้งานทั่วไป และควรหลีกเลี่ยงการเรียก method ที่คลาสลูก override ได้จาก constructor เพราะสถานะของคลาสลูกอาจยังเริ่มต้นไม่ครบ

#4. Push Down Method: ย้ายพฤติกรรมเฉพาะลงคลาสลูก

ปัญหา: method อยู่ในคลาสแม่ แต่มีความหมายกับคลาสลูกเพียงบางชนิด [5]

ก่อนปรับ:

class Employee {
    public double commission(double sales) {
        return sales * 0.05;
    }
}

class Salesperson extends Employee { }
class Accountant extends Employee { }

หลังปรับ:

class Employee { }

class Salesperson extends Employee {
    public double commission(double sales) {
        return sales * 0.05;
    }
}

class Accountant extends Employee { }

ผลที่ได้: การคำนวณค่าคอมมิชชันอยู่กับประเภทพนักงานที่ต้องใช้

ข้อควรระวัง: ผู้เรียกที่ถือ reference เป็น Employee จะเรียก commission() ไม่ได้อีก ต้องปรับชนิดหรือสัญญาของผู้เรียกอย่างเหมาะสม หากหลายคลาสต้องมีความสามารถนี้ อาจใช้ interface เช่น CommissionEligible แทนการเพิ่มการตรวจชนิดและ cast กระจายทั่วระบบ

#5. Push Down Field: ย้ายข้อมูลเฉพาะลงคลาสลูก

ปัญหา: คลาสแม่มี field ที่คลาสลูกส่วนใหญ่ไม่ใช้ เช่น พนักงานทุกคนถูกบังคับให้มีเลขใบอนุญาตขับรถ [6]

ก่อนปรับ:

class Employee {
    protected String drivingLicenseNumber;
}

class Driver extends Employee { }
class Accountant extends Employee { }

หลังปรับ:

class Employee { }

class Driver extends Employee {
    private String drivingLicenseNumber;
}

class Accountant extends Employee { }

ผลที่ได้: ข้อมูลเกี่ยวกับการขับรถอยู่ใน Driver และไม่สร้างสถานะที่ไม่มีความหมายให้ชนิดอื่น

ข้อควรระวัง: ย้าย constructor, accessor และ logic ที่เกี่ยวข้องตามไปด้วย ตรวจการแมป ORM และการ serialize หากระบบภายนอกคาดหวัง field เดิมอยู่ เพราะการเปลี่ยนตำแหน่งในโมเดลอาจต้องอาศัยการย้ายข้อมูลเพิ่มเติม

#6. Extract Subclass: แยกกรณีเฉพาะเป็นคลาสลูก

ปัญหา: คลาสเดียวมีข้อมูลหรือพฤติกรรมที่ใช้เฉพาะบางกรณี [7]

ก่อนปรับ:

class Employee {
    private final boolean manager;

    Employee(boolean manager) {
        this.manager = manager;
    }

    public int bonus() {
        return manager ? 10000 : 3000;
    }
}

หลังปรับ:

class Employee {
    public int bonus() {
        return 3000;
    }
}

class Manager extends Employee {
    @Override
    public int bonus() {
        return 10000;
    }
}

ผู้เรียกเดิมที่สร้าง new Employee(true) เปลี่ยนเป็น new Manager() ส่วน new Employee(false) เปลี่ยนเป็น new Employee() การเรียก bonus() ผ่านตัวแปรชนิด Employee ยังเลือกพฤติกรรมตามออบเจ็กต์จริงได้

ข้อควรระวัง: เหมาะกับความแตกต่างที่เป็นชนิดย่อยค่อนข้างคงที่ หากพนักงานเปลี่ยนบทบาทระหว่างทำงานบ่อย หรือมีหลายบทบาทพร้อมกัน การแยกบทบาทเป็นออบเจ็กต์หรือ Strategy อาจเหมาะกว่า เพราะไม่ต้องเปลี่ยนชนิดของออบเจ็กต์เดิม

#7. Extract Superclass: สร้างคลาสแม่จากส่วนร่วม

ปัญหา: คลาสที่แยกกันมีทั้งข้อมูลและพฤติกรรมร่วมที่ควรมีแหล่งดูแลเดียว [8]

ก่อนปรับ:

class Student {
    private final String name;

    Student(String name) { this.name = name; }
    public String name() { return name; }
}

class Lecturer {
    private final String name;

    Lecturer(String name) { this.name = name; }
    public String name() { return name; }
}

หลังปรับ:

abstract class Person {
    private final String name;

    protected Person(String name) {
        this.name = name;
    }

    public String name() {
        return name;
    }
}

class Student extends Person {
    Student(String name) { super(name); }
}

class Lecturer extends Person {
    Lecturer(String name) { super(name); }
}

ต่างจาก Pull Up อย่างไร: Extract Superclass สร้างคลาสแม่ใหม่ก่อน แล้วอาจใช้ Pull Up Field, Pull Up Method และ Pull Up Constructor Body เพื่อย้ายส่วนร่วมตามมา

ข้อควรระวัง: นักศึกษาและอาจารย์เป็น “บทบาท” ที่บุคคลเดียวอาจมีพร้อมกันได้ ตัวอย่างนี้เหมาะกับโมเดลแบบง่าย หากระบบรองรับหลายบทบาท ควรพิจารณา Person ที่มีรายการบทบาทผ่าน composition แทนการบังคับแยกออบเจ็กต์เป็นคนละชนิด

#8. Extract Interface: แยกสัญญาความสามารถ

ปัญหา: ผู้เรียกต้องการใช้ความสามารถเพียงส่วนหนึ่ง แต่ผูกกับคลาสเฉพาะโดยตรง [9]

ก่อนปรับ:

class EmailSender {
    public void send(String text) {
        System.out.println("Email: " + text);
    }
}

class NotificationService {
    private final EmailSender sender;

    NotificationService(EmailSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String text) {
        sender.send(text);
    }
}

หลังปรับ:

interface MessageSender {
    void send(String text);
}

class EmailSender implements MessageSender {
    @Override
    public void send(String text) {
        System.out.println("Email: " + text);
    }
}

class NotificationService {
    private final MessageSender sender;

    NotificationService(MessageSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String text) {
        sender.send(text);
    }
}

ผลที่ได้: service ต้องรู้เพียงว่าสิ่งที่ได้รับส่งข้อความได้ ต่อมาจึงเพิ่ม implementation อื่นหรือ test double ได้โดยไม่ต้องเปลี่ยน logic ของ service

ข้อควรระวัง: interface ไม่ได้ย้าย instance field หรือ implementation ร่วมออกจากคลาสโดยอัตโนมัติ ประโยชน์หลักในตัวอย่างนี้คือแยกสัญญาที่ผู้เรียกพึ่งพา ควรเลือก method ตามความต้องการของผู้ใช้สัญญา และไม่สร้าง interface ขนาดใหญ่โดยรวมทุก method ของคลาสเดิม

#9. Collapse Hierarchy: รวมระดับที่ไม่เพิ่มคุณค่า

ปัญหา: คลาสลูกแทบไม่ต่างจากคลาสแม่ และระดับเพิ่มเติมทำให้ต้องตามอ่านหลายไฟล์ [10]

ก่อนปรับ:

class Employee {
    public String category() {
        return "employee";
    }
}

class RegularEmployee extends Employee { }

หลังปรับ:

class Employee {
    public String category() {
        return "employee";
    }
}

เปลี่ยนการสร้าง new RegularEmployee() และ reference ที่เกี่ยวข้องมาใช้ Employee แล้วลบชนิดที่เกินความจำเป็น

ข้อควรระวัง: คลาสว่างอาจยังมีบทบาทในระบบ เช่น เป็นชนิดสำหรับเลือก dependency หรือเป็น discriminator ของ ORM จึงต้องตรวจการใช้ชนิดนั้นจริงก่อนลบ รวมถึง reflection, serialization และโค้ดนอก repository ที่อาจพึ่งชื่อคลาสเดิม

#10. Form Template Method: รวมโครงของอัลกอริทึม

ปัญหา: หลายคลาสทำงานตามลำดับขั้นตอนเดียวกัน แต่บางขั้นตอนมีรายละเอียดต่างกัน [11]

ก่อนปรับ:

class PlainReport {
    public String render() {
        String data = "Naruapon";
        String body = data;
        return "BEGIN\n" + body + "\nEND";
    }
}

class BracketReport {
    public String render() {
        String data = "Naruapon";
        String body = "[" + data + "]";
        return "BEGIN\n" + body + "\nEND";
    }
}

หลังปรับ:

abstract class Report {
    public final String render() {
        String data = load();
        String body = format(data);
        return "BEGIN\n" + body + "\nEND";
    }

    private String load() {
        return "Naruapon";
    }

    protected abstract String format(String data);
}

class PlainReport extends Report {
    @Override
    protected String format(String data) {
        return data;
    }
}

class BracketReport extends Report {
    @Override
    protected String format(String data) {
        return "[" + data + "]";
    }
}

ผลที่ได้: คลาสแม่ควบคุมลำดับ load → format → ประกอบรายงาน คลาสลูกกำหนดเฉพาะรูปแบบเนื้อหา final ป้องกันการ override method ที่กำหนดลำดับหลักในตัวอย่างนี้

ต่างจาก Pull Up Method อย่างไร: Pull Up Method ย้ายพฤติกรรมที่เหมือนกัน ส่วน Form Template Method แยกโครงที่เหมือนกันออกจากรายละเอียดที่ต่างกัน จึงทำให้ได้การออกแบบแบบ Template Method

ข้อควรระวัง: หากลำดับขั้นตอนต่างกันมาก การบังคับให้อยู่ใน template เดียวจะเพิ่ม hook และเงื่อนไขจำนวนมาก ควรพิจารณา composition หรือ Strategy เพื่อประกอบกระบวนการแทน

#11. Replace Inheritance with Delegation: เปลี่ยนจากสืบทอดเป็นส่งต่องาน

ปัญหา: คลาสใช้ inheritance เพื่อขอยืมความสามารถบางส่วน ทั้งที่ไม่ควรเป็นชนิดย่อยของคลาสนั้น [12]

ตัวอย่าง ReportService ใช้ Printer เพื่อพิมพ์รายงาน แต่บริการรายงานไม่ได้มีความหมายว่าเป็นเครื่องพิมพ์

ก่อนปรับ:

class Printer {
    public void print(String text) {
        System.out.println(text);
    }
}

class ReportService extends Printer {
    public void printReport(String title) {
        print("Report: " + title);
    }
}

หลังปรับ:

class Printer {
    public void print(String text) {
        System.out.println(text);
    }
}

class ReportService {
    private final Printer printer;

    ReportService(Printer printer) {
        this.printer = printer;
    }

    public void printReport(String title) {
        printer.print("Report: " + title);
    }
}

การสร้างออบเจ็กต์เปลี่ยนเป็น new ReportService(new Printer()) และ printReport("Annual") ยังแสดงข้อความเดิม

ผลที่ได้: ความสัมพันธ์สะท้อนการ “ใช้เครื่องพิมพ์” และสลับ dependency ได้ผ่าน constructor

ข้อควรระวัง: หลังปรับ ReportService ไม่ใช่ Printer และไม่มี inherited print() อีก หากผู้เรียกเคยใช้สองลักษณะนี้ ต้องย้ายผู้เรียกหรือคง forwarding method ที่จำเป็นในช่วงเปลี่ยนผ่านก่อน อย่าลบความสามารถที่มีผู้ใช้โดยไม่ตรวจสัญญาของระบบ

#12. Replace Delegation with Inheritance: เปลี่ยนตัวห่อเป็นชนิดย่อย

ปัญหา: คลาสมี method ส่งต่องานซ้ำ ๆ ไปยังออบเจ็กต์เดียว และความสัมพันธ์เป็นชนิดย่อยนั้นถูกต้องจริง [13]

ตัวอย่างสมมติ BasicPrinter เป็นคลาสภายในที่ออกแบบให้ขยายต่อได้ มีเพียงสอง public methods ที่แสดงด้านล่าง ส่วน OfficePrinter ไม่ได้เพิ่มนโยบายควบคุมอื่น

ก่อนปรับ:

class BasicPrinter {
    public void print(String text) {
        System.out.println(text);
    }

    public boolean ready() {
        return true;
    }
}

class OfficePrinter {
    private final BasicPrinter printer = new BasicPrinter();

    public void print(String text) {
        printer.print(text);
    }

    public boolean ready() {
        return printer.ready();
    }
}

หลังปรับ:

class BasicPrinter {
    public void print(String text) {
        System.out.println(text);
    }

    public boolean ready() {
        return true;
    }
}

class OfficePrinter extends BasicPrinter { }

ผลที่ได้: ลบ forwarding methods และออบเจ็กต์ตัวกลางที่ไม่เพิ่มพฤติกรรม OfficePrinter ใช้ print() และ ready() ที่สืบทอดมา

ข้อควรระวัง: จำนวน forwarding methods เป็นเพียงสัญญาณเริ่มต้น ต้องยืนยันว่าคลาสลูกใช้แทนคลาสแม่ได้ตามสัญญาเดิม และยอมรับ public API ทั้งหมดของคลาสแม่ได้ Java สืบทอดคลาสได้เพียงหนึ่งคลาส คลาสแม่ต้องไม่เป็น final และ constructor ต้องเข้าถึงได้ การเปลี่ยนนี้ยังลดความยืดหยุ่นในการเปลี่ยน delegate ระหว่างทำงาน หากตัวห่อมีหน้าที่ด้าน authorization, logging หรือนโยบายอื่น อย่าลบหน้าที่เหล่านั้นเพียงเพื่อลดจำนวนบรรทัด

#เลือก inheritance, interface หรือ delegation อย่างไร

คำถามในการออกแบบ แนวทางที่ควรพิจารณา สิ่งที่ต้องตรวจ
ต้องการใช้สถานะและ implementation ร่วมกัน และชนิดย่อยใช้แทนชนิดแม่ได้หรือไม่ superclass / abstract class สัญญาพฤติกรรมและ dependency ที่เพิ่มขึ้น
ผู้เรียกต้องการเพียงความสามารถร่วม โดย implementation ต่างกันได้หรือไม่ interface ขอบเขตสัญญาควรเล็กและสอดคล้องกัน
คลาสหนึ่งใช้ความสามารถของอีกคลาส โดยไม่ต้องเป็นชนิดเดียวกันหรือไม่ composition และ delegation การควบคุม API และวงจรชีวิตของ dependency
ความแตกต่างเปลี่ยนระหว่าง runtime หรือมีหลายมิติที่ผสมกันได้หรือไม่ Strategy หรือ composition หลีกเลี่ยงจำนวน subclass ที่เพิ่มตามทุกชุดผสม

Liskov Substitution Principle (LSP) ช่วยทบทวนการใช้ inheritance: เมื่อผู้เรียกทำงานกับชนิดแม่ การแทนด้วยชนิดลูกควรรักษาสัญญาที่ผู้เรียกพึ่งพา ตัวอย่างเช่น หากชนิดแม่รับอินพุตชุดหนึ่งได้ คลาสลูกไม่ควรเพิ่มข้อบังคับจนผู้เรียกเดิมใช้งานไม่ได้

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

#เทคนิคที่มักสับสน

คู่เทคนิค ความแตกต่าง
Pull Up ↔ Push Down Pull Up รวมส่วนที่ใช้ร่วมกันขึ้นไป ส่วน Push Down จำกัดส่วนเฉพาะให้อยู่ในชนิดที่ต้องใช้
Extract Superclass ↔ Extract Interface superclass รองรับการรวมสถานะและ implementation ส่วน interface แยกสัญญาความสามารถในบริบทของตัวอย่างนี้
Extract Subclass ↔ Collapse Hierarchy อย่างแรกเพิ่มชนิดย่อยเพื่อแยกความแตกต่าง อย่างหลังลดระดับเมื่อความต่างนั้นไม่จำเป็น
Pull Up Method ↔ Form Template Method อย่างแรกย้าย method ที่เหมือนกัน อย่างหลังรวมโครงอัลกอริทึมและคงจุดแตกต่างไว้ให้ override
Inheritance ↔ Delegation inheritance สร้างความสัมพันธ์ชนิดย่อย ส่วน delegation ส่งต่องานให้ออบเจ็กต์อีกตัว

#ขั้นตอนนำไปใช้กับโค้ดจริง

  1. ระบุสัญญาที่ต้องรักษา เช่น ผลตอบกลับ exception ที่ผู้เรียกพึ่งพา ลำดับ side effects และชนิดที่ API เปิดเผย
  2. ตรวจผู้เรียกและ dependency รวม constructor, overriding, ORM, reflection และ serialization เมื่อเกี่ยวข้อง
  3. เลือกการเปลี่ยนแปลงเล็กที่สุด เช่น Pull Up Method หนึ่งรายการ ก่อนสร้างโครงสร้างใหม่ทั้งชุด
  4. ย้ายส่วนร่วมตาม dependency หาก method ต้องใช้ข้อมูลร่วม อาจต้องจัดการ field หรือ accessor ก่อน
  5. ปรับผู้เรียกอย่างครบถ้วน โดยเฉพาะเมื่อสร้าง subclass ใหม่ ลบชนิด หรือเลิกใช้ inheritance
  6. ตรวจผลลัพธ์ก่อนและหลัง ด้วย automated tests ที่ครอบคลุมพฤติกรรมเดิม แล้ว compile และตรวจระบบที่ได้รับผลกระทบ

กรณีทดสอบที่มีประโยชน์ ได้แก่ ผลลัพธ์และ exception ของ method ที่ย้าย ค่าเริ่มต้นหลังเรียก constructor การเรียกผ่าน reference ชนิดแม่ ลำดับขั้นตอนของ template และ side effects ที่ delegate ต้องทำงานเพียงครั้งเดียว

#แบบฝึกหัดสำหรับห้องเรียน

  1. Lecturer และ Researcher มี employeeId และ organization() ซ้ำกัน ควรใช้เทคนิคใดบ้าง และต้องตรวจความหมายอะไรบ้างก่อนย้าย?
  2. Employee มี drivingLicenseNumber แต่ใช้จริงเฉพาะ Driver ควรย้ายอะไรนอกเหนือจาก field?
  3. รายงานสองแบบอ่านข้อมูลเหมือนกันและประกอบหัวท้ายเหมือนกัน แต่ format ต่างกัน ออกแบบ Template Method โดยให้ลำดับหลักเปลี่ยนไม่ได้
  4. NotificationService ผูกกับ EmailSender โดยตรง จะแยกสัญญาเพื่อทดสอบ service ได้อย่างไร?
  5. ReportService extends Printer มีผู้เรียกใช้ inherited print() อยู่ จะเปลี่ยนเป็น delegation อย่างเป็นขั้นตอนได้อย่างไร?
  6. ตัวห่อมี forwarding methods จำนวนมาก แต่ตรวจสิทธิ์ก่อนส่งต่องานทุกครั้ง เหตุใดการเปลี่ยนเป็น inheritance ทันทีจึงอาจเปลี่ยนพฤติกรรมระบบ?

#หลักที่ควรนำกลับไปใช้

วางส่วนร่วมไว้ใน abstraction ที่มีความหมาย วางส่วนเฉพาะไว้กับชนิดที่ต้องใช้ และเลือกความสัมพันธ์จากสัญญาพฤติกรรมจริง การลดโค้ดซ้ำมีประโยชน์เมื่อไม่ทำให้ลำดับชั้นซับซ้อนหรือผูกติดกันเกินความจำเป็น

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

ตรวจสอบแหล่งอ้างอิงวันที่ 16 ตุลาคม 2026 ตัวอย่าง Java เขียนขึ้นใหม่สำหรับบทความนี้ ไม่ได้คัดลอกตัวอย่างโค้ดจากแหล่งอ้างอิง

  1. Refactoring.Guru — Dealing with Generalization
  2. Pull Up Field
  3. Pull Up Method
  4. Pull Up Constructor Body
  5. Push Down Method
  6. Push Down Field
  7. Extract Subclass
  8. Extract Superclass
  9. Extract Interface
  10. Collapse Hierarchy
  11. Form Template Method
  12. Replace Inheritance with Delegation
  13. Replace Delegation with Inheritance