- Generalization เกี่ยวข้องกับการออกแบบอย่างไร
- ภาพรวมทั้ง 12 เทคนิค
- 1. Pull Up Field: ย้ายข้อมูลร่วมขึ้นคลาสแม่
- 2. Pull Up Method: ย้ายพฤติกรรมร่วมขึ้นคลาสแม่
- 3. Pull Up Constructor Body: รวมการเริ่มต้นข้อมูลร่วม
- 4. Push Down Method: ย้ายพฤติกรรมเฉพาะลงคลาสลูก
- 5. Push Down Field: ย้ายข้อมูลเฉพาะลงคลาสลูก
- 6. Extract Subclass: แยกกรณีเฉพาะเป็นคลาสลูก
- 7. Extract Superclass: สร้างคลาสแม่จากส่วนร่วม
- 8. Extract Interface: แยกสัญญาความสามารถ
- 9. Collapse Hierarchy: รวมระดับที่ไม่เพิ่มคุณค่า
- 10. Form Template Method: รวมโครงของอัลกอริทึม
- 11. Replace Inheritance with Delegation: เปลี่ยนจากสืบทอดเป็นส่งต่องาน
- 12. Replace Delegation with Inheritance: เปลี่ยนตัวห่อเป็นชนิดย่อย
- เลือก inheritance, interface หรือ delegation อย่างไร
- เทคนิคที่มักสับสน
- ขั้นตอนนำไปใช้กับโค้ดจริง
- แบบฝึกหัดสำหรับห้องเรียน
- หลักที่ควรนำกลับไปใช้
- แหล่งอ้างอิง
การจัดวางความรับผิดชอบในลำดับชั้นของคลาส ด้วย 12 เทคนิค Refactoring

#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 ส่งต่องานให้ออบเจ็กต์อีกตัว |
#ขั้นตอนนำไปใช้กับโค้ดจริง
- ระบุสัญญาที่ต้องรักษา เช่น ผลตอบกลับ exception ที่ผู้เรียกพึ่งพา ลำดับ side effects และชนิดที่ API เปิดเผย
- ตรวจผู้เรียกและ dependency รวม constructor, overriding, ORM, reflection และ serialization เมื่อเกี่ยวข้อง
- เลือกการเปลี่ยนแปลงเล็กที่สุด เช่น Pull Up Method หนึ่งรายการ ก่อนสร้างโครงสร้างใหม่ทั้งชุด
- ย้ายส่วนร่วมตาม dependency หาก method ต้องใช้ข้อมูลร่วม อาจต้องจัดการ field หรือ accessor ก่อน
- ปรับผู้เรียกอย่างครบถ้วน โดยเฉพาะเมื่อสร้าง subclass ใหม่ ลบชนิด หรือเลิกใช้ inheritance
- ตรวจผลลัพธ์ก่อนและหลัง ด้วย automated tests ที่ครอบคลุมพฤติกรรมเดิม แล้ว compile และตรวจระบบที่ได้รับผลกระทบ
กรณีทดสอบที่มีประโยชน์ ได้แก่ ผลลัพธ์และ exception ของ method ที่ย้าย ค่าเริ่มต้นหลังเรียก constructor การเรียกผ่าน reference ชนิดแม่ ลำดับขั้นตอนของ template และ side effects ที่ delegate ต้องทำงานเพียงครั้งเดียว
#แบบฝึกหัดสำหรับห้องเรียน
LecturerและResearcherมีemployeeIdและorganization()ซ้ำกัน ควรใช้เทคนิคใดบ้าง และต้องตรวจความหมายอะไรบ้างก่อนย้าย?EmployeeมีdrivingLicenseNumberแต่ใช้จริงเฉพาะDriverควรย้ายอะไรนอกเหนือจาก field?- รายงานสองแบบอ่านข้อมูลเหมือนกันและประกอบหัวท้ายเหมือนกัน แต่ format ต่างกัน ออกแบบ Template Method โดยให้ลำดับหลักเปลี่ยนไม่ได้
NotificationServiceผูกกับEmailSenderโดยตรง จะแยกสัญญาเพื่อทดสอบ service ได้อย่างไร?ReportService extends Printerมีผู้เรียกใช้ inheritedprint()อยู่ จะเปลี่ยนเป็น delegation อย่างเป็นขั้นตอนได้อย่างไร?- ตัวห่อมี forwarding methods จำนวนมาก แต่ตรวจสิทธิ์ก่อนส่งต่องานทุกครั้ง เหตุใดการเปลี่ยนเป็น inheritance ทันทีจึงอาจเปลี่ยนพฤติกรรมระบบ?
#หลักที่ควรนำกลับไปใช้
วางส่วนร่วมไว้ใน abstraction ที่มีความหมาย วางส่วนเฉพาะไว้กับชนิดที่ต้องใช้ และเลือกความสัมพันธ์จากสัญญาพฤติกรรมจริง การลดโค้ดซ้ำมีประโยชน์เมื่อไม่ทำให้ลำดับชั้นซับซ้อนหรือผูกติดกันเกินความจำเป็น
#แหล่งอ้างอิง
ตรวจสอบแหล่งอ้างอิงวันที่ 16 ตุลาคม 2026 ตัวอย่าง Java เขียนขึ้นใหม่สำหรับบทความนี้ ไม่ได้คัดลอกตัวอย่างโค้ดจากแหล่งอ้างอิง