เงื่อนไขที่อ่านยากทำให้ผู้พัฒนาต้องไล่ตามเส้นทางการทำงานหลายชั้นก่อนจะเข้าใจกฎของระบบ การปรับโครงสร้างในกลุ่ม Simplifying Conditional Expressions ช่วยทำให้เงื่อนไขสื่อความหมาย ลดความซ้ำซ้อน และเปิดเผยเส้นทางการทำงานหลักได้ชัดขึ้น

info-refactoring-simplifying-conditional-expressions-java

บทความนี้อธิบาย 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 ปฏิเสธข้อมูลที่ไม่เป็นไปตามสัญญา

#แนวทางนำไปใช้ในงานจริง

  1. ระบุพฤติกรรมปัจจุบัน รวมถึงผลลัพธ์ exception การเปลี่ยนสถานะ และการเรียกบริการภายนอก
  2. เลือกเทคนิคจากปัญหาที่ต้องการแก้ เช่น ลดการซ้อนด้วย guard ก่อนแยกพฤติกรรมตามชนิด
  3. ปรับทีละส่วนเล็ก ๆ โดยคงลำดับการประเมินและขอบเขตข้อมูลเดิม
  4. ตรวจกรณีปกติ กรณีพิเศษ ค่าที่ขอบเขต และหลายเงื่อนไขที่เป็นจริงพร้อมกัน
  5. พิจารณาว่าชื่อเมธอดและโครงสร้างใหม่ช่วยให้ผู้อ่านเข้าใจกฎเร็วขึ้นหรือไม่

ตัวอย่างชุดกรณีสำหรับ enrollmentStatus ควรครอบคลุม boolean ทั้งสามตัวครบ 8 ชุด เพื่อตรวจลำดับเหตุปฏิเสธ ส่วนการย้ายคำสั่งส่วนร่วมต้องตรวจจำนวนและลำดับการเรียกด้วย การตรวจเฉพาะค่าที่ส่งกลับอาจไม่พบผลข้างเคียงที่เปลี่ยนไป

การลดจำนวน if ไม่ใช่ตัวชี้วัดความสำเร็จเพียงอย่างเดียว โค้ดที่ดีควรแสดงกฎได้ตรงความหมาย มีความซับซ้อนเหมาะกับปัญหา และยังรักษาพฤติกรรมตามสัญญาเดิม

#แบบฝึกหัด

  • เลือกเมธอดที่มีเงื่อนไขซ้อนอย่างน้อย 3 ชั้น แล้วเขียน guard clauses โดยรักษาลำดับความสำคัญของข้อผิดพลาด
  • หาเมธอดที่มีหลาย if คืนค่าเดียวกัน แล้วรวมเป็นเมธอด boolean ที่มีชื่อสื่อกฎ
  • หาโค้ดซ้ำท้าย if/else แล้วย้ายออก พร้อมอธิบายว่าลำดับการทำงานยังเหมือนเดิมอย่างไร
  • ออกแบบ Null Object สำหรับบริการที่ปิดใช้งานได้ และอธิบายกรณีที่ควรแจ้งข้อผิดพลาดแทน

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

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

  1. Refactoring.Guru — Decompose Conditional
  2. Refactoring.Guru — Consolidate Conditional Expression
  3. Refactoring.Guru — Consolidate Duplicate Conditional Fragments
  4. Refactoring.Guru — Remove Control Flag
  5. Refactoring.Guru — Replace Nested Conditional with Guard Clauses
  6. Refactoring.Guru — Replace Conditional with Polymorphism
  7. Refactoring.Guru — Introduce Null Object
  8. Refactoring.Guru — Introduce Assertion
  9. Oracle — Java Language Specification §14.10: The assert Statement