การเขียนโปรแกรมที่ดี
การเขียนโปรแกรมหรือโคดดิ้งแต่ละคนย่อมมีสไตล์การเขียนที่ไม่เหมือนกัน มีวิธีคิดที่หลากหลายรวมถึงลำดับขั้นตอนการทำงานที่ไม่เหมือนกัน อาจเปรียบได้กับการเขียนบทประพันธ์ที่มีลิขสิทธิ์เฉพาะของแต่ละคน การดำเนินเรื่อง ลำดับเหตุการณ์ให้น่าสนใจเร้าอารมณ์ซึ่งนักประพันธ์แต่ละคนย่อมเขียนไม่เหมือนกันแม้จะเป็นเรื่องเดียวกันก็ตาม แต่การเขียนโปรแกรมหากจะเขียนไปเรื่อยๆ ตามจินตนาการ ไอเดียที่สร้างสรรค์ที่เกิดขึ้นโดยไร้หลักการอาจทำให้มีปัญหาตามมาในภายหลัง เพราะโปรแกรมอาจจะไม่ได้เขียนด้วยคนคนเดียว การทำงานเป็นทีมจึงจะต้องมีหลักการทำงานที่ตกลงร่วมกันเพื่อความเข้าใจไปในทิศทางเดียวกัน หรือแม้แต่โปรแกรมที่เขียนโค้ดด้วยคนเดียวก็ตามหากไม่มีการวางแผนที่ดีอาจเป็นเรื่องยากในการแก้ไขในภายหลัง แน่นอนว่าการจัดการกับสิ่งเหล่านี้มันใช้เวลาแต่มันคุ้มค่าที่จะทำและเมื่อเราทำจนเป็นนิสัยแล้วมันจะเร็วขึ้นและดีขึ้นเรื่อยๆ
ลองนึกภาพตาม วันหนึ่งตื่นเช้ามาพร้อมกับอากาศที่สดใสกับสมองที่ปลอดโปร่ง มานั่งจิบกาแฟหน้าคอมพิวเตอร์พร้อมกับความคิดที่บรรเจิดสามารถเขียนโค้ดได้เป็นร้อยๆบรรทัด และคอมไพล์ผ่านอย่างสบายๆ ปิดโปรเจ็คหนึ่งไปได้อย่างสวยงามไม่มีปัญหาใดๆ พอเวลาผ่านไปหนึ่งปี(หรืออาจแค่ไม่กี่วัน) ต้องการกลับมาเขียนเพิ่มเติมหรือแก้ไขบักเล็กน้อยที่เกิดขึ้นในโปรแกรมที่เราเขียนกลับเกิดอาการอีหยังวะ!! อ่านโค้ดที่เราเขียนเองไม่รู้เรื่อง จำไม่ได้ ไม่เข้าใจว่าเขียนอะไรลงไปทำให้เสียเวลาเป็นวันหรือหลายๆวันในการแกะเนื้อหาที่เขียนเพื่อแก้ไขเพียงเล็กน้อย หรือต้องมาแก้ไขสิ่งที่พัวพันกันไปหมดจนไม่รู้ว่าจะเริ่มต้นตรงไหนดีจนกลายเป็นวันที่เลยร้ายของการเขียนโค้ดขึ้นมาทันที ดังนั้นก่อนที่จะลงมือเขียนโปรแกรมจะต้องมีการกำหนดหลักการ การวางแผนที่ดีซึ่งก็แล้วแต่วิธีการของแต่ละคนเช่นกันหรือมีการตกลงกันในทีมให้ชัดเจน
ส่วนประกอบของการเขียนโค้ดที่ดี
การเขียนโค้ดที่ดีหมายถึงเขียนแล้วสามารถอ่านแล้วเข้าใจได้โดยง่ายสิ่งที่เขียนลงไปเพื่อวัตถุประสงค์อะไร แต่ก็เข้าใจได้ว่ามีโปรแกรมเมอร์บางคนที่ตั้งใจเขียนให้คนอื่นไม่เข้าใจเพราะกลัวการเอาไปใช้โดยไม่ได้รับอนุญาติแต่การทำเช่นนั้นก็จะแลกกับการที่เราอาจจะเป็นผู้ที่ได้รับผลกระทบเสียเองนั่นก็แล้วแต่แนวความคิดละกันซึ่งไม่อาจก้าวล่วงได้ การเขียนโค้ดที่ดีควรจะประกอบไปด้วยการจัดการสิ่งเหล่านี้
- Documentation – การเขียนอธิบายสิ่งที่เรากำลังจะทำหรือการเขียนคอมเม้นต์นั่นเอง
- Decomposition – แยกโปรแกรมออกเป็นส่วนๆ ทำงานเฉพาะอย่างไม่รวมทุกอย่างไว้ในฟังก์ชั่นเดียว
- Naming – การตั้งชื่อให้เหมาะสม เข้าใจง่าย ทั้งชื่อตัวแปร และชื่อฟังก์ชั่น
- Use of the language – การเลือกใช้ feature ที่มีในภาษาที่เราเขียนได้อย่างเหมาะสม
- Formatting – การจัดฟอร์แมทให้อ่านได้ง่าย
Documentation
การเขียนคอมเมนต์มีจุดประสงค์หลายอย่างแต่หลักๆก็คือ เป็นการเขียนเพื่ออธิบายสิ่งที่เรากำลังทำอยู่ว่าฟังก์ชั่นการทำงานในส่วนนี้ต้องการทำอะไร อะไรคือพารามิเตอร์ที่รับค่า อะไรคือค่าที่ส่งกลับมา และยกเว้นอะไร (Throw exception) ใครเป็นผู้เขียน เขียนตั้งแต่เมื่อไหร่ มีการแก้ไขเพิ่มเติมอะไรบ้าง เป็นต้น ซึ่งบางบริษัทอาจจะมีรูปแบบที่กำหนดไว้ชัดเจนว่าโปรแกรมเมอร์จะต้องเขียนคอมเมนต์ตามรูปแบบที่กำหนดไว้
ตัวอย่างการเขียนคอมเมนต์เพื่ออธิบายการทำงานของฟังก์ชั่น
/* * SaveRecord() * Saves the given record to the database. * * Parameter: Record& record * Record& record: the record to save to the database. * Returen: int * An integer representing the ID of the saved record. * Throws: * DatabaseNotOpenedException if the openDatabase() method has not * been called yet. */ int saveRecord(Record& record);
ตัวอย่างการอธิบายแต่ละบรรทัด
void sort(int inArray[], size_t inSize)
{
//Start at postion 1 and examin each element.
for (size_t i = 1; i < inSize; i++)
{ //Loop invariant:
// All elements in the range 0 to i-1 (inclusive) are sorted.
int element = inArray[i];
// j marks the position in the sorted part after which element
// will be inserted
size_t j = i - 1;
// As long as the current slot in the sorted array is higher than
// element, shift values to the right to make room for inserting
// (hence the name, "insertion sort") element in the right position.
while (j >= 0 && inArray[j] > element)
{
inArray[j + 1] = inArray[j];
j--;
}
// At this point the current position in the sorted array
// is *not* greater than the element, so this is its new position.
inArray[j + 1] = element;
}
for (size_t i = 0; i < inSize; i++)
{
// print out each value in the inArray
std::cout << inArray[i] << "\n";
}
}
//Comment inline
int ex1() // Get doodat Result and loging data
{
int result; // Declare an integer to hold the result
result = doodad.getResult(); // Get the doodad's result.
if (result % 2 == 0) // If the result modelate 2 is 0 ...
{
logError(); // Then log an error
}
else
{
logSuccess(); // Otherwise log successs.
}
return result; // Return the result.
}
การเขียนคอมเมนต์เพื่อบอกข้อมูล meta-information
/* * Author: thesolutions.tech * Date: 230426 * Feature: Example PG Version 1 * * Date |Change * ------------------------------------------------------ * 230501 | REQ #005:Do not normalize values. * 230608 | REQ #006: use nullptr instead of NULL. */
Decomposition
ควรเขียนแยกโปรแกรมออกเป็นส่วนๆ ไม่ควรรวมทุกฟังก์ชั่นไว้ใน Main ทั้งหมดเพราะจะทำให้ยากต่อการ debug หรือแก้ไขใดๆในอนาคต เทคนิคการแบ่งโค้ดฟังก์ชั่นเป็นส่วนย่อยๆอาจจะทำดังนี้
- วิธีกำหนดขอบเขตของงาน
- Encapsulate Field: กำหนดขอบเขตให้เป็น private แล้ว access ผ่านวิธีการ getter, setter
- Generalize Type: เพื่อที่จะสามารถแชร์โค้ดใช้ได้กับหลายๆส่วน
- วิธีแยกตามลอจิกส์การทำงาน
- Extract Method: แยกเป็น method ย่อยๆเพื่อให้ง่ายต่อความเข้าใจ
- Extract Class: แยกเป็น class ย่อยๆ
- วิธีแยกตามชื่อและตำแหน่ง (location)
Naming
การตั้งชื่อที่เหมาะสมให้กับ variable, method, function, parameter, class, namespace จะช่วยให้เราเข้าใจการทำงานของมันได้ง่ายขึ้นลดการเขียนอธิบายยาวๆไปได้ ใน C++ มีกฎการตั้งชื่ออยู่เล็กน้อยดังนี้
- ชื่อจะต้องไม่ขึ้นต้นด้วยตัวเลข
- ไม่ควรใช้ double underscore ในชื่อ (my__name)
- ไม่ควรขึ้นต้นด้วย underscore (_name, _Name)
การตั้งชื่อให้เหมาะสม
นอกเหนือจากกฎที่กำหนดไว้เราสามารถตั้งชื่ออย่างไรก็ได้ให้เราเข้าใจง่าย แต่อาจจะมีหลักการตั้งชื่อเล็กน้อยลองพิจารณาเทคนิคการตั้งชื่อดังนี้
- Counter: โดยปกติแล้วเวลาเขียนโค้ดวนลูปเราจะกำหนดตัวแปรนับเป็น i, j, k มันเข้าใจง่ายถ้าเป็นลูปที่ไม่ซับซ้อนและยากมาก แต่ถ้าเป็นลูปซ้อนลูปบางทีอาจทำให้เรางงได้ว่าตอนนี้ใช้ i ที่เท่าไหร่ ทำงานอยู่ในลูปไหนอยู่ เพื่อให้เข้าใจง่ายขึ้น ถ้าเป็นการทำงานกับข้อมูล 2 มิติ (2-D) อาจจะตั้งชื่อตัวแปรนับ (counter) เป็น row, column แทน i, j หรือตั้งชื่อเป็น outerLoopIndex/innerLoopIndex ก็ได้ ถ้ามีลูปเยอะๆ อาจตั้งชื่อตามหน้าที่แทนเช่น nameLoopIndex เป็นต้น
- Prefixs: การเพิ่ม prefix หน้าชื่อตัวแปรช่วยให้เราเข้าใจง่ายขึ้นว่าเป็นตัวแปรชนิดไหน
| PREFIX | NAME | MEANING | USAGE |
| m m_ | mData m_data | "member" | Data member within a class |
| s ms ms_ | sLookupTable msLookupTable ms_lookupTable | "static" | Static variable/data member |
| k | kMaximumLength | "konstant" (ภาษาเยอรมัน) "constant" | a constant value, บางคนอาจจะกำหนด ค่าคงที่ด้วยอักษรตัวพิมพ์ใหญ่ (uppercase) แทนก็ได้ |
| b is | bCompleted isCompleted | "Boolean" | Designates a Boolean value |
| n mNum | nLines mNumLines | "number" | ตัวเลขรวมถึง counter ด้วย |
- Getters and Setters: การเรียกใช้ข้อมูลใน class อาจจะใช้วิธี get/set data member ใน class ตัวอย่างเช่น getStatus() หรือ setStatus() เป็นต้น หรือถ้าข้อมูลเป็น Boolean อาจจะเพิ่ม prefix เข้าไปเช่น isRunning(), isConnected() เป็นต้น
- Capitalization: หลักการตั้งชื่อถ้าเป็นชื่อตัวแปรจะขึ้นต้นด้วยอักษรพิมพ์ตัวเล็ก และถ้าเป็น Function/Method จะขึ้นต้นด้วอักษรพิมพ์ตัวใหญ่ แต่บางที function ที่เรียกจาก class อาจขึ้นต้นด้วยตัวอักษรพิมพ์เล็กได้เช่นกัน เช่น getStatus() การเขียนชื่อตัวแปรที่ยาวๆเวลาอ่านบางทีไม่สะดวก ดังนั้นอาจจะขั้นคำด้วย underscore เช่น my_name หรืออาจจะแบ่งคำด้วยตัวอักษรพิมพ์ใหญ่ก็ได้เช่น myName เป็นต้น
- Namespace constants: ถ้าโปรแกรมมีหลายๆเมนูเช่น File, Edit, Help เราควรจะกำหนด ID ให้แต่ละเมนู เช่น เมนู Help กำหนด ID เป็น kHelp สำหรับการเรียกใช้ เช่นการเพิ่ม Button help ในโปรแกรม ID button ใช้ kHelp ได้เลย ซึ่งจะทำให้การเรียกใช้ในหลายๆ namespace สะดวกมากขึ้น เช่นถ้าหากต้องการเรียกใช้ Help ใน Menu Bar กับ Button สามารถใช้ Menu::kHelp และ Button::kHelp เป็นต้น นอกจากนี้เราสามารถกำหนดเป็น enum ไว้เลยก็ได้เช่น enum class Menu{File, Edit, Help};
ที่มา:
Marc Gregoire, Professional C++, Forth Edition, John Willey & Sons, 2018









