Debugging คือกระบวนการค้นหา วิเคราะห์ และแก้ไขข้อผิดพลาดของโปรแกรมอย่างมีหลักฐาน เป้าหมายไม่ใช่แค่ทำให้ error หายไป แต่ต้องเข้าใจว่า error เกิดจากอะไร เกิดในเงื่อนไขใด และการแก้ไขจะไม่ทำให้ส่วนอื่นเสียหาย
ผู้เริ่มต้นมักแก้ Bug ด้วยการเดา เช่น เปลี่ยนเงื่อนไข เพิ่มตัวแปร หรือลบโค้ดบางบรรทัดโดยไม่เข้าใจสาเหตุ วิธีนี้อาจทำให้โปรแกรมดูเหมือนทำงานได้ชั่วคราว แต่มีโอกาสสร้าง Bug ใหม่ การ Debugging ที่ดีต้องเริ่มจากการทำให้ปัญหาเกิดซ้ำได้ก่อน แล้วค่อยตรวจค่าที่เกี่ยวข้องทีละจุด
flowchart TD
A[พบอาการผิดปกติ] --> B[Reproduce<br/>ทำให้ปัญหาเกิดซ้ำได้]
B --> C[Observe<br/>ดู input, output, error message]
C --> D[Hypothesize<br/>ตั้งสมมติฐานสาเหตุ]
D --> E[Inspect<br/>ใช้ breakpoint, watch, call stack]
E --> F{พบสาเหตุจริงหรือไม่}
F -- ไม่พบ --> C
F -- พบ --> G[Fix<br/>แก้เฉพาะสาเหตุ]
G --> H[Verify<br/>ทดสอบซ้ำ]
H --> I[Add Test<br/>เพิ่ม test case กันปัญหาซ้ำ]
เครื่องมือหลักที่ใช้ระหว่าง Debugging:
| เครื่องมือ | ใช้ทำอะไร | ตัวอย่างสถานการณ์ |
|---|---|---|
| Breakpoint | หยุดโปรแกรมที่บรรทัดสำคัญ | หยุดก่อนคำนวณส่วนลดเพื่อตรวจค่า input |
| Step Over | รันคำสั่งถัดไปโดยไม่เข้าไปในฟังก์ชันย่อย | ต้องการดู flow หลักอย่างรวดเร็ว |
| Step Into | เข้าไปตรวจการทำงานในฟังก์ชันที่ถูกเรียก | สงสัยว่าฟังก์ชันคำนวณให้ค่าผิด |
| Watch | ติดตามค่าตัวแปรหรือนิพจน์ | ดูว่าค่า total เปลี่ยนตอนไหน |
| Call Stack | ดูลำดับการเรียกฟังก์ชัน | ตรวจว่า error ถูกเรียกมาจาก flow ใด |
ตัวอย่างสถานการณ์:
double average(int total, int count) {
return total / count;
}
โค้ดนี้อาจเกิด Runtime Error หรือผลลัพธ์ผิดได้ หาก count เป็น 0 หรือหากต้องการผลลัพธ์ทศนิยมแต่ใช้การหารแบบจำนวนเต็ม Debugger จะช่วยดูค่าของ total และ count ณ เวลาที่ฟังก์ชันถูกเรียก ส่วน Test Case จะช่วยยืนยันว่ากรณี count = 0 ถูกจัดการแล้ว
เมื่อได้รับรายงานปัญหา ผู้พัฒนาควรแยก "อาการ" ออกจาก "สาเหตุ" เช่น ผู้ใช้บอกว่าโปรแกรมค้าง นั่นเป็นอาการ แต่สาเหตุอาจมาจาก loop ไม่สิ้นสุด การอ่านไฟล์ขนาดใหญ่ หรือการรอ network โดยไม่มี timeout
flowchart LR
A[Bug Report] --> B[อาการที่เห็น]
B --> C[ข้อมูลประกอบ<br/>input, log, screen, step]
C --> D[สมมติฐาน]
D --> E[ตรวจด้วย Debugger]
E --> F[Root Cause]
F --> G[Fix + Test]
ข้อมูลที่ควรบันทึกเมื่อพบ Bug:
Testing คือการตรวจสอบว่าโปรแกรมทำงานตรงตามความต้องการและยังทำงานถูกต้องเมื่อมีการเปลี่ยนแปลง การทดสอบที่ดีต้องมีกรณีปกติ กรณีขอบเขต และกรณีผิดพลาด ไม่ควรทดสอบเฉพาะข้อมูลที่ผู้เขียนคาดว่าจะถูกต้องเท่านั้น
ประเภทการทดสอบที่สำคัญ:
| ประเภท | ตรวจอะไร | ตัวอย่าง |
|---|---|---|
| Unit Test | ฟังก์ชันหรือโมดูลย่อย | ทดสอบฟังก์ชันคำนวณภาษี |
| Integration Test | การทำงานร่วมกันของหลายส่วน | ทดสอบการอ่านไฟล์แล้วส่งข้อมูลไปคำนวณ |
| System Test | ระบบทั้งชุดตาม flow ของผู้ใช้ | ทดสอบตั้งแต่รับข้อมูล ประมวลผล และแสดงผล |
| Regression Test | ตรวจว่าของเดิมไม่เสียหลังแก้ไข | รัน test เดิมหลังแก้ Bug |
flowchart TB
A[System Test<br/>ตรวจทั้งระบบ]
B[Integration Test<br/>ตรวจหลายโมดูลทำงานร่วมกัน]
C[Unit Test<br/>ตรวจฟังก์ชันหรือโมดูลย่อย]
A --> B --> C
ในทางปฏิบัติ Unit Test ควรมีจำนวนมากที่สุด เพราะรันเร็วและบอกตำแหน่งปัญหาได้ชัดเจน Integration Test และ System Test มีจำนวนน้อยกว่าแต่ช่วยยืนยันว่าภาพรวมของระบบทำงานถูกต้อง
Test Case ที่ดีควรระบุ input, expected output, ขั้นตอนทดสอบ และผลลัพธ์จริง เพื่อให้ผู้อื่นตรวจซ้ำได้ การเขียน Test Case ไม่ใช่เพียงงานหลังเขียนโค้ดเสร็จ แต่ช่วยให้ผู้พัฒนาเข้าใจ requirement ชัดขึ้นตั้งแต่ต้น
ตัวอย่าง Test Case สำหรับฟังก์ชันคำนวณส่วนลด:
| กรณี | Input | Expected Output | เหตุผล |
|---|---|---|---|
| Normal case | 1500 | 75 | มากกว่า 1000 ได้ลด 5% |
| Boundary case | 1000 | 50 | ครบ 1000 ต้องได้ลด |
| Below boundary | 999 | 0 | ยังไม่ครบเงื่อนไข |
| Invalid case | -100 | Error หรือ 0 | ราคาไม่ควรติดลบ |
| Zero case | 0 | 0 | ไม่มีการซื้อ |
รูปแบบการคิด Test Case:
mindmap
root((Test Case Design))
Normal Case
ข้อมูลถูกต้องทั่วไป
Boundary Case
ค่าต่ำสุด
ค่าสูงสุด
จุดเปลี่ยนเงื่อนไข
Invalid Case
ค่าว่าง
ชนิดข้อมูลผิด
ค่าติดลบ
Regression Case
กรณีที่เคยเกิด Bug
ฟีเจอร์เดิมที่ต้องไม่เสีย
Refactoring คือการปรับโครงสร้างโค้ดโดยไม่เปลี่ยนพฤติกรรมภายนอกของโปรแกรม เช่น แยกฟังก์ชัน ลดโค้ดซ้ำ เปลี่ยนชื่อให้สื่อความหมาย หรือจัดโมดูลใหม่ เป้าหมายคือทำให้โค้ดอ่านง่าย ทดสอบง่าย และแก้ไขง่ายขึ้น
Refactoring ควรทำหลังจากมีหลักฐานว่าโปรแกรมทำงานถูกต้องแล้ว เช่น มี Test Case หรือผลทดสอบก่อนหน้า เพราะหากปรับโครงสร้างโดยไม่มี test จะไม่รู้ว่าการปรับนั้นทำให้พฤติกรรมเดิมเปลี่ยนหรือไม่
flowchart LR
A[Code ทำงานได้] --> B[มี Test Case รองรับ]
B --> C[Refactor ทีละจุด]
C --> D[Run Test]
D --> E{ผลลัพธ์ยังเหมือนเดิมหรือไม่}
E -- ใช่ --> F[Commit / บันทึกการเปลี่ยนแปลง]
E -- ไม่ใช่ --> G[ย้อนตรวจจุดที่เพิ่งแก้]
G --> C
ตัวอย่างสัญญาณว่าโค้ดควรถูก Refactor:
| สัญญาณ | ผลกระทบ | แนวทางปรับ |
|---|---|---|
| ฟังก์ชันยาวมาก | อ่านและทดสอบยาก | Extract Function |
| โค้ดซ้ำหลายจุด | แก้จุดหนึ่งแล้วลืมอีกจุด | Create Reusable Function |
| ชื่อตัวแปรไม่สื่อ | อ่านแล้วเข้าใจยาก | Rename Variable |
| เงื่อนไขซ้อนหลายชั้น | เสี่ยงต่อ logic error | Guard Clause หรือแยกฟังก์ชัน |
| โมดูลทำหลายหน้าที่ | แก้แล้วกระทบหลายส่วน | Split Module |
Source Code Maintenance คือการดูแลโค้ดให้พร้อมต่อการพัฒนาระยะยาว ครอบคลุมการจัดโครงสร้างไฟล์ การตั้งชื่อ การเขียนเอกสาร การจัดการ dependency การติดตาม issue และการควบคุมเวอร์ชัน
flowchart TD
A[Source Code Maintenance] --> B[Code Organization]
A --> C[Documentation]
A --> D[Dependency Management]
A --> E[Issue Tracking]
A --> F[Version Control]
B --> B1[แยกไฟล์และโมดูลตามหน้าที่]
C --> C1[README, comment ที่จำเป็น]
D --> D1[บันทึก library และ version]
E --> E1[บันทึก Bug และงานที่ต้องแก้]
F --> F1[commit เป็นช่วงที่ตรวจสอบได้]
โค้ดที่ดูแลได้ดีควรตอบคำถามเหล่านี้ได้:
ใช้ Debugger ตรวจโปรแกรมที่ทำงานผิด จากนั้นเขียน Test Case และปรับโครงสร้างโค้ดบางส่วนให้ดูแลรักษาง่ายขึ้น
ผู้เรียนฝึกไล่การทำงานของโปรแกรมจากอาการผิดปกติไปยังสาเหตุจริง โดยใช้ Breakpoint เพื่อหยุดโปรแกรมในจุดสำคัญ ใช้ Watch ตรวจค่าตัวแปร และใช้ Call Stack ดูลำดับการเรียกฟังก์ชัน หลังจากพบสาเหตุแล้วต้องอธิบายได้ว่าข้อผิดพลาดเกิดจากข้อมูลเข้า เงื่อนไข ลำดับคำสั่ง หรือการออกแบบฟังก์ชัน
ในส่วน Testing ผู้เรียนออกแบบ Test Case ทั้งกรณีปกติ กรณีขอบเขต และกรณีผิดพลาด แล้วเชื่อมโยงกับ Regression Test เพื่อป้องกันไม่ให้การแก้ไขครั้งใหม่ทำให้ความสามารถเดิมเสียหาย
เขียน Test Case อย่างน้อย 6 กรณีสำหรับฟังก์ชันคำนวณหรือประมวลผลข้อมูล พร้อมบันทึกผลก่อนและหลังแก้ Bug
แนวคิดหลัก (Key Concept): การแก้ Bug ที่ดีต้องเริ่มจากหลักฐาน ไม่ใช่การเดา ผู้เรียนต้องเชื่อมโยง อาการ (Symptom) ไปสู่ สาเหตุจริง (Root Cause) แล้วเพิ่ม Regression Test เพื่อป้องกันปัญหาซ้ำ
%%{init: {'theme': 'base', 'themeVariables': {
'background': '#282828',
'primaryColor': '#3c3836',
'primaryTextColor': '#fbf1c7',
'primaryBorderColor': '#fabd2f',
'lineColor': '#83a598',
'secondaryColor': '#504945',
'tertiaryColor': '#665c54',
'fontFamily': 'Arial'
}}}%%
flowchart LR
A[Symptom
อาการผิดปกติ] --> B[Reproduce
ทำซ้ำได้]
B --> C[Debug
ตรวจด้วยเครื่องมือ]
C --> D[Root Cause
สาเหตุจริง]
D --> E[Fix
แก้ไข]
E --> F[Regression Test
ทดสอบกันปัญหาซ้ำ]
#include <iostream>
using namespace std;
// ฟังก์ชันนี้ป้องกันการหารด้วยศูนย์ก่อนคำนวณค่าเฉลี่ย
double average(double total, int count) {
if (count <= 0) {
return 0; // invalid case: ไม่มีจำนวนข้อมูลให้หาร
}
return total / count;
}
int main() {
// ตัวอย่างการใช้งานและใช้เป็น test case เบื้องต้น
cout << average(100, 5) << endl; // expected: 20
cout << average(100, 0) << endl; // expected: 0
return 0;
}
| รายการตรวจ | คำถามสำหรับผู้เรียน |
|---|---|
| Reproduce | ทำให้ Bug เกิดซ้ำได้หรือไม่ |
| Debugger | ใช้ Breakpoint, Watch หรือ Call Stack จุดใด |
| Test Case | มี normal, boundary, invalid และ regression case หรือไม่ |
| Refactor | ปรับโค้ดแล้วผลลัพธ์เดิมยังถูกต้องหรือไม่ |