Code Smell คือตัวบ่งชี้ระดับพื้นผิวในโค้ดที่ส่งสัญญาณถึงปัญหาที่อาจเกิดขึ้นในการออกแบบหรือสถาปัตยกรรมของแอปพลิเคชัน คำนี้ถูกบัญญัติขึ้นโดย Kent Beck และทำให้เป็นที่นิยมโดย Martin Fowler ในหนังสือ “Refactoring: Improving the Design of Existing Code” ตามที่ Martin Fowler กล่าว กลิ่นของโค้ดไม่ได้หมายถึงบั๊กเสมอไป แต่เกือบทุกครั้งบ่งบอกถึงความจำเป็นในการรีแฟกเตอริงเพื่อปรับปรุงความสามารถในการบำรุงรักษา
ประเด็นสำคัญ
Code Smell คืออุปมาสำหรับอาการในซอร์สโค้ดที่มีความเป็นไปได้สูงที่จะบ่งชี้ถึงปัญหาที่ลึกซึ้งยิ่งขึ้น คำนี้ไม่มีคำจำกัดความที่เป็นทางการ — มันเป็นฮิวริสติกที่อิงตามประสบการณ์ของนักพัฒนา Martin Fowler และ Kent Beck จัดระบบกลิ่น 22 ชนิดเป็นครั้งแรกในปี 1999 ในหนังสือ “Refactoring” และส่วนใหญ่ยังคงมีความเกี่ยวข้องในหลายทศวรรษต่อมา
สิ่งสำคัญคือต้องเข้าใจความแตกต่างระหว่าง Code Smell และบั๊ก กลิ่นไม่ใช่ข้อผิดพลาด: โค้ดคอมไพล์ ทำงาน และให้ผลลัพธ์ที่ถูกต้อง ปัญหาคือโค้ดดังกล่าวอ่าน เปลี่ยนแปลง และทดสอบได้ยาก เมื่อเวลาผ่านไป ต้นทุนของการเปลี่ยนแปลงแต่ละครั้งเพิ่มขึ้นและความเชื่อมั่นในความถูกต้องของการรีแฟกเตอริงลดลง เครื่องมือวิเคราะห์แบบสแตติก (SonarQube, Detekt, SwiftLint) ตรวจจับกลิ่นมากมายโดยอัตโนมัติ
ลักษณะ ฮิวริสติก ของ Code Smell หมายความว่าไม่ใช่ทุกเมธอดที่ยาวจะต้องถูกแบ่ง และไม่ใช่ทุกคลาสขนาดใหญ่ที่ต้องการรีแฟกเตอริง การตัดสินใจทำโดยนักพัฒนาโดยประเมินบริบท: ความถี่ของการเปลี่ยนแปลง ความสำคัญของโมดูล แผนการพัฒนา วิศวกรที่มีประสบการณ์รับรู้กลิ่นได้โดยสัญชาตญาณ — โค้ด “มีกลิ่นเหม็น” แม้ว่ากฎระเบียบทั้งหมดจะถูกปฏิบัติตาม
Fowler ระบุกลิ่น 22 ชนิดที่แบ่งออกเป็นหลายหมวดหมู่ สำหรับการพัฒนาแอปพลิเคชันมือถือ กลิ่นที่เกี่ยวข้องมากที่สุดคือกลิ่นเชิงโครงสร้าง กลิ่นการออกแบบเชิงวัตถุ และปัญหาเฉพาะที่เกี่ยวข้องกับข้อจำกัดของแพลตฟอร์ม มาสำรวจแต่ละกลุ่มพร้อมตัวอย่างจริง
Long Method เป็นกลิ่นที่พบบ่อยที่สุดในแอปพลิเคชันมือถือ หน้าจอฟอร์มลงทะเบียนมักมีเมธอด setupUI เพียงเมธอดเดียวที่ยาว 200+ บรรทัด ซึ่งสร้าง Views ทั้งหมด ตั้งค่าข้อจำกัด สมัครรับอีเวนต์ และจัดการข้อผิดพลาด วิธีแก้ไข: แบ่งเป็นเมธอดตามบล็อกตรรกะ — configureEmailField, configurePasswordField, setupConstraints, bindViewModel
Large Class — Activity หรือ ViewController ที่รับผิดชอบทั้งการแสดงผล การนำทาง ตรรกะทางธุรกิจ และการโต้ตอบเครือข่ายทั้งหมดพร้อมกัน คลาสดังกล่าวละเมิดหลักการความรับผิดชอบเดียวและประกอบด้วยฟิลด์และเมธอดหลายสิบรายการ ใน Android มักเป็น Fragment ที่มี 1000+ บรรทัดซึ่งมีตรรกะของหน้าจอต่าง ๆ วิธีแก้ไข: แยก presenter/ViewModel ย้ายโค้ดเครือข่ายไปยัง repository และการนำทางไปยัง coordinator
Duplicate Code — การคัดลอกบล็อกที่เหมือนกันในส่วนต่าง ๆ ของแอปพลิเคชัน ตัวอย่างทั่วไป: สองหน้าจอแสดงการ์ดสินค้า — ในแคตตาล็อกและในรายการโปรด หากตรรกะการแสดงผลถูกคัดลอก การแก้ไขบั๊กในที่หนึ่งจะไม่แก้ไขในอีกที่หนึ่ง วิธีแก้ไข: แยกตรรกะทั่วไปไปยังคอมโพเนนต์หรือส่วนขยายที่นำกลับมาใช้ใหม่ได้
Feature Envy — เมธอดของคลาสหนึ่งใช้ข้อมูลของอีกคลาสหนึ่งอย่างมาก ใน Android สิ่งนี้ปรากฏเมื่อ ViewModel เข้าถึงฟิลด์ของโมเดล User โดยตรงแทนที่จะเรียกเมธอดของโมเดล สัญญาณ: ถ้าเมธอดสามารถย้ายไปยังคลาสที่ใช้ข้อมูลได้ — ให้ย้าย Switch Statements (ลูกโซ่เงื่อนไข) — โครงสร้าง switch หรือลูกโซ่ if-else ที่ตรวจสอบชนิดของออบเจ็กต์ ให้ใช้พหุรูปหรือแพทเทิร์น strategy แทน
Data Class — คลาสที่เก็บข้อมูลเท่านั้นแต่ไม่มีพฤติกรรม data class ใน Kotlin หรือโครงสร้างใน Swift ไม่ใช่กลิ่นในตัวเอง ปัญหาเกิดขึ้นเมื่อตรรกะทางธุรกิจที่ทำงานกับข้อมูลนั้นกระจายอยู่ทั่วโค้ดเบสแทนที่จะถูกห่อหุ้ม Refused Bequest — คลาสย่อยไม่ใช้เมธอดส่วนใหญ่ของคลาสแม่และแทนที่ด้วยสตับว่าง สัญญาณของการสืบทอดที่ไม่ถูกต้อง: แทนที่การสืบทอดด้วยองค์ประกอบ
God Activity / God Fragment — Activity หรือ Fragment ที่รู้ทุกอย่าง: วงจรชีวิต ข้อมูล การนำทาง สิทธิ์ DI นี่คือคลาสที่มีค่าใช้จ่ายสูงที่สุดในการบำรุงรักษาในแอปพลิเคชัน วิธีแก้ไข: แพทเทิร์นสถาปัตยกรรม MVVM, MVI หรือ Clean Architecture แยกความรับผิดชอบ Giant ViewController — สิ่งที่เทียบเท่าใน iOS ที่ UIViewController มีตรรกะทั้งหมดของหน้าจอและมักเกิน 500 บรรทัด
Hardcoded Resources — สตริง สี ขนาด URL API ที่ฝังอยู่ในโค้ดโดยตรง ในAndroid สิ่งนี้ละเมิดระบบทรัพยากร R; ใน iOS ละเมิด NSLocalizedString และ Asset Catalog การแก้ไข: ย้ายสตริงทั้งหมดไปยัง strings.xml หรือ Localizable.strings, URL ไปยังไฟล์คอนฟิก, ขนาดไปยัง dimens Leaking Context — การเก็บอ้างอิงไปยัง Activity หรือ ViewController นานกว่าอายุของคอมโพเนนต์เอง ทำให้เกิดหน่วยความจำรั่วและแครช วิธีแก้ไข: การอ้างอิงแบบอ่อน, Jetpack Lifecycle, RxSwift DisposeBag
| กลิ่น | เกิดขึ้นที่ไหน | วิธีแก้ไข |
|---|---|---|
| Long Method | Android/iOS | Extract Method, การแบ่ง |
| Large Class | Activity, ViewController | MVVM, VIPER, Clean Arch |
| Duplicate Code | หน้าจอใดก็ได้ | คอมโพเนนต์ที่ใช้ร่วมกัน, DRY |
| Feature Envy | ViewModel, Presenter | Move Method |
| Leaking Context | Android | คอมโพเนนต์ Lifecycle-aware |
การตรวจสอบโค้ด เป็นวิธีที่เชื่อถือได้มากที่สุดในการตรวจจับกลิ่น สายตามนุษย์สังเกตเห็นโครงสร้างที่ไม่เป็นธรรมชาติซึ่งเครื่องมือวิเคราะห์อัตโนมัติพลาด ประสิทธิภาพของการตรวจสอบโค้ดดีขึ้นเมื่อทีมใช้รายการตรวจสอบกลิ่นทั่วไป แนะนำให้ตรวจสอบโค้ดไม่เกิน 200–400 บรรทัดต่อครั้ง — หลังจากเกณฑ์นี้ ความสนใจลดลงและกลิ่นเริ่มถูกมองข้าม
การวิเคราะห์แบบสแตติก ทำให้การค้นหากลิ่นเชิงโครงสร้างเป็นอัตโนมัติ สำหรับ Android เครื่องมือมาตรฐานคือ Detekt (Kotlin) และ Android Lint; สำหรับ iOS คือ SwiftLint และ SonarQube เครื่องมือเหล่านี้พบเมธอดที่ยาว คลาสขนาดใหญ่ โค้ดที่ซ้ำกัน และปัญหาอื่น ๆ อีกมากมาย สิ่งสำคัญคือต้องปรับกฎให้เหมาะสมกับโปรเจกต์ — การกำหนดค่าเริ่มต้นมักเข้มงวดเกินไปหรือตรงกันข้าม มองข้ามกลิ่นที่สำคัญ
เมตริกซ์โค้ด ให้เกณฑ์วัตถุประสงค์: ความซับซ้อนของไซโคลเมติก (เกณฑ์ >10 ต้องการความสนใจ), จำนวนบรรทัดโค้ดต่อเมธอด (เกณฑ์ >30), ความลึกของการสืบทอด (>3 — เหตุผลที่ต้องคิด) เครื่องมืออย่าง CodeMetrics (Xcode) และ Gradle Metrics Plugin สร้างกราฟการเปลี่ยนแปลงเมตริกซ์เมื่อเวลาผ่านไป หากความซับซ้อนของเมธอดเพิ่มขึ้นจาก 5 เป็น 15 หลังจาก commit ล่าสุด — นั่นคือสัญญาณสำหรับรีแฟกเตอริง
// ตัวอย่าง: เมธอดที่มีความซับซ้อนแบบ Cyclomatic = 7 (เกินเกณฑ์ 5)
fun processOrder(order: Order) {
if (order.status == Status.NEW) { /* 10 บรรทัด */ }
else if (order.status == Status.PAID) { /* 15 บรรทัด */ }
else if (order.status == Status.SHIPPED) { /* 20 บรรทัด */ }
else if (order.status == Status.DELIVERED) { /* 8 บรรทัด */ }
else if (order.status == Status.CANCELLED) { /* 5 บรรทัด */ }
else { throw IllegalStateException() }
}
// วิธีแก้ไข: พหุรูปแทน switch
interface OrderHandler {
fun handle(order: Order)
}
การตรวจจับกลิ่น อัตโนมัติ ไม่ได้แทนที่การตรวจสอบโค้ด: เครื่องมือวิเคราะห์แบบสแตติกพบเพียงปัญหาเชิงโครงสร้างแต่ไม่สามารถจับกลิ่นเชิงความหมาย (Feature Envy, Inappropriate Intimacy) การรวมกันของเครื่องมืออัตโนมัติและการตรวจสอบโดยมนุษย์ให้ผลลัพธ์ที่ดีที่สุด กำหนดค่าไปป์ไลน์ CI/CD ของคุณให้บิลด์ล้มเหลวเมื่อเกินเกณฑ์ความซับซ้อนหรือความยาวของเมธอด
รีแฟกเตอริง เป็นวิธีการหลักในการกำจัดกลิ่นของโค้ด Fowler อธิบายเทคนิครีแฟกเตอริงหลายสิบแบบ ซึ่งแต่ละแบบใช้ได้กับกลิ่นเฉพาะ Extract Method — สำหรับเมธอดที่ยาว, Extract Class — สำหรับคลาสขนาดใหญ่, Move Method — สำหรับ Feature Envy สิ่งสำคัญคือต้องดำเนินการรีแฟกเตอริงเป็นขั้นตอนเล็ก ๆ โดยรักษาให้โค้ดทำงานได้หลังการเปลี่ยนแปลงแต่ละครั้ง
การทดสอบก่อนรีแฟกเตอริง เป็นข้อบังคับ หากโค้ดไม่ครอบคลุมโดยการทดสอบหน่วย รีแฟกเตอริงจะกลายเป็นการเขียนใหม่โดยไม่ทราบผลลัพธ์ สำหรับโค้ดเดิมที่ไม่มีทดสอบ ให้ใช้ Characterization Tests — เขียนทดสอบที่บันทึกพฤติกรรมปัจจุบัน จากนั้นรีแฟกเตอร์ การทดสอบให้ความมั่นใจว่าตรรกะทางธุรกิจไม่เสียหายหลังรีแฟกเตอริง
ความค่อยเป็นค่อยไป คือกุญแจสู่ความสำเร็จในการแก้ไขกลิ่นในการพัฒนาแอปพลิเคชันมือถือ อย่าพยายามเขียน God Activity ใหม่ทั้งหมด ขั้นแรกให้แยกชั้นการนำทาง จากนั้นชั้นข้อมูล จากนั้นตรรกะการแสดงผล แต่ละขั้นตอนควรมาพร้อมกับ commit และการรันทดสอบ ใช้ feature toggle เพื่อเปิดใช้งานรีแฟกเตอริงสำหรับผู้ใช้กลุ่มย่อยและย้อนกลับหากเกิดปัญหา
เครื่องมือ IDE ทำให้เทคนิครีแฟกเตอริงหลายอย่างเป็นอัตโนมัติ Android Studio และ IntelliJ IDEA มีรีแฟกเตอริงในตัว: Extract Method, Extract Interface, Pull Members Up, Encapsulate Fields Xcode (เริ่มตั้งแต่เวอร์ชัน 14) ปรับปรุงการรองรับรีแฟกเตอริงสำหรับ Swift การใช้รีแฟกเตอริงอัตโนมัติช่วยลดความเสี่ยงของข้อผิดพลาดเมื่อเทียบกับการคัดลอกโค้ดด้วยตนเอง
การพัฒนาแอปพลิเคชันมือถือ เพิ่มกลิ่นเฉพาะของตัวเองที่เกี่ยวข้องกับข้อจำกัดของแพลตฟอร์ม ใน Android รวมถึงการรั่วไหลของ Context, Cursor ที่ไม่ได้ปิด และการใช้ Lifecycle ที่ไม่เหมาะสม ใน iOS รวมถึง retain cycle ผ่าน closure, การจัดการ Auto Layout ที่ไม่ถูกต้อง และ ViewController ขนาดใหญ่ กลิ่นเหล่านี้ไม่เพียงแค่ทำให้ความสามารถในการบำรุงรักษาลดลง แต่ยังส่งผลโดยตรงต่อประสิทธิภาพและความเสถียรของแอปพลิเคชัน
Callback Hell เป็นกลิ่นเฉพาะของโค้ดที่ทำงานกับการดำเนินการแบบอะซิงโครนัส Callback ที่ซ้อนกันทำให้โค้ดอ่านไม่ได้และดีบักยาก วิธีแก้ไข: coroutines (Kotlin), async/await (Swift 5.5+), RxJava/RxSwift หรือ Combine ตามข้อมูลจาก Google I/O 2023 โปรเจกต์ที่เปลี่ยนจากสไตล์ callback เป็น coroutines ลดจำนวนบั๊กลง 30% และเร่งการเพิ่มฟีเจอร์ใหม่
Platform Coupling — การผูกตรรกะทางธุรกิจกับคอมโพเนนต์ของแพลตฟอร์มอย่างแน่นหนา การทดสอบตรรกะดังกล่าวต้องเปิดอีมูเลเตอร์ ซึ่งทำให้วงจรป้อนกลับช้าลง การแก้ไข: Clean Architecture แยกโค้ดเป็นชั้น Domain (Kotlin/Swift บริสุทธิ์ที่ไม่มี dependencies ของแพลตฟอร์ม) และ Data/UI (ที่มี dependencies ของแพลตฟอร์ม) ตรรกะทางธุรกิจถูกทดสอบบน JVM โดยไม่มีอีมูเลเตอร์
คำถามที่พบบ่อย
ไม่ — Code Smell ไม่ใช่ข้อผิดพลาด โค้ดที่มีกลิ่นทำงานได้อย่างถูกต้อง แต่ยากต่อการบำรุงรักษา เปลี่ยนแปลง และทดสอบ บั๊กคือพฤติกรรมที่ไม่ถูกต้อง; กลิ่นคือคำเตือนเกี่ยวกับปัญหาที่อาจเกิดขึ้นในอนาคต
22 กลิ่น ในหนังสือ “Refactoring” ฉบับที่สอง (2019) รวมถึง Long Method, Large Class, Primitive Obsession, Data Clumps, Switch Statements, Speculative Generality และอื่น ๆ ชุมชนได้เพิ่มกลิ่นใหม่อีกหลายสิบชนิดสำหรับกระบวนทัศน์และแพลตฟอร์มสมัยใหม่
การรวมกัน ให้ผลลัพธ์ที่ดีที่สุด: Detekt (Android/Kotlin), SwiftLint (iOS), SonarQube (ทั้งคู่) สำหรับการวิเคราะห์อัตโนมัติ และการตรวจสอบโค้ดสำหรับกลิ่นเชิงความหมาย ไม่มีเครื่องมือใดค้นพบปัญหา 100% — ประสบการณ์ของมนุษย์ยังคงเป็นปัจจัยชี้ขาด
ได้ หากโค้ดเปลี่ยนแปลงน้อยครั้งหรือจะถูกเขียนใหม่ทั้งหมดในเร็ว ๆ นี้ อย่างไรก็ตาม การสะสมของกลิ่นกลายเป็นหนี้ทางเทคนิค: การเปลี่ยนแปลงใหม่แต่ละครั้งยากขึ้นเรื่อย ๆ และต้นทุนการแก้ไขเพิ่มขึ้นแบบทวีคูณ
ใช่ — เฟรมเวิร์กแบบ Declarative ก่อให้เกิดกลิ่นใหม่: บล็อก @State ขนาดใหญ่ การจัดการเรนเดอร์ซ้ำที่ไม่ถูกต้องการประกอบใหม่มากเกินไป และขาดการแยกเป็น Views แต่ละส่วน สำหรับ SwiftUI กลิ่นทั่วไปคือ Massive View ที่มีตัวแปร @State หลายสิบตัว
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม