- Code Smell คืออะไร?
- ตัวอย่าง Code Smells ที่พบบ่อย
- Workshop 1: Long Method → Extract Method
- Workshop 2: Duplicate Code → Extract Method
- Workshop 3: Long Parameter List → Parameter Object
- ตรวจสอบด้วย Unit Test (JUnit 5)
- ขั้นตอน Refactoring อย่างปลอดภัย
- เครื่องมือที่ช่วยตรวจจับ
- แบบฝึกหัด
- สรุป
#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 อย่างปลอดภัย
- ระบุ Code Smell และปัญหาที่เกิดจริง เช่น แก้ไขยากหรือทดสอบยาก
- เขียน Characterization Tests เพื่อบันทึกพฤติกรรมปัจจุบัน รวมถึงกรณีขอบ
- เปลี่ยนโค้ดทีละขั้นเล็ก ๆ โดยไม่เปลี่ยนความสามารถภายนอก
- รัน Unit Tests และ Integration Tests หลังแต่ละขั้น
- ตรวจสอบ diff, static analysis และ code review
- แยกการเปลี่ยนพฤติกรรม (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,000 และรายการว่าง
- สร้างคลาสที่มี
switchตามประเภทการชำระเงิน แล้วพิจารณาว่าควรใช้ Strategy Pattern หรือไม่ - เปรียบเทียบ readability, coupling และจำนวนจุดที่ต้องแก้ไขก่อน–หลัง Refactoring
#สรุป
Code Smell เป็นสัญญาณให้ตรวจสอบคุณภาพการออกแบบ ส่วน Refactoring เป็นกระบวนการปรับโครงสร้างโดยรักษาพฤติกรรมเดิม แนวทางที่มีประสิทธิภาพคือ ทดสอบก่อน → ปรับทีละน้อย → ทดสอบซ้ำ → ตรวจทาน