#Code Smell คืออะไร?

Code Smell คือสัญญาณในโค้ดที่บ่งชี้ว่าการออกแบบอาจทำให้บำรุงรักษา ทดสอบ หรือขยายระบบได้ยาก แม้โปรแกรมยังทำงานถูกต้อง Code Smell ไม่ได้หมายความว่าเป็น Bug เสมอไป

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

#ตัวอย่าง Code Smells ที่พบบ่อย

Code Smell ลักษณะ เทคนิค Refactoring
Long Method เมธอดยาว ทำหลายหน้าที่ Extract Method
Large Class คลาสมีความรับผิดชอบมากเกินไป Extract Class
Duplicate Code ตรรกะซ้ำหลายจุด Extract Method / Pull Up Method
Long Parameter List เมธอดรับพารามิเตอร์จำนวนมาก Introduce Parameter Object
Feature Envy เมธอดใช้ข้อมูลของคลาสอื่นมากเกินไป Move Method
Data Clumps ข้อมูลหลายตัวปรากฏคู่กันเสมอ Extract Class
Primitive Obsession ใช้ String/int แทนแนวคิดโดเมน Replace Primitive with Value Object
Switch Statements เงื่อนไขแยกตามชนิดซ้ำหลายจุด Replace Conditional with Polymorphism (เมื่อเหมาะสม)
Shotgun Surgery แก้เรื่องเดียวแต่ต้องเปลี่ยนหลายไฟล์ Move Method / Encapsulate Change
Dead Code โค้ดที่ไม่มีการใช้งาน Remove Dead Code

ไม่ใช่ทุก Code Smell ต้องแก้ทันที: พิจารณาความถี่ในการเปลี่ยนโค้ด ความเสี่ยง และต้นทุนการแก้ไข

#Workshop 1: Long Method → Extract Method

#ก่อน Refactoring

import java.util.List;

public class OrderService {
    public double calculateTotal(List<Double> prices) {
        double total = 0;
        for (double price : prices) {
            if (price < 0) throw new IllegalArgumentException("Negative price");
            total += price;
        }
        if (total >= 1000) total *= 0.90;
        return total;
    }
}

#หลัง Refactoring

import java.util.List;

public class OrderService {
    public double calculateTotal(List<Double> prices) {
        double subtotal = sumValidPrices(prices);
        return applyDiscount(subtotal);
    }

    private double sumValidPrices(List<Double> prices) {
        double total = 0;
        for (double price : prices) {
            if (price < 0) throw new IllegalArgumentException("Negative price");
            total += price;
        }
        return total;
    }

    private double applyDiscount(double subtotal) {
        return subtotal >= 1000 ? subtotal * 0.90 : subtotal;
    }
}

เมธอด calculateTotal() กลายเป็นขั้นตอนระดับสูงที่อ่านเข้าใจง่าย โดยยังคงลำดับการตรวจสอบราคาและการคิดส่วนลดเดิม

#Workshop 2: Duplicate Code → Extract Method

#ก่อน Refactoring

public class UserValidator {
    public boolean validCustomerEmail(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }
    public boolean validAdminEmail(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }
}

#หลัง Refactoring

public class UserValidator {
    public boolean validCustomerEmail(String email) {
        return validEmail(email);
    }
    public boolean validAdminEmail(String email) {
        return validEmail(email);
    }
    private boolean validEmail(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }
}

หมายเหตุ: เงื่อนไขตัวอย่างนี้เป็นเพียงการตรวจแบบง่าย ไม่ใช่ตัวตรวจสอบรูปแบบอีเมลที่สมบูรณ์

#Workshop 3: Long Parameter List → Parameter Object

#ก่อน Refactoring

public class ShippingService {
    public String label(String name, String street, String city, String postalCode) {
        return name + " | " + street + " | " + city + " | " + postalCode;
    }
}

#หลัง Refactoring (Java 17+)

public record Address(String name, String street, String city, String postalCode) {}

public class ShippingService {
    public String label(Address address) {
        return address.name() + " | " + address.street() + " | "
             + address.city() + " | " + address.postalCode();
    }
}

#ตรวจสอบด้วย Unit Test (JUnit 5)

import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;

class OrderServiceTest {
    private final OrderService service = new OrderService();

    @Test void normalOrder() {
        assertEquals(300.0, service.calculateTotal(List.of(100.0, 200.0)), 0.00001);
    }

    @Test void discountedOrder() {
        assertEquals(900.0, service.calculateTotal(List.of(600.0, 400.0)), 0.00001);
    }

    @Test void rejectsNegativePrice() {
        assertThrows(IllegalArgumentException.class,
            () -> service.calculateTotal(List.of(100.0, -1.0)));
    }
}

#ขั้นตอน Refactoring อย่างปลอดภัย

  1. ระบุ Code Smell และปัญหาที่เกิดจริง เช่น แก้ไขยากหรือทดสอบยาก
  2. เขียน Characterization Tests เพื่อบันทึกพฤติกรรมปัจจุบัน รวมถึงกรณีขอบ
  3. เปลี่ยนโค้ดทีละขั้นเล็ก ๆ โดยไม่เปลี่ยนความสามารถภายนอก
  4. รัน Unit Tests และ Integration Tests หลังแต่ละขั้น
  5. ตรวจสอบ diff, static analysis และ code review
  6. แยกการเปลี่ยนพฤติกรรม (feature/bug fix) ออกจาก commit ที่ทำ refactoring

#เครื่องมือที่ช่วยตรวจจับ

  • IntelliJ IDEA / Eclipse: คำสั่ง Extract Method, Rename, Move และ Inline
  • SonarQube: วิเคราะห์ maintainability issues และ code smells ตามกฎของเครื่องมือ
  • PMD / Checkstyle: วิเคราะห์รูปแบบและกฎคุณภาพโค้ด
  • JaCoCo: วัด test coverage (coverage สูงไม่รับประกันว่าทดสอบครบ)
  • JUnit 5: ยืนยันพฤติกรรมด้วย automated tests

#แบบฝึกหัด

  1. เพิ่มการทดสอบที่ยอดรวมเท่ากับ 1,000 และรายการว่าง
  2. สร้างคลาสที่มี switch ตามประเภทการชำระเงิน แล้วพิจารณาว่าควรใช้ Strategy Pattern หรือไม่
  3. เปรียบเทียบ readability, coupling และจำนวนจุดที่ต้องแก้ไขก่อน–หลัง Refactoring

#สรุป

Code Smell เป็นสัญญาณให้ตรวจสอบคุณภาพการออกแบบ ส่วน Refactoring เป็นกระบวนการปรับโครงสร้างโดยรักษาพฤติกรรมเดิม แนวทางที่มีประสิทธิภาพคือ ทดสอบก่อน → ปรับทีละน้อย → ทดสอบซ้ำ → ตรวจทาน