- ภาพรวมทั้ง 8 เทคนิค
- 1. Decompose Conditional — แยกเงื่อนไขให้สื่อความหมาย
- 2. Consolidate Conditional Expression — รวมเงื่อนไขที่ให้ผลเดียวกัน
- 3. Consolidate Duplicate Conditional Fragments — ย้ายส่วนซ้ำออกจากสาขา
- 4. Remove Control Flag — ลดตัวแปรที่ใช้ควบคุมการไหล
- 5. Replace Nested Conditional with Guard Clauses — จัดการกรณีพิเศษก่อน
- 6. Replace Conditional with Polymorphism — ให้ออบเจ็กต์เลือกพฤติกรรม
- 7. Introduce Null Object — แทนการไม่มีบริการด้วยพฤติกรรมเริ่มต้น
- 8. Introduce Assertion — ทำสมมติฐานภายในให้ตรวจสอบได้
- เทคนิคที่คล้ายกัน แต่แก้คนละปัญหา
- แนวทางนำไปใช้ในงานจริง
- แบบฝึกหัด
- แหล่งอ้างอิง
เงื่อนไขที่อ่านยากทำให้ผู้พัฒนาต้องไล่ตามเส้นทางการทำงานหลายชั้นก่อนจะเข้าใจกฎของระบบ การปรับโครงสร้างในกลุ่ม Simplifying Conditional Expressions ช่วยทำให้เงื่อนไขสื่อความหมาย ลดความซ้ำซ้อน และเปิดเผยเส้นทางการทำงานหลักได้ชัดขึ้น

บทความนี้อธิบาย 8 เทคนิคตามหมวดหมู่ของ Refactoring.Guru พร้อมตัวอย่าง Java ที่เขียนขึ้นใหม่เพื่อการสอน [1–8] เป้าหมายของ refactoring คือปรับโครงสร้างโดยรักษาพฤติกรรมที่สังเกตได้สำหรับขอบเขตข้อมูลและสัญญาการใช้งานเดิม
ตัวอย่างเป็นส่วนของโปรแกรมสำหรับเปรียบเทียบก่อน–หลัง ให้นำเมธอดไปไว้ในคลาสที่เหมาะสม โค้ดก่อนและหลังเป็นทางเลือก ไม่ใช่โค้ดที่ต้องวางรวมกัน ตัวเลขใช้สื่อแนวคิดเท่านั้น ระบบการเงินจริงควรกำหนดชนิดข้อมูลและการปัดเศษให้ชัดเจน
#ภาพรวมทั้ง 8 เทคนิค
| เทคนิค | ปัญหาที่พบ | แนวทางแก้ไข |
|---|---|---|
| Decompose Conditional | นิพจน์และงานในแต่ละสาขาซับซ้อน | แยกเป็นเมธอดที่มีชื่อสื่อความหมาย |
| Consolidate Conditional Expression | หลายเงื่อนไขนำไปสู่ผลเดียวกัน | รวมเงื่อนไขและตั้งชื่อกฎ |
| Consolidate Duplicate Conditional Fragments | ทุกสาขามีคำสั่งบางส่วนเหมือนกัน | ย้ายส่วนร่วมออกจากเงื่อนไข |
| Remove Control Flag | ใช้ตัวแปรคอยสั่งให้วนต่อหรือหยุด | ใช้ break, continue หรือ return ตามขอบเขต |
| Replace Nested Conditional with Guard Clauses | เงื่อนไขซ้อนจนเส้นทางหลักมองเห็นยาก | จัดการกรณีพิเศษแล้วออกก่อน |
| Replace Conditional with Polymorphism | เลือกพฤติกรรมตามชนิดของออบเจ็กต์ | ให้ออบเจ็กต์แต่ละชนิดรับผิดชอบพฤติกรรม |
| Introduce Null Object | ตรวจ null เพื่อเลือกพฤติกรรมเริ่มต้นซ้ำ ๆ |
ใช้ออบเจ็กต์ที่ให้พฤติกรรมเริ่มต้น |
| Introduce Assertion | โค้ดอาศัยสมมติฐานภายในที่ไม่ได้ระบุ | เขียน assertion ให้สมมติฐานตรวจสอบได้ |
#1. Decompose Conditional — แยกเงื่อนไขให้สื่อความหมาย
ปัญหา: ต้องอ่านรายละเอียดของเงื่อนไขและสูตรคำนวณพร้อมกัน จึงจะรู้ว่าแต่ละสาขาหมายถึงอะไร
แนวทาง: แยกส่วนตรวจเงื่อนไข งานเมื่อเป็นจริง และงานเมื่อเป็นเท็จเป็นเมธอด ตั้งชื่อให้บอกเจตนา [1]
#ก่อนปรับโครงสร้าง
double deliveryFee(boolean member, double subtotal, double weightKg) {
if (member && subtotal >= 1000) {
return weightKg * 5;
} else {
return 40 + weightKg * 10;
}
}
#หลังปรับโครงสร้าง
double deliveryFee(boolean member, double subtotal, double weightKg) {
if (qualifiesForMemberDelivery(member, subtotal)) {
return memberDeliveryFee(weightKg);
}
return standardDeliveryFee(weightKg);
}
private boolean qualifiesForMemberDelivery(boolean member, double subtotal) {
return member && subtotal >= 1000;
}
private double memberDeliveryFee(double weightKg) {
return weightKg * 5;
}
private double standardDeliveryFee(double weightKg) {
return 40 + weightKg * 10;
}
ผู้อ่านเห็นนโยบายการจัดส่งก่อน แล้วจึงเปิดดูรายละเอียดเมื่อจำเป็น การแยกเมธอดคุ้มค่าเมื่อชื่อช่วยอธิบายกฎ หากชื่อไม่เพิ่มความเข้าใจหรือทำให้ต้องกระโดดอ่านมากเกินไป ควรพิจารณาคงนิพจน์สั้น ๆ ไว้
จุดตรวจ: สมาชิกที่ยอดซื้อ 999 และ 1000 ต้องเลือกสาขาเหมือนเดิม และต้องไม่ย้ายการคำนวณที่มีผลข้างเคียงให้ทำงานในสาขาที่เดิมไม่เคยทำ
#2. Consolidate Conditional Expression — รวมเงื่อนไขที่ให้ผลเดียวกัน
ปัญหา: หลาย if ส่งกลับผลเดียวกัน ทำให้กฎเดียวกระจัดกระจาย
แนวทาง: รวมด้วย || หรือ && ให้ตรงกับตรรกะเดิม แล้วแยกเป็นเมธอดที่บอกความหมายของกฎ [2]
#ก่อนปรับโครงสร้าง
boolean canEnroll(boolean active, boolean prerequisitePassed, boolean full) {
if (!active) {
return false;
}
if (!prerequisitePassed) {
return false;
}
if (full) {
return false;
}
return true;
}
#หลังปรับโครงสร้าง
boolean canEnroll(boolean active, boolean prerequisitePassed, boolean full) {
return !isEnrollmentBlocked(active, prerequisitePassed, full);
}
private boolean isEnrollmentBlocked(
boolean active, boolean prerequisitePassed, boolean full) {
return !active || !prerequisitePassed || full;
}
กฎใหม่อ่านได้ว่า “ลงทะเบียนได้เมื่อไม่มีเหตุปิดกั้น” และยังให้ผลเดิมครบทั้ง 8 ชุดค่าของ boolean ทั้งสามตัว
จุดตรวจ: a() || b() และ a() && b() ใช้ short-circuit จึงอาจไม่เรียก b() ต้องรักษาลำดับ จำนวนครั้ง และผลข้างเคียงของการประเมินเงื่อนไข อย่ารวมกรณีที่ผลลัพธ์หรือข้อความแจ้งข้อผิดพลาดจำเป็นต้องแตกต่างกัน
#3. Consolidate Duplicate Conditional Fragments — ย้ายส่วนซ้ำออกจากสาขา
ปัญหา: ทุกสาขามีคำสั่งชุดเดียวกัน ทำให้ต้องแก้หลายตำแหน่งเมื่อเปลี่ยนงานส่วนร่วม
แนวทาง: ย้ายส่วนซ้ำออกมาไว้ก่อนหรือหลังเงื่อนไข โดยรักษาลำดับที่จำเป็น [3]
#ก่อนปรับโครงสร้าง
void printReceipt(boolean member, double subtotal) {
double total;
if (member) {
total = subtotal * 0.90;
System.out.println("Total: " + total);
} else {
total = subtotal;
System.out.println("Total: " + total);
}
}
#หลังปรับโครงสร้าง
void printReceipt(boolean member, double subtotal) {
double total;
if (member) {
total = subtotal * 0.90;
} else {
total = subtotal;
}
System.out.println("Total: " + total);
}
คำสั่งพิมพ์ยังทำงานหลังคำนวณยอดเสร็จหนึ่งครั้งเหมือนเดิม ความแตกต่างของสาขาเหลือเฉพาะกฎส่วนลด
จุดตรวจ: ตรวจทั้งค่าที่พิมพ์และจำนวนครั้ง อย่าย้ายคำสั่งข้ามงานที่เปลี่ยนข้อมูลที่คำสั่งนั้นใช้ รวมถึงตรวจสาขาที่มี return หรือโยน exception เพราะอาจทำให้ส่วนร่วมไม่ถูกเรียกตามเดิม
#4. Remove Control Flag — ลดตัวแปรที่ใช้ควบคุมการไหล
ปัญหา: ต้องติดตามตัวแปรอย่าง found หรือ done หลายตำแหน่งเพื่อรู้ว่าลูปควรหยุดเมื่อใด
แนวทาง: ใช้คำสั่งควบคุมการไหลโดยตรง เมื่อมีขอบเขตและความหมายตรงกับ flag เดิม [4]
#ก่อนปรับโครงสร้าง
boolean containsBlockedCode(String[] codes) {
boolean found = false;
int index = 0;
while (index < codes.length && !found) {
if ("BLOCK".equals(codes[index])) {
found = true;
}
index++;
}
return found;
}
#หลังปรับโครงสร้าง
boolean containsBlockedCode(String[] codes) {
for (String code : codes) {
if ("BLOCK".equals(code)) {
return true;
}
}
return false;
}
เมธอดมีหน้าที่ตอบคำถามเดียว เมื่อพบคำตอบจึงส่งกลับได้ทันที ตัวอย่างสมมติว่าอาร์เรย์ไม่เป็น null แต่สมาชิกในอาร์เรย์เป็น null ได้ทั้งก่อนและหลัง
| คำสั่ง | ขอบเขตที่เปลี่ยนการทำงาน |
|---|---|
break |
ออกจากลูปหรือ switch ที่ครอบอยู่ใกล้ที่สุดตามรูปแบบการใช้ |
continue |
ข้ามงานที่เหลือของรอบปัจจุบันไปยังรอบถัดไป |
return |
ออกจากเมธอด |
boolean ที่เป็นสถานะทางธุรกิจ เช่น paid หรือ active ไม่จำเป็นต้องลบ การใช้ flag บางแบบยังช่วยให้โค้ดเข้าใจง่ายได้
จุดตรวจ: อาร์เรย์ว่าง ไม่พบ พบต้นรายการ และพบท้ายรายการ รวมถึงงานหลังลูปที่ยังต้องทำก่อนออกจากเมธอด
#5. Replace Nested Conditional with Guard Clauses — จัดการกรณีพิเศษก่อน
ปัญหา: เส้นทางปกติอยู่ลึกใน if หลายระดับ ทำให้ต้องจำเงื่อนไขที่ผ่านมาทั้งหมด
แนวทาง: ตรวจกรณีพิเศษทีละข้อและจบการทำงานของกรณีนั้น แล้ววางงานหลักในระดับปกติ [5]
#ก่อนปรับโครงสร้าง
String enrollmentStatus(boolean active, boolean passed, boolean full) {
if (active) {
if (passed) {
if (!full) {
return "ENROLLED";
} else {
return "CLASS_FULL";
}
} else {
return "PREREQUISITE_REQUIRED";
}
} else {
return "INACTIVE";
}
}
#หลังปรับโครงสร้าง
String enrollmentStatus(boolean active, boolean passed, boolean full) {
if (!active) {
return "INACTIVE";
}
if (!passed) {
return "PREREQUISITE_REQUIRED";
}
if (full) {
return "CLASS_FULL";
}
return "ENROLLED";
}
เส้นทางสำเร็จอ่านได้โดยไม่ต้องมองผ่านวงเล็บซ้อน และลำดับความสำคัญของเหตุปฏิเสธยังเหมือนเดิม
จุดตรวจ: เมื่อ active = false, passed = false และ full = true พร้อมกัน ต้องได้ INACTIVE เหมือนเดิม การสลับลำดับ guard อาจเปลี่ยนผลลัพธ์ แม้จะตรวจเงื่อนไขชุดเดียวกัน
#6. Replace Conditional with Polymorphism — ให้ออบเจ็กต์เลือกพฤติกรรม
ปัญหา: ใช้รหัสชนิดเลือกพฤติกรรม และโค้ดลักษณะนี้เกิดซ้ำหลายตำแหน่ง
แนวทาง: สร้างชนิดย่อยที่มีเมธอดร่วมกัน แล้วกระจายงานแต่ละสาขาไปยังชนิดที่รับผิดชอบ [6]
#ก่อนปรับโครงสร้าง
enum DeliveryType { STANDARD, EXPRESS }
long deliveryFee(DeliveryType type, int weightKg) {
switch (type) {
case STANDARD:
return 30L + 10L * weightKg;
case EXPRESS:
return 60L + 20L * weightKg;
default:
throw new IllegalArgumentException("Unsupported delivery type");
}
}
#หลังปรับโครงสร้าง
abstract class Delivery {
abstract long fee(int weightKg);
}
final class StandardDelivery extends Delivery {
@Override
long fee(int weightKg) {
return 30L + 10L * weightKg;
}
}
final class ExpressDelivery extends Delivery {
@Override
long fee(int weightKg) {
return 60L + 20L * weightKg;
}
}
เรียกพฤติกรรมผ่านชนิดแม่:
Delivery delivery = new ExpressDelivery();
long fee = delivery.fee(2); // 100
จุดเรียกไม่ต้องตรวจชนิดอีก และสามารถเพิ่มวิธีจัดส่งโดยเพิ่มชนิดย่อยได้ อย่างไรก็ตาม จุดแปลงข้อมูลภายนอกเป็นออบเจ็กต์ เช่น factory อาจยังต้องมีเงื่อนไขเลือกชนิด
ข้อพิจารณา: ไม่จำเป็นต้องแทนทุก switch ด้วยคลาส หากมีเพียงจุดเดียวและกฎเรียบง่าย switch อาจเหมาะกว่า หากความแตกต่างเป็นนโยบายที่เลือกหรือเปลี่ยนระหว่างทำงาน การใช้ Strategy ผ่าน composition อาจสื่อแบบจำลองได้ดีกว่า
จุดตรวจ: ค่าบริการของทุกชนิดและกรณีข้อมูลไม่ถูกต้อง การย้ายไปใช้คลาสต้องย้ายผู้เรียกและรักษานโยบายรับมือรหัสชนิดที่ไม่รู้จักด้วย
#7. Introduce Null Object — แทนการไม่มีบริการด้วยพฤติกรรมเริ่มต้น
ปัญหา: ผู้เรียกต้องตรวจ null ซ้ำ ๆ เพื่อเลือกว่าจะเรียกบริการหรือไม่
แนวทาง: ใช้ออบเจ็กต์ที่ปฏิบัติตามสัญญาเดียวกันและให้พฤติกรรมเริ่มต้นที่เหมาะสม เช่น ไม่ส่งการแจ้งเตือน [7]
#ส่วนประกอบร่วม
interface Notifier {
void send(String message);
}
#ก่อนปรับโครงสร้าง
void notifyCompleted(Notifier notifier) {
if (notifier != null) {
notifier.send("Completed");
}
}
#หลังปรับโครงสร้าง
final class NoOpNotifier implements Notifier {
@Override
public void send(String message) {
// ไม่ส่งข้อความตามนโยบายปิดการแจ้งเตือน
}
}
void notifyCompleted(Notifier notifier) {
notifier.send("Completed");
}
แปลงค่าเพียงครั้งเดียวที่ขอบเขตการประกอบระบบ:
Notifier effectiveNotifier = configuredNotifier != null
? configuredNotifier
: new NoOpNotifier();
notifyCompleted(effectiveNotifier);
configuredNotifier หมายถึงค่าที่อ่านมาจากการตั้งค่า หลังการแปลง ผู้เรียกต้องส่ง Notifier ที่ไม่เป็น null จึงจะเป็นไปตามสัญญาใหม่ของเมธอด
ข้อพิจารณา: “ไม่ส่งการแจ้งเตือน” ต้องเป็นพฤติกรรมที่ธุรกิจยอมรับ ไม่ควรสร้าง Null Object เพื่อซ่อนบริการที่ล้มเหลวหรือข้อมูลที่จำเป็นแต่หายไป หากผู้เรียกจำเป็นต้องรู้ว่าค้นหาไม่พบ การคืน Optional หรือแจ้งข้อผิดพลาดอาจเหมาะกว่า
จุดตรวจ: บริการจริงส่งหนึ่งครั้ง และบริการเริ่มต้นไม่ส่งอะไร พร้อมตรวจจุดประกอบระบบให้ไม่มี null หลุดถึงผู้เรียก
#8. Introduce Assertion — ทำสมมติฐานภายในให้ตรวจสอบได้
ปัญหา: โค้ดจะถูกต้องต่อเมื่อเงื่อนไขบางอย่างเป็นจริง แต่มีเพียงความเข้าใจหรือคอมเมนต์กำกับ
แนวทาง: เขียน assertion เพื่อบอก invariant หรือสมมติฐานภายในที่โปรแกรมคาดว่าจะเป็นจริง [8]
#ก่อนปรับโครงสร้าง
int remainingSeats(int capacity, int enrolled) {
// ภายในระบบต้องรักษา 0 <= enrolled <= capacity
return capacity - enrolled;
}
#หลังปรับโครงสร้าง
int remainingSeats(int capacity, int enrolled) {
assert capacity >= 0 : "Capacity must be non-negative";
assert enrolled >= 0 && enrolled <= capacity
: "Enrollment invariant violated";
return capacity - enrolled;
}
เมื่อ invariant เป็นจริง ผลคำนวณยังเหมือนเดิม เมื่อเปิด assertions และ invariant ถูกละเมิด Java จะโยน AssertionError หากปิด assertions นิพจน์ใน assert จะไม่ถูกประเมิน จึงต้องไม่ใส่การเปลี่ยนสถานะที่จำเป็นไว้ในนั้น [9]
ตัวอย่างการเปิด assertions สำหรับโปรแกรมที่มีคลาสหลักชื่อ Demo:
java -ea Demo
อย่าใช้ assert เป็นการตรวจข้อมูลภายนอกที่จำเป็นต้องทำทุกครั้ง หากเมธอดสาธารณะรับจำนวนที่นั่งจากคำขอ API ควรตรวจด้วยเงื่อนไขและ exception ตามปกติ:
if (requestedSeats <= 0) {
throw new IllegalArgumentException("requestedSeats must be positive");
}
ตัวอย่าง requestedSeats เป็นการเปรียบเทียบวิธีตรวจ input ไม่ใช่ส่วนต่อของเมธอด remainingSeats หากระบบเดิมโยน exception สำหรับ input ที่ผิดอยู่แล้ว อย่าแทนด้วย assertion เพราะอาจเปลี่ยนพฤติกรรมเมื่อปิด assertions
จุดตรวจ: ทดสอบค่าที่ขอบเขตและค่าที่ผิด invariant โดยเปิด assertions และยืนยันว่าการปิด assertions ไม่ทำให้การทำงานปกติขาดขั้นตอนสำคัญ
#เทคนิคที่คล้ายกัน แต่แก้คนละปัญหา
| คู่ที่มักสับสน | ความแตกต่าง |
|---|---|
| Decompose กับ Consolidate Expression | Decompose ตั้งชื่อให้รายละเอียด ส่วน Consolidate รวมเงื่อนไขที่ไปสู่ผลเดียวกัน |
| Consolidate Expression กับ Duplicate Fragments | อย่างแรกจัดการส่วนตรวจเงื่อนไข อย่างหลังจัดการคำสั่งที่ซ้ำในสาขา |
| Guard Clauses กับ Remove Control Flag | Guard Clauses ลดความลึก ส่วน Remove Control Flag ลดการติดตามตัวแปรควบคุม |
| Guard Clauses กับ Assertion | Guard จัดการกรณีที่เกิดได้ตามสัญญา ส่วน assertion ตรวจสมมติฐานภายใน |
| Null Object กับการตรวจ input | Null Object ให้พฤติกรรมเริ่มต้น ส่วน validation ปฏิเสธข้อมูลที่ไม่เป็นไปตามสัญญา |
#แนวทางนำไปใช้ในงานจริง
- ระบุพฤติกรรมปัจจุบัน รวมถึงผลลัพธ์ exception การเปลี่ยนสถานะ และการเรียกบริการภายนอก
- เลือกเทคนิคจากปัญหาที่ต้องการแก้ เช่น ลดการซ้อนด้วย guard ก่อนแยกพฤติกรรมตามชนิด
- ปรับทีละส่วนเล็ก ๆ โดยคงลำดับการประเมินและขอบเขตข้อมูลเดิม
- ตรวจกรณีปกติ กรณีพิเศษ ค่าที่ขอบเขต และหลายเงื่อนไขที่เป็นจริงพร้อมกัน
- พิจารณาว่าชื่อเมธอดและโครงสร้างใหม่ช่วยให้ผู้อ่านเข้าใจกฎเร็วขึ้นหรือไม่
ตัวอย่างชุดกรณีสำหรับ enrollmentStatus ควรครอบคลุม boolean ทั้งสามตัวครบ 8 ชุด เพื่อตรวจลำดับเหตุปฏิเสธ ส่วนการย้ายคำสั่งส่วนร่วมต้องตรวจจำนวนและลำดับการเรียกด้วย การตรวจเฉพาะค่าที่ส่งกลับอาจไม่พบผลข้างเคียงที่เปลี่ยนไป
การลดจำนวน if ไม่ใช่ตัวชี้วัดความสำเร็จเพียงอย่างเดียว โค้ดที่ดีควรแสดงกฎได้ตรงความหมาย มีความซับซ้อนเหมาะกับปัญหา และยังรักษาพฤติกรรมตามสัญญาเดิม
#แบบฝึกหัด
- เลือกเมธอดที่มีเงื่อนไขซ้อนอย่างน้อย 3 ชั้น แล้วเขียน guard clauses โดยรักษาลำดับความสำคัญของข้อผิดพลาด
- หาเมธอดที่มีหลาย
ifคืนค่าเดียวกัน แล้วรวมเป็นเมธอด boolean ที่มีชื่อสื่อกฎ - หาโค้ดซ้ำท้าย
if/elseแล้วย้ายออก พร้อมอธิบายว่าลำดับการทำงานยังเหมือนเดิมอย่างไร - ออกแบบ Null Object สำหรับบริการที่ปิดใช้งานได้ และอธิบายกรณีที่ควรแจ้งข้อผิดพลาดแทน
#แหล่งอ้างอิง
ตรวจสอบแหล่งข้อมูลวันที่ 16 ตุลาคม 2026 ตัวอย่าง Java และคำแนะนำเชิงการสอนในบทความนี้เขียนขึ้นใหม่ ไม่ได้คัดลอกตัวอย่างจากแหล่งอ้างอิง
- Refactoring.Guru — Decompose Conditional
- Refactoring.Guru — Consolidate Conditional Expression
- Refactoring.Guru — Consolidate Duplicate Conditional Fragments
- Refactoring.Guru — Remove Control Flag
- Refactoring.Guru — Replace Nested Conditional with Guard Clauses
- Refactoring.Guru — Replace Conditional with Polymorphism
- Refactoring.Guru — Introduce Null Object
- Refactoring.Guru — Introduce Assertion
- Oracle — Java Language Specification §14.10: The assert Statement