เหตุผลที่ต้องเขียน comment ก็คือคำสั่งในภาษาการเขียนโปรแกรมไม่สามารถเก็บข้อมูลสำคัญทั้งหมดที่อยู่ในความคิดของนักพัฒนาในตอนที่เขียนโค้ดได้ Comment จะบันทึกข้อมูลเหล่านี้เอาไว้ เพื่อให้นักพัฒนาที่เข้ามาทำงานทีหลังสามารถเข้าใจและแก้ไขโค้ดได้ง่ายขึ้น หลักการสำคัญของ comment คือ comment ควรอธิบายสิ่งที่เห็นได้ไม่ชัดเจนจากโค้ด
มีหลายสิ่งที่ไม่ชัดเจนจากโค้ด บางครั้งก็เป็นรายละเอียดระดับต่ำที่ไม่ชัดเจน ตัวอย่างเช่น เมื่อคู่ของ index ใช้อธิบายช่วงของค่า ก็ไม่ชัดเจนว่าองค์ประกอบที่ index ชี้ถึงนั้นอยู่ภายในช่วงหรือนอกช่วง บางครั้งก็ไม่ชัดเจนว่าโค้ดนั้นจำเป็นหรือไม่ หรือทำไมถึงถูก implement ในรูปแบบนั้น บางครั้งก็มีกฎที่นักพัฒนาใช้ เช่น "ต้องเรียกใช้ a ก่อน b เสมอ" คุณอาจจะเดากฎได้จากการดูโค้ดทั้งหมด แต่มันก็เจ็บปวดและผิดพลาดได้ง่าย comment สามารถทำให้กฎชัดเจนและแจ่มแจ้งได้
หนึ่งในเหตุผลที่สำคัญที่สุดของ comment คือ abstraction ซึ่งมีข้อมูลมากมายที่ไม่ชัดเจนจากโค้ด แนวคิดของ abstraction คือการให้วิธีคิดที่ง่ายเกี่ยวกับบางสิ่งบางอย่าง แต่โค้ดนั้นมีรายละเอียดมากจนอาจมองเห็น abstraction ได้ยากจากการอ่านโค้ดเพียงอย่างเดียว Comment สามารถให้มุมมองที่ง่ายกว่าและมีระดับสูงกว่า ("หลังจากเรียกใช้งาน method นี้แล้ว ปริมาณ traffic ของเครือข่ายจะถูกจำกัดไว้ที่ maxBandwidth ไบต์ต่อวินาที") แม้ว่าข้อมูลนี้จะสามารถสรุปได้จากการอ่านโค้ด แต่เราไม่ต้องการบังคับให้ผู้ใช้ module ต้องทำแบบนั้น เพราะการอ่านโค้ดใช้เวลานานและบังคับให้พวกเขาต้องพิจารณาข้อมูลจำนวนมากที่ไม่จำเป็นต้องใช้เพื่อ ใช้งาน module นั้น นักพัฒนาควรสามารถเข้าใจ abstraction ที่ module จัดเตรียมไว้ให้ได้โดยไม่ต้องอ่านโค้ดอื่นใดนอกเหนือจาก declaration ที่มองเห็นจากภายนอก วิธีเดียวที่จะทำได้คือการเสริม declaration ด้วย comment
บทนี้จะพูดถึงว่าข้อมูลใดบ้างที่ต้องอธิบายใน comment และวิธีเขียน comment ที่ดี อย่างที่คุณจะเห็น comment ที่ดีมักจะอธิบายสิ่งต่างๆ ในระดับรายละเอียดที่แตกต่างจากโค้ด ซึ่งในบางสถานการณ์ก็มีรายละเอียดมากกว่า และในบางสถานการณ์ก็มีรายละเอียดน้อยกว่า (เป็นนามธรรมมากกว่า)
13.1 Pick conventions (เลือกกำหนดแนวปฏิบัติ)
ขั้นตอนแรกในการเขียน comment คือการตัดสินใจเลือกแนวปฏิบัติสำหรับการเขียน comment เช่น จะ comment อะไรและจะใช้รูปแบบใด หากคุณเขียนโปรแกรมด้วยภาษาที่มีเครื่องมือสำหรับสร้างเอกสาร เช่น Javadoc สำหรับ Java, Doxygen สำหรับ C++ หรือ godoc สำหรับ Go! ให้ทำตามแนวปฏิบัติของเครื่องมือเหล่านั้น ไม่มีแนวปฏิบัติใดที่สมบูรณ์แบบ แต่เครื่องมือเหล่านี้ก็ให้ประโยชน์มากพอที่จะชดเชยข้อบกพร่องได้ หากคุณเขียนโปรแกรมในสภาพแวดล้อมที่ไม่มีแนวปฏิบัติใดให้ทำตาม ลองนำแนวปฏิบัติจากภาษาหรือโปรเจกต์อื่นที่คล้ายคลึงกันมาใช้ เพราะจะทำให้นักพัฒนาคนอื่นเข้าใจและทำตามแนวปฏิบัติของคุณได้ง่ายขึ้น
แนวปฏิบัติมีจุดประสงค์สองอย่าง อย่างแรกคือช่วยให้เกิดความสอดคล้อง ซึ่งทำให้ comment อ่านและเข้าใจได้ง่ายขึ้น อย่างที่สองคือช่วยให้แน่ใจว่าคุณเขียน comment จริงๆ ถ้าคุณไม่มีความคิดที่ชัดเจนว่าคุณจะ comment อะไรและอย่างไร มันก็ง่ายที่จะลงเอยด้วยการไม่เขียน comment เลย
comment ส่วนใหญ่จะอยู่ในหนึ่งในประเภทต่อไปนี้:
Interface: comment block ที่วางไว้หน้า declaration ของ module เช่น class, data structure, function หรือ method โดย comment จะอธิบาย interface ของ module สำหรับ class นั้น comment จะอธิบาย abstraction โดยรวมที่ class จัดเตรียมไว้ให้ สำหรับ method หรือ function นั้น comment จะอธิบายพฤติกรรมโดยรวม argument และ return value (ถ้ามี) side effect หรือ exception ใดๆ ที่เกิดขึ้น รวมถึงข้อกำหนดอื่นๆ ที่ผู้เรียกต้องทำให้สำเร็จก่อนที่จะเรียกใช้ method
Data structure member: comment ที่วางไว้ถัดจาก declaration ของ field ใน data structure เช่น instance variable หรือ static variable สำหรับ class
Implementation comment: comment ที่อยู่ภายในโค้ดของ method หรือ function ซึ่งอธิบายว่าโค้ดทำงานภายในอย่างไร
Cross-module comment: comment ที่อธิบาย dependency ที่ข้ามขอบเขตของ module
comment ที่สำคัญที่สุดคือสองประเภทแรก ทุก class ควรมี interface comment ทุก class variable ควรมี comment และทุก method ควรมี interface comment ในบางครั้ง declaration ของตัวแปรหรือ method ก็ชัดเจนจนไม่มีอะไรที่เป็นประโยชน์จะเพิ่มใน comment (getter และ setter บางครั้งก็อยู่ในหมวดนี้) แต่กรณีนี้เกิดขึ้นได้ยาก การ comment ทุกอย่างง่ายกว่าการเสียพลังงานกังวลว่าต้องการ comment หรือไม่ Implementation comment มักจะไม่จำเป็น (ดู Section 13.6 ด้านล่าง) Cross-module comment เป็นประเภทที่พบได้ยากที่สุดและเขียนได้ยาก แต่เมื่อจำเป็นแล้วก็มีความสำคัญมาก Section 13.7 จะพูดถึงในรายละเอียดเพิ่มเติม
13.2 Don't repeat the code (อย่าเขียน comment ซ้ำกับโค้ด)
น่าเสียดายที่ comment จำนวนมากไม่ได้มีประโยชน์มากนัก สาเหตุที่พบบ่อยที่สุดคือ comment เหล่านั้นพูดซ้ำกับโค้ด: ข้อมูลทั้งหมดใน comment สามารถสรุปได้ง่ายๆ จากโค้ดที่อยู่ข้างเคียง นี่คือตัวอย่างโค้ดที่ปรากฏในงานวิจัยเมื่อไม่นานมานี้:
ptr_copy = get_copy(obj) | # Get pointer copy |
if is_unlocked(ptr_copy): | # Is obj free? |
return obj | # return current obj |
if is_copy(ptr_copy): | # Already a copy? |
return obj | # return obj |
thread_id = get_thread_id(ptr_copy) | |
if thread_id == ctx.thread_id: | # Locked by current ctx |
return ptr_copy | # Return copy |
ไม่มีข้อมูลที่เป็นประโยชน์ใน comment เหล่านี้เลย ยกเว้น comment "Locked by" ที่บอกอะไรบางอย่างเกี่ยวกับ thread ซึ่งอาจไม่ชัดเจนจากโค้ด สังเกตว่า comment เหล่านี้อยู่ในระดับรายละเอียดที่พอๆ กับโค้ด คือมีหนึ่ง comment ต่อหนึ่งบรรทัดโค้ด ซึ่งอธิบายบรรทัดนั้นๆ Comment แบบนี้แทบจะไม่มีประโยชน์เลย
นี่คือตัวอย่างเพิ่มเติมของ comment ที่พูดซ้ำกับโค้ด:
// Add a horizontal scroll bar
hScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
add(hScrollBar, BorderLayout.SOUTH);
// Add a vertical scroll bar
vScrollBar = new JScrollBar(JScrollBar.VERTICAL);
add(vScrollBar, BorderLayout.EAST);
// Initialize the caret-position related values
caretX = 0;
caretY = 0;
caretMemX = null;
Comment เหล่านี้ไม่มีค่าใดๆ เลย สำหรับสอง comment แรก โค้ดก็ชัดเจนพออยู่แล้วว่าไม่จำเป็นต้องมี comment ส่วนในกรณีที่สาม comment อาจมีประโยชน์ได้ แต่ comment ปัจจุบันให้รายละเอียดไม่พอที่จะเป็นประโยชน์
หลังจากที่คุณเขียน comment แล้ว ให้ถามตัวเองด้วยคำถามนี้: คนที่ไม่เคยเห็นโค้ดมาก่อนสามารถเขียน comment นี้ได้หรือไม่ เพียงแค่มองดูโค้ดข้างๆ comment? ถ้าคำตอบคือใช่ เช่นในตัวอย่างข้างบน แสดงว่า comment นั้นไม่ได้ทำให้โค้ดเข้าใจง่ายขึ้น Comment แบบนี้คือสาเหตุที่บางคนคิดว่า comment ไม่มีค่า
ข้อผิดพลาดทั่วไปอีกอย่างหนึ่งคือการใช้คำเดียวกันใน comment ที่ปรากฏในชื่อของสิ่งที่กำลังถูก document:
/*
* Obtain a normalized resource name from REQ.
*/
private static String[] getNormalizedResourceNames(
HTTPRequest req) ...
/*
* Downcast PARAMETER to TYPE.
*/
private static Object downCastParameter(String parameter, String type) ...
/*
* The horizontal padding of each line in the text.
*/
private static final int textHorizontalPadding = 4;
Comment เหล่านี้แค่เอาคำจากชื่อ method หรือตัวแปร บางครั้งก็เพิ่มคำจากชื่อและประเภทของ argument แล้วนำมาเรียงเป็นประโยค ตัวอย่างเช่น สิ่งเดียวใน comment ที่สองที่ไม่ได้อยู่ในโค้ดคือคำว่า "to"! เช่นเดียวกัน comment เหล่านี้สามารถเขียนได้โดยแค่มองดู declaration โดยไม่ต้องเข้าใจ method หรือตัวแปรเลย ดังนั้นมันจึงไม่มีค่า
Red Flag: Comment Repeats Code (ธงแดง: Comment ซ้ำกับโค้ด)
ถ้าข้อมูลใน comment เห็นได้ชัดเจนอยู่แล้วจากโค้ดที่อยู่ข้างเคียง comment นั้นก็ไม่มีประโยชน์ ตัวอย่างหนึ่งคือเมื่อ comment ใช้คำเดียวกับชื่อของสิ่งที่กำลังอธิบาย
ในขณะเดียวกัน ก็มีข้อมูลสำคัญที่หายไปจาก comment เหล่านี้ เช่น "normalized resource name" คืออะไร และองค์ประกอบของ array ที่คืนมาจาก getNormalizedResourceNames มีอะไรบ้าง "Downcast" หมายถึงอะไร padding มีหน่วยอะไร และ padding อยู่ด้านใดของแต่ละบรรทัด ด้านเดียวหรือทั้งสองด้าน? การอธิบายสิ่งเหล่านี้ใน comment จะเป็นประโยชน์
ขั้นตอนแรกในการเขียน comment ที่ดีคือ ใช้คำใน comment ที่แตกต่างจากชื่อของสิ่งที่กำลังอธิบาย เลือกคำที่ให้ข้อมูลเพิ่มเติมเกี่ยวกับความหมายของสิ่งนั้น แทนที่จะแค่พูดชื่อซ้ำ ตัวอย่างเช่น นี่คือ comment ที่ดีกว่าสำหรับ textHorizontalPadding :
/*
* The amount of blank space to leave on the left and
* right sides of each line of text, in pixels.
*/
private static final int textHorizontalPadding = 4;
Comment นี้ให้ข้อมูลเพิ่มเติมที่ไม่ชัดเจนจาก declaration เอง เช่น หน่วย (pixels) และความจริงที่ว่า padding ใช้กับทั้งสองด้านของแต่ละบรรทัด แทนที่จะใช้คำว่า "padding" comment จะอธิบายว่า padding คืออะไร เผื่อว่าผู้อ่านยังไม่คุ้นเคยกับคำนี้
13.3 Lower-level comments add precision (Comment ระดับต่ำเพิ่มความแม่นยำ)
เมื่อคุณรู้แล้วว่าอะไรไม่ควรทำ มาพูดถึงว่าควรใส่อะไรใน comment บ้าง Comment ช่วยเสริมโค้ดโดยให้ข้อมูลในระดับรายละเอียดที่แตกต่างออกไป Comment บางตัวให้ข้อมูลในระดับที่ต่ำกว่าและมีรายละเอียดมากกว่าโค้ด ซึ่งช่วยเพิ่ม ความแม่นยำ โดยการทำให้ความหมายที่แท้จริงของโค้ดชัดเจนขึ้น Comment อื่นๆ ให้ข้อมูลในระดับที่สูงกว่าและเป็นนามธรรมมากกว่าโค้ด ซึ่งให้ ความเข้าใจโดยสัญชาตญาณ เช่น เหตุผลเบื้องหลังโค้ด หรือวิธีคิดที่ง่ายกว่าและเป็นนามธรรมมากกว่าเกี่ยวกับโค้ด Comment ที่อยู่ในระดับเดียวกันกับโค้ดมักจะเป็นการพูดซ้ำกับโค้ด ส่วนนี้จะพูดถึงแนวทาง ระดับต่ำในรายละเอียดเพิ่มเติม และส่วนถัดไปจะพูดถึงแนวทางระดับสูง
ความแม่นยำมีประโยชน์มากที่สุดในการ comment declaration ของตัวแปร เช่น class instance variable, method argument และ return value ชื่อและประเภทใน declaration ของตัวแปรนั้นโดยทั่วไปไม่ค่อยแม่นยำนัก Comment สามารถเติมเต็มรายละเอียดที่ขาดหายไป เช่น:
- หน่วยของตัวแปรนี้คืออะไร?
- เงื่อนไขขอบเขตเป็นแบบ inclusive หรือ exclusive?
- ถ้า允许ค่า null ได้ ค่านั้นหมายถึงอะไร?
- ถ้าตัวแปรอ้างอิงถึง resource ที่ต้องถูกปลดปล่อยหรือปิดในที่สุด ใครเป็นผู้รับผิดชอบในการปลดปล่อยหรือปิดมัน?
- มีคุณสมบัติบางอย่างที่เป็นจริงเสมอสำหรับตัวแปรนี้หรือไม่ ( invariants ) เช่น "list นี้จะมีอย่างน้อยหนึ่ง entry เสมอ"?
ข้อมูลบางอย่างนี้อาจจะสามารถหาได้จากการตรวจสอบโค้ดทั้งหมดที่ใช้ตัวแปรนั้น แต่มันใช้เวลานานและผิดพลาดได้ง่าย comment ของ declaration ควรชัดเจนและสมบูรณ์พอที่จะทำให้ไม่จำเป็นต้องทำแบบนั้น เมื่อฉันบอกว่า comment สำหรับ declaration ควรอธิบายสิ่งที่เห็นได้ไม่ชัดเจนจากโค้ด "โค้ด" ในที่นี้หมายถึงโค้ดที่อยู่ข้างๆ comment (declaration) ไม่ใช่ "โค้ดทั้งหมดในแอปพลิเคชัน"
ปัญหาที่พบบ่อยที่สุดของ comment สำหรับตัวแปรคือ comment นั้นคลุมเครือเกินไป นี่คือตัวอย่างสองอันของ comment ที่ไม่แม่นยำพอ:
// Current offset in resp Buffer
uint32_t offset;
// Contains all line-widths inside the document and
// number of appearances.
private TreeMap<Integer, Integer> lineWidths;
ในตัวอย่างแรก ไม่ชัดเจนว่า "current" หมายถึงอะไร ในตัวอย่างที่สอง ไม่ชัดเจนว่า key ใน TreeMap คือความกว้างของบรรทัด และ value คือจำนวนครั้งที่ปรากฏ นอกจากนี้ ความกว้างวัดเป็น pixels หรือ characters? Comment ที่แก้ไขแล้วด้านล่างให้รายละเอียดเพิ่มเติม:
// Position in this buffer of the first object that hasn't
// been returned to the client.
uint32_t offset;
// Holds statistics about line lengths of the form <length, count>
// where length is the number of characters in a line (including
// the newline), and count is the number of lines with
// exactly that many characters. If there are no lines with
// a particular length, then there is no entry for that length.
private TreeMap<Integer, Integer> numLinesWithLength;
declaration ที่สองใช้ชื่อที่ยาวขึ้นซึ่งสื่อความหมายได้มากขึ้น นอกจากนี้ยังเปลี่ยนจาก "width" เป็น "length" เพราะคำนี้มีแนวโน้มที่จะทำให้คนคิดว่าหน่วยเป็น characters มากกว่า pixels สังเกตว่า comment ที่สองไม่ได้แค่ document รายละเอียดของแต่ละ entry แต่ยัง document ด้วยว่าการไม่มี entry หมายถึงอะไร
เมื่อ document ตัวแปร ให้คิดเป็น คำนาม ไม่ใช่ คำกริยา กล่าวคือ เน้นว่าตัวแปรแทนอะไร ไม่ใช่ว่ามันถูกจัดการอย่างไร ลองพิจารณา comment ต่อไปนี้:
/* FOLLOWER VARIABLE: indicator variable that allows the Receiver and the
* PeriodicTasks thread to communicate about whether a heartbeat has been
* received within the follower's election timeout window.
* Toggled to TRUE when a valid heartbeat is received.
* Toggled to FALSE when the election timeout window is reset. */
private boolean receivedValidHeartbeat;
Document นี้บรรยายว่าตัวแปรถูกแก้ไขโดยโค้ดหลายส่วนใน class อย่างไร Comment จะสั้นกว่าและมีประโยชน์มากกว่าถ้ามันอธิบายว่าตัวแปรแทนอะไร แทนที่จะสะท้อนโครงสร้างของโค้ด:
/* True means that a heartbeat has been received since the last time
* the election timer was reset. Used for communication between the
* Receiver and PeriodicTasks threads. */
private boolean receivedValidHeartbeat;
จาก documentation นี้ ก็สามารถอนุมานได้ง่ายว่าตัวแปรต้องถูก set เป็น true เมื่อได้รับ heartbeat และ set เป็น false เมื่อ election timer ถูกรีเซ็ต
13.4 Higher-level comments enhance intuition (Comment ระดับสูงช่วยเสริมความเข้าใจโดยสัญชาตญาณ)
วิธีที่สองที่ comment สามารถเสริมโค้ดได้คือการให้ความเข้าใจโดยสัญชาตญาณ Comment เหล่านี้เขียนในระดับที่สูงกว่าโค้ด พวกมันละเว้นรายละเอียดและช่วยให้ผู้อ่านเข้าใจเจตนาและโครงสร้างโดยรวมของโค้ด วิธีนี้มักใช้กับ comment ภายใน method และ interface comment ตัวอย่างเช่น พิจารณาโค้ดต่อไปนี้:
// If there is a LOADING readRpc using the same session
// as PKHash pointed to by assignPos, and the last PKHash
// in that readRPC is smaller than current assigning
// PKHash, then we put assigning PKHash into that readRPC.
int readActiveRpcId = RPC_ID_NOT_ASSIGNED;
for (int i = 0; i < NUM_READ_RPC; i++) {
if (session == readRpc[i].session
&& readRpc[i].status == LOADING
&& readRpc[i].maxPos < assignPos
&& readRpc[i].numHashes < MAX_PKHASHES_PERRPC) {
readActiveRpcId = i;
break;
}
}
Comment นี้มีรายละเอียดต่ำและละเอียดเกินไป ในแง่หนึ่ง มันพูดซ้ำกับโค้ดบางส่วน: "if there is a LOADING readRPC" ก็แค่พูดซ้ำเงื่อนไข readRpc[i].status == LOADING ในอีกแง่หนึ่ง comment ไม่ได้อธิบายวัตถุประสงค์โดยรวมของโค้ดนี้ หรือว่ามันเข้ากับ method ที่บรรจุมันอยู่อย่างไร ผลลัพธ์ก็คือ comment ไม่ได้ช่วยให้ผู้อ่านเข้าใจโค้ด
นี่คือ comment ที่ดีกว่า:
// Try to append the current key hash onto an existing
// RPC to the desired server that hasn't been sent yet.
Comment นี้ไม่มีรายละเอียดใดๆ แต่กลับอธิบายฟังก์ชันโดยรวมของโค้ดในระดับที่สูงกว่า ด้วยข้อมูลระดับสูงนี้ ผู้อ่านสามารถอธิบายเกือบทุกอย่างที่เกิดขึ้นในโค้ดได้: loop ต้องเป็นการวนซ้ำผ่าน RPC ที่มีอยู่ทั้งหมด การทดสอบ session น่าจะใช้เพื่อดูว่า RPC ใดถูกส่งไปยัง server ที่ถูกต้อง การทดสอบ LOADING บอกเป็นนัยว่า RPC สามารถมีหลายสถานะ และในบางสถานะก็ไม่ปลอดภัยที่จะเพิ่ม hash เพิ่มเติม การทดสอบ MAX_PKHASHES_PERRPC บอกเป็นนัยว่ามีขีดจำกัดว่า можноส่งกี่ hash ใน RPC เดียว สิ่งเดียวที่ comment ไม่ได้อธิบายคือการทดสอบ maxPos ยิ่งไปกว่านั้น comment ใหม่ยังให้พื้นฐานสำหรับผู้อ่านในการตัดสินโค้ด: มันทำทุกอย่างที่จำเป็นเพื่อเพิ่ม key hash ลงใน RPC ที่มีอยู่หรือไม่? Comment เดิมไม่ได้อธิบายเจตนาโดยรวมของโค้ด ดังนั้นจึงยากสำหรับผู้อ่านที่จะตัดสินว่าโค้ดทำงานถูกต้องหรือไม่
Comment ระดับสูงเขียนยากกว่า comment ระดับต่ำเพราะคุณต้องคิดเกี่ยวกับโค้ดในมุมมองที่แตกต่างออกไป ถามตัวเองว่า: โค้ดนี้พยายามทำอะไร? สิ่งที่ง่ายที่สุดที่คุณสามารถพูดได้เพื่ออธิบายทุกอย่างในโค้ดคืออะไร? อะไรคือสิ่งที่สำคัญที่สุดเกี่ยวกับโค้ดนี้?
วิศวกรมักจะใส่ใจรายละเอียดเป็นอย่างมาก เราชอบรายละเอียดและเก่งในการจัดการกับมันจำนวนมาก นี่คือสิ่งจำเป็นสำหรับการเป็นวิศวกรที่ดี แต่ผู้ออกแบบซอฟต์แวร์ที่ยอดเยี่ยมยังสามารถถอยออกมาจากรายละเอียดและคิดเกี่ยวกับระบบในระดับที่สูงขึ้นได้ ซึ่งหมายถึงการตัดสินใจว่าด้านใดของระบบที่สำคัญที่สุด และสามารถละเว้นรายละเอียดระดับต่ำและคิดเกี่ยวกับระบบในแง่ของ คุณลักษณะพื้นฐานที่สุดของมันเท่านั้น นี่คือแก่นแท้ของ abstraction (การหาวิธีที่ง่ายในการคิดเกี่ยวกับสิ่งที่ซับซ้อน) และนี่คือสิ่งที่คุณต้องทำเมื่อเขียน comment ระดับสูง Comment ระดับสูงที่ดีจะแสดงแนวคิดง่ายๆ หนึ่งหรือสองอย่างที่ให้กรอบแนวคิด เช่น "append to an existing RPC" เมื่อมีกรอบแนวคิดนี้ ก็จะเห็นได้ง่ายว่าคำสั่งโค้ดเฉพาะเจาะจงเกี่ยวข้องกับเป้าหมายโดยรวมอย่างไร
นี่คือตัวอย่างโค้ดอีกตัวอย่างหนึ่งซึ่งมี comment ระดับสูงที่ดี:
if (numProcessedPKHashes < readRpc[i].numHashes) {
// Some of the key hashes couldn't be looked up in
// this request (either because they aren't stored
// on the server, the server crashed, or there
// wasn't enough space in the response message).
// Mark the unprocessed hashes so they will get
// reassigned to new RPCs.
for (size_t p = removePos; p < insertPos; p++) {
if (activeRpcId[p] == i) {
if (numProcessedPKHashes > 0) {
numProcessedPKHashes--;
} else {
if (p < assignPos)
assignPos = p;
activeRpcId[p] = RPC_ID_NOT_ASSIGNED;
}
}
}
}
Comment นี้ทำสองสิ่ง ประโยคที่สองให้คำอธิบายเชิงนามธรรมว่าโค้ดทำอะไร ประโยคแรกแตกต่างออกไป: มันอธิบาย (ในเชิงระดับสูง) ว่าทำไม โค้ดถึงถูก execute Comment ในรูปแบบ "เรามาถึงจุดนี้ได้อย่างไร" มีประโยชน์มากในการช่วยให้ผู้คนเข้าใจโค้ด ตัวอย่างเช่น เมื่อ document method การอธิบายเงื่อนไขที่ method มีแนวโน้มจะถูกเรียกใช้มากที่สุดนั้นมีประโยชน์มาก (โดยเฉพาะถ้า method ถูกเรียกใช้เฉพาะในสถานการณ์ที่ผิดปกติเท่านั้น)
13.5 Interface documentation (เอกสารสำหรับ Interface)
หนึ่งในบทบาทที่สำคัญที่สุดของ comment คือการกำหนด abstraction ย้อนไปถึง Chapter 4 ที่กล่าวว่า abstraction คือมุมมองที่เรียบง่ายของสิ่งหนึ่ง ซึ่งเก็บข้อมูลสำคัญไว้แต่ละเว้นรายละเอียดที่สามารถมองข้ามได้อย่างปลอดภัย โค้ดไม่เหมาะสมสำหรับการอธิบาย abstraction เพราะมันมีรายละเอียดต่ำเกินไปและรวมรายละเอียดการ implement ที่ ไม่ควรมองเห็นได้ใน abstraction วิธีเดียวที่จะอธิบาย abstraction คือการใช้ comment ถ้าคุณต้องการโค้ดที่นำเสนอ abstraction ที่ดี คุณต้อง document abstraction เหล่านั้นด้วย comment
ขั้นตอนแรกในการ document abstraction คือการแยก interface comment ออกจาก implementation comment Interface comment ให้ข้อมูลที่ใครสักคนจำเป็นต้องรู้เพื่อใช้งาน class หรือ method โดยมันจะกำหนด abstraction Implementation comment อธิบายว่า class หรือ method ทำงานภายในอย่างไรเพื่อ implement abstraction การแยก comment ทั้งสองประเภทนี้เป็นสิ่งสำคัญ เพื่อที่ผู้ใช้ interface จะไม่ถูกเปิดเผยกับรายละเอียดการ implement ยิ่งไปกว่านั้น comment ทั้งสองรูปแบบนี้ควรจะแตกต่างกัน ถ้า interface comment ต้องอธิบาย implementation ด้วย แสดงว่า class หรือ method นั้นตื้น (shallow) นั่นหมายความว่าการเขียน comment สามารถให้เบาะแสเกี่ยวกับคุณภาพของการออกแบบได้ Chapter 15 จะกลับมาที่แนวคิดนี้อีกครั้ง
Interface comment สำหรับ class จะให้คำอธิบายระดับสูงของ abstraction ที่ class จัดเตรียมไว้ให้ เช่นตัวอย่างต่อไปนี้:
/**
* This class implements a simple server-side interface to the HTTP
* protocol: by using this class, an application can receive HTTP
* requests, process them, and return responses. Each instance of
* this class corresponds to a particular socket used to receive
* requests. The current implementation is single-threaded and
* processes one request at a time.
*/
public class Http {...}
Comment นี้ describes ความสามารถโดยรวมของ class โดยไม่มีรายละเอียดการ implement หรือแม้แต่ข้อมูลเฉพาะของ method แต่ละตัว นอกจากนี้ยังอธิบายด้วยว่าแต่ละ instance ของ class แทนอะไร สุดท้าย comment ยังอธิบายข้อจำกัดของ class (มันไม่รองรับการเข้าถึงพร้อมกันจากหลาย threads) ซึ่งอาจสำคัญต่อนักพัฒนาที่กำลังพิจารณาว่าจะใช้งานมันหรือไม่
Interface comment สำหรับ method จะรวมทั้งข้อมูลระดับสูงสำหรับ abstraction และข้อมูลระดับต่ำสำหรับความแม่นยำ:
- comment มักจะเริ่มต้นด้วยหนึ่งหรือสองประโยคที่อธิบายพฤติกรรมของ method ตามที่ผู้เรียกมองเห็น ซึ่งคือ abstraction ระดับสูง
- comment ต้องอธิบาย argument แต่ละตัวและ return value (ถ้ามี) comment เหล่านี้ต้องแม่นยำมาก และต้องอธิบายข้อจำกัดใดๆ ของค่ของ argument รวมถึง dependencies ระหว่าง argument ด้วย
- ถ้า method มี side effect ใดๆ จะต้องถูก document ไว้ใน interface comment Side effect คือผลกระทบใดๆ ของ method ที่ส่งผลต่อพฤติกรรมในอนาคตของระบบ แต่ไม่ใช่ส่วนหนึ่งของผลลัพธ์ ตัวอย่างเช่น ถ้า method เพิ่มค่าลงใน data structure ภายใน ซึ่งสามารถเรียกค้นได้โดยการเรียก method ในอนาคต นี่คือ side effect การเขียนลงใน file system ก็เป็น side effect เช่นกัน
- Interface comment ของ method ต้องอธิบาย exception ใดๆ ที่สามารถเกิดขึ้นได้จาก method
- ถ้ามี precondition ใดๆ ที่ต้องถูกทำให้สำเร็จก่อนที่จะเรียกใช้ method จะต้องอธิบายไว้ (บางทีอาจต้องเรียก method อื่นก่อน สำหรับ binary search method list ที่กำลังค้นหาต้องถูก sort ไว้แล้ว) ควรลด precondition ให้เหลือน้อยที่สุด แต่ precondition ที่ยังเหลืออยู่ต้องถูก document ไว้
นี่คือ interface comment สำหรับ method ที่คัดลอกข้อมูลจาก Buffer object:
ไวยากรณ์ของ comment นี้ (เช่น \return ) เป็นไปตามแนวปฏิบัติของ Doxygen โปรแกรมที่แยก comment จากโค้ด C/C++ และรวบรวมเป็นหน้าเว็บ เป้าหมายของ comment คือการให้ข้อมูลทั้งหมดที่นักพัฒนาต้องการเพื่อเรียกใช้ method รวมถึงการจัดการกรณีพิเศษ (สังเกตว่า method นี้ทำตามคำแนะนำของ Chapter 10 และกำหนดให้ไม่มี error ที่เกี่ยวข้องกับการระบุช่วง) นักพัฒนาไม่จำเป็นต้องอ่าน body ของ method เพื่อเรียกใช้มัน และ interface comment ก็ไม่ได้ให้ข้อมูลใดๆ เกี่ยวกับว่า method ถูก implement อย่างไร เช่น มันสแกน data structure ภายในอย่างไรเพื่อหาข้อมูลที่ต้องการ
สำหรับตัวอย่างที่ยาวขึ้น ลองพิจารณา class ที่ชื่อ IndexLookup ซึ่งเป็นส่วนหนึ่งของระบบ distributed storage ระบบ storage เก็บรวบรวม table หลายตาราง แต่ละตารางมี object จำนวนมาก นอกจากนี้ แต่ละ table สามารถมี index ได้หนึ่งตัวหรือมากกว่า แต่ละ index ให้การเข้าถึง object ใน table อย่างมีประสิทธิภาพโดยอ้างอิงจาก field ใด field หนึ่งของ object ตัวอย่างเช่น index หนึ่งอาจใช้เพื่อค้นหา object จาก field name และอีก index หนึ่งอาจใช้เพื่อค้นหา object จาก field age ด้วย index เหล่านี้ แอปพลิเคชันสามารถดึง object ทั้งหมดที่มี name เฉพาะ หรือทั้งหมดที่มี age ในช่วงที่กำหนดได้อย่างรวดเร็ว
Class IndexLookup จัดเตรียม interface ที่สะดวกสำหรับการค้นหาแบบใช้ index นี่คือตัวอย่างการใช้งานในแอปพลิเคชัน:
query = new IndexLookup(table, index, key1, key2);
while (true) {
object = query.getNext();
if (object == NULL) {
break;
}
... process object ...
}
แอปพลิเคชันจะสร้าง object ของ type IndexLookup โดยระบุ argument ที่เลือก table, index และช่วงภายใน index (ตัวอย่างเช่น ถ้า index อ้างอิงจาก field age key1 และ key2 อาจระบุเป็น 21 และ 65 เพื่อเลือก object ทั้งหมดที่มีอายุระหว่างค่านั้น) จากนั้นแอปพลิเคชันจะเรียก method getNext ซ้ำๆ แต่ละครั้งจะคืนค่า object หนึ่งตัวที่อยู่ในช่วงที่ต้องการ เมื่อ object ทั้งหมดที่ตรงตามเงื่อนไขถูกคืนมาแล้ว getNext จะคืนค่า NULL เนื่องจากระบบ storage เป็นแบบ distributed การ implement class นี้จึงค่อนข้างซับซ้อน object ใน table อาจกระจายอยู่บน server หลายตัว และแต่ละ index ก็อาจกระจายอยู่บน server คนละชุดกัน โค้ดใน IndexLookup ต้องสื่อสารกับ server index ที่เกี่ยวข้องทั้งหมดก่อนเพื่อรวบรวมข้อมูลเกี่ยวกับ object ในช่วงนั้น จากนั้นต้องสื่อสารกับ server ที่เก็บ object จริงเพื่อดึงค่ามา
ทีนี้ลองพิจารณาว่าข้อมูลใดบ้างที่ควรอยู่ใน interface comment สำหรับ class นี้ สำหรับข้อมูลแต่ละชิ้นด้านล่าง ให้ถามตัวเองว่านักพัฒนาจำเป็นต้องรู้ข้อมูลนั้นเพื่อใช้งาน class หรือไม่ (คำตอบของฉันต่อคำถามเหล่านี้อยู่ที่ท้ายบท):
-
รูปแบบของ message ที่ IndexLookup ส่งไปยัง server ที่เก็บ index และ object
-
ฟังก์ชันเปรียบเทียบที่ใช้ในการพิจารณาว่า object ใดอยู่ในช่วงที่ต้องการ (การเปรียบเทียบใช้ integer, floating-point number หรือ string?)
-
โครงสร้างข้อมูลที่ใช้เก็บ index บน server
-
ว่า IndexLookup ส่ง request หลายรายการไปยัง server ต่างๆ พร้อมกันหรือไม่
-
กลไกการจัดการเมื่อ server ล่ม
นี่คือเวอร์ชันดั้งเดิมของ interface comment สำหรับ class IndexLookup โดยตัวอย่างยังรวมถึงสองสามบรรทัดจากนิยามของ class ซึ่งถูกอ้างถึงใน comment:
/*
* This class implements the client side framework for index range
* lookups. It manages a single LookupIndexKeys RPC and multiple
* IndexedRead RPCs. Client side just includes "IndexLookup.h" in
* its header to use IndexLookup class. Several parameters can be set
* in the config below:
* - The number of concurrent indexedRead RPCs
* - The max number of PKHashes a indexedRead RPC can hold at a time
* - The size of the active PKHashes
*
* To use IndexLookup, the client creates an object of this class by
* providing all necessary information. After construction of
* IndexLookup, client can call getNext() function to move to next
* available object. If getNext() returns NULL, it means we reached
* the last object. Client can use getKey, getKeyLength, getValue,
* and getValueLength to get object data of current object.
*/
class IndexLookup {
...
private:
/// Max number of concurrent indexedRead RPCs
static const uint8_t NUM_READ_RPC = 10;
/// Max number of PKHashes that can be sent in one
/// indexedRead RPC
static const uint32_t MAX_PKHASHES_PERRPC = 256;
/// Max number of PKHashes that activeHashes can
/// hold at once.
static const size_t MAX_NUM_PK = (1 << LG_BUFFER_SIZE);
}
ก่อนที่จะอ่านต่อ ลองดูว่าคุณสามารถระบุปัญหาของ comment นี้ได้หรือไม่ นี่คือปัญหาที่ฉันพบ:
- ส่วนใหญ่ของย่อหน้าแรกเกี่ยวข้องกับการ implement ไม่ใช่ interface ตัวอย่างหนึ่งคือ ผู้ใช้ไม่จำเป็นต้องรู้ชื่อของ remote procedure call ที่ใช้สื่อสารกับ server ค่า configuration parameter ที่อ้างถึงในครึ่งหลังของย่อหน้าแรกเป็น private variable ทั้งหมดที่เกี่ยวข้องเฉพาะผู้ดูแล class เท่านั้น ไม่ใช่ผู้ใช้ ข้อมูลเกี่ยวกับ implementation ทั้งหมดนี้ควรถูกละเว้นจาก comment
- comment ยังรวมหลายสิ่งที่เห็นได้ชัดเจน ตัวอย่างเช่น ไม่จำเป็นต้องบอกผู้ใช้ให้ include IndexLookup.h เพราะใครก็ตามที่เขียนโค้ด C++ จะเดาได้ว่ามันจำเป็น นอกจากนี้ ข้อความ "by providing all necessary information" ไม่ได้บอกอะไรเลย ดังนั้นก็ละเว้นได้
comment ที่สั้นกว่าสำหรับ class นี้ก็เพียงพอแล้ว (และดีกว่า):
/*
* This class is used by client applications to make range queries
* using indexes. Each instance represents a single range query.
*
* To start a range query, a client creates an instance of this
* class. The client can then call getNext() to retrieve the objects
* in the desired range. For each object returned by getNext(), the
* caller can invoke getKey(), getKeyLength(), getValue(), and
* getValueLength() to get information about that object.
*/
ย่อหน้าสุดท้ายของ comment นี้ไม่จำเป็นอย่างเคร่งครัด เพราะส่วนใหญ่เป็นการพูดซ้ำข้อมูลใน comment ของ method แต่ละตัว อย่างไรก็ตาม การมีตัวอย่างใน class documentation ที่แสดงให้เห็นว่า method ต่างๆ ทำงานร่วมกันอย่างไรก็เป็นประโยชน์ โดยเฉพาะสำหรับ class ที่มีความลึกและมีรูปแบบการใช้งานที่ไม่ชัดเจน สังเกตว่า comment ใหม่ไม่ได้กล่าวถึงการคืนค่า NULL จาก getNext comment นี้ไม่ได้ตั้งใจจะ document ทุกรายละเอียดของแต่ละ method แต่ให้ข้อมูลระดับสูงเพื่อช่วยให้ผู้อ่านเข้าใจว่า method ต่างๆ ทำงานร่วมกันอย่างไรและเมื่อใดที่แต่ละ method อาจถูกเรียกใช้ สำหรับรายละเอียด ผู้อ่านสามารถดู interface comment ของ method แต่ละตัวได้ Comment นี้ยังไม่ได้กล่าวถึง server ล่ม เพราะ server ล่มนั้นมองไม่เห็นสำหรับผู้ใช้ class นี้ (ระบบกู้คืนจาก server ล่มโดยอัตโนมัติ)
Red Flag: Implementation Documentation Contaminates Interface (ธงแดง: เอกสารการ Implement ปนเปื้อน Interface)
ธงแดงนี้เกิดขึ้นเมื่อ interface documentation เช่น method documentation อธิบายรายละเอียดการ implement ที่ไม่จำเป็นในการใช้งานสิ่งที่กำลังถูก document
ทีนี้พิจารณาโค้ดต่อไปนี้ ซึ่งแสดงเวอร์ชันแรกของ documentation สำหรับ method isReady ใน IndexLookup :
/**
* Check if the next object is RESULT_READY. This function is
* implemented in a DCFT module, each execution of isReady() tries
* to make small progress, and getNext() invokes isReady() in a
* while loop, until isReady() returns true.
*
* isReady() is implemented in a rule-based approach. We check
* different rules by following a particular order, and perform
* certain actions if some rule is satisfied.
*
* \return
* True means the next Object is available. Otherwise, return
* false.
*/
bool IndexLookup::isReady() { ... }
อีกครั้งหนึ่ง documentation ส่วนใหญ่ เช่น การอ้างถึง DCFT และย่อหน้าที่สองทั้งหมด เกี่ยวข้องกับการ implement ดังนั้นมันจึงไม่ควรอยู่ที่นี่ นี่เป็นหนึ่งในข้อผิดพลาดที่พบบ่อยที่สุดใน interface comment บางส่วนของ implementation documentation ก็มีประโยชน์ แต่มันควรอยู่ใน method ซึ่งจะถูกแยกออกจาก interface documentation อย่างชัดเจน นอกจากนี้ ประโยคแรกของ documentation ก็คลุมเครือ (คำว่า RESULT_READY หมายถึงอะไร?) และข้อมูลสำคัญบางอย่างก็หายไป สุดท้าย ไม่จำเป็นต้องอธิบายการ implement ของ getNext ที่นี่ นี่คือ comment เวอร์ชันที่ดีกว่า:
/*
* Indicates whether an indexed read has made enough progress for
* getNext to return immediately without blocking. In addition, this
* method does most of the real work for indexed reads, so it must
* be invoked (either directly, or indirectly by calling getNext) in
* order for the indexed read to make progress.
*
* \return
* True means that the next invocation of getNext will not block
* (at least one object is available to return, or the end of the
* lookup has been reached); false means getNext may block.
*/
Comment เวอร์ชันนี้ให้ข้อมูลที่แม่นยำมากขึ้นเกี่ยวกับว่า "ready" หมายถึงอะไร และให้ข้อมูลสำคัญว่า method นี้ต้องถูกเรียกใช้ในที่สุดถ้าต้องการให้การค้นหาแบบใช้ index ดำเนินต่อไปได้
13.6 Implementation comments: what and why, not how (Implementation comment: อะไรและทำไม ไม่ใช่อย่างไร)
Implementation comment คือ comment ที่ปรากฏภายใน method เพื่อช่วยให้ผู้อ่านเข้าใจว่ามันทำงานภายในอย่างไร method ส่วนใหญ่นั้นสั้นและเรียบง่ายพอที่จะไม่ต้องมี implementation comment เมื่อมีโค้ดและ interface comment ก็สามารถเข้าใจได้ง่ายว่า method ทำงานอย่างไร
เป้าหมายหลักของ implementation comment คือการช่วยให้ผู้อ่านเข้าใจ ว่า โค้ดกำลังทำอะไร (ไม่ใช่ว่ามันทำได้อย่างไร) เมื่อผู้อ่านรู้แล้วว่าโค้ดพยายามทำอะไร ก็มักจะเข้าใจได้ง่ายว่าโค้ดทำงานอย่างไร สำหรับ method สั้นๆ โค้ดจะทำเพียงสิ่งเดียว ซึ่งอธิบายไว้แล้วใน interface comment ดังนั้นจึงไม่จำเป็นต้องมี implementation comment Method ที่ยาวกว่าจะมีโค้ดหลายบล็อกที่ทำสิ่งที่แตกต่างกันเป็นส่วนหนึ่งของงานโดยรวมของ method ให้เพิ่ม comment ก่อนแต่ละบล็อกหลักเพื่อให้คำอธิบายระดับสูง (นามธรรมมากขึ้น) ว่าบล็อกนั้นทำอะไร นี่คือตัวอย่าง:
// Phase 1: Scan active RPCs to see if any have completed.
สำหรับ loop การมี comment ก่อน loop ที่อธิบายว่าแต่ละ iteration ทำอะไรมีประโยชน์:
// Each iteration of the following loop extracts one request from
// the request message, increments the corresponding object, and
// appends a response to the response message.
สังเกตว่า comment นี้ describe loop ในระดับที่เป็นนามธรรมและเข้าใจได้ง่ายขึ้น มันไม่ลงรายละเอียดใดๆ เกี่ยวกับการแยก request ออกจาก request message หรือการ increment object Comment สำหรับ loop จำเป็นเฉพาะกับ loop ที่ยาวหรือซับซ้อนกว่า ซึ่งอาจไม่ชัดเจนว่า loop กำลังทำอะไร loop หลายๆ ตัวนั้นสั้นและเรียบง่ายพอที่พฤติกรรมของมันก็เห็นได้ชัดเจนอยู่แล้ว
นอกจากจะอธิบาย ว่า โค้ดกำลังทำอะไรแล้ว implementation comment ยังมีประโยชน์ในการอธิบาย ว่าทำไม ด้วย ถ้ามีลักษณะที่ทำให้เกิดความสับสนในโค้ดซึ่งอาจไม่ชัดเจนจากการอ่าน คุณควร document มันไว้ ตัวอย่างเช่น ถ้าการแก้ไข bug ต้องมีการเพิ่มโค้ดที่จุดประสงค์ไม่ชัดเจนนัก ให้เพิ่ม comment อธิบายว่าเหตุใดจึงต้องมีโค้ดนี้ สำหรับการแก้ไข bug ที่มี bug report ที่เขียนดีซึ่งอธิบายปัญหา comment สามารถอ้างถึง issue ในฐานข้อมูล bug tracking แทนที่จะพูดซ้ำรายละเอียดทั้งหมด ("Fixes RAM-436, related to device driver crashes in Linux 2.4.x") นักพัฒนาสามารถดูใน bug database เพื่อดูรายละเอียดเพิ่มเติม (นี่คือตัวอย่างของการหลีกเลี่ยงการพูดซ้ำใน comment ซึ่งจะพูดถึงใน Chapter 16 )
สำหรับ method ที่ยาวขึ้น การเขียน comment สำหรับ local variable ที่สำคัญที่สุดสองสามตัวก็มีประโยชน์ อย่างไรก็ตาม local variable ส่วนใหญ่ไม่จำเป็นต้องมี documentation ถ้ามันมีชื่อที่ดี ถ้าทุกการใช้ตัวแปรอยู่ภายในระยะไม่กี่บรรทัดจากกัน ก็มักจะเข้าใจวัตถุประสงค์ของตัวแปรได้ง่ายโดยไม่ต้องมี comment ในกรณีนี้ ก็ปล่อยให้ผู้อ่านอ่านโค้ดเพื่อหาความหมายของตัวแปรได้ แต่ถ้าตัวแปรถูกใช้ในโค้ดช่วงกว้าง คุณควรพิจารณาเพิ่ม comment เพื่ออธิบายตัวแปร เมื่อ document ตัวแปร ให้เน้นที่ ว่า ตัวแปรแทนอะไร ไม่ใช่ว่ามันถูกจัดการในโค้ดอย่างไร
13.7 Cross-module design decisions (การตัดสินใจออกแบบข้าม Module)
ในโลกที่สมบูรณ์แบบ การตัดสินใจออกแบบที่สำคัญทุกอย่างจะถูก encapsulate ไว้ใน class เดียว น่าเสียดายที่ระบบจริงนั้น不可避免地ต้องมีการตัดสินใจออกแบบที่ส่งผลกระทบต่อหลาย class ตัวอย่างเช่น การออกแบบ network protocol จะส่งผลกระทบทั้งผู้ส่งและผู้รับ และสิ่งเหล่านี้อาจถูก implement ในสถานที่ต่างกัน การตัดสินใจข้าม module มักจะซับซ้อนและละเอียดอ่อน และเป็นสาเหตุของ bug จำนวนมาก ดังนั้น documentation ที่ดีสำหรับมันจึงเป็นสิ่งสำคัญ
ความท้าทายที่ใหญ่ที่สุดของ cross-module documentation คือการหาสถานที่ที่จะวางไว้ซึ่งนักพัฒนาจะพบได้โดยธรรมชาติ บางครั้งก็มีสถานที่ศูนย์กลางที่ชัดเจนสำหรับ documentation ดังกล่าว ตัวอย่างเช่น ระบบ RAMcloud storage กำหนดค่า Status ซึ่งถูกคืนโดยแต่ละ request เพื่อบ่งชี้ความสำเร็จหรือล้มเหลว การเพิ่ม Status สำหรับเงื่อนไข error ใหม่จำเป็นต้องแก้ไขไฟล์ที่แตกต่างกันหลายไฟล์ (ไฟล์หนึ่ง map ค่า Status ไปยัง exception อีกไฟล์หนึ่งให้ human-readable message สำหรับแต่ละ Status และอื่นๆ) โชคดีที่มีสถานที่หนึ่งที่ชัดเจนที่นักพัฒนาจะต้องไปเมื่อเพิ่ม status ใหม่ นั่นคือ declaration ของ Status enum เราใช้ประโยชน์จากสิ่งนี้โดยการเพิ่ม comment ใน enum นั้นเพื่อระบุสถานที่อื่นๆ ทั้งหมดที่ต้องแก้ไข:
typedef enum Status {
STATUS_OK = 0,
STATUS_UNKNOWN_TABLET = 1,
STATUS_WRONG_VERSION = 2,
...
STATUS_INDEX_DOESNT_EXIST = 29,
STATUS_INVALID_PARAMETER = 30,
STATUS_MAX_VALUE = 30,
// Note: if you add a new status value you must make the following
// additional updates:
// (1) Modify STATUS_MAX_VALUE to have a value equal to the
// largest defined status value, and make sure its definition
// is the last one in the list. STATUS_MAX_VALUE is used
// primarily for testing.
// (2) Add new entries in the tables "messages" and "symbols" in
// Status.cc.
// (3) Add a new exception class to ClientException.h
// (4) Add a new "case" to ClientException::throwException to map
// from the status value to a status-specific ClientException
// subclass.
// (5) In the Java bindings, add a static class for the exception
// to ClientException.java
// (6) Add a case for the status of the exception to throw the
// exception in ClientException.java
// (7) Add the exception to the Status enum in Status.java, making
// sure the status is in the correct position corresponding to
// its status code.
}
ค่า status ใหม่จะถูกเพิ่มต่อท้ายรายการที่มีอยู่ ดังนั้น comment ก็จะถูกวางไว้ที่ท้ายเช่นกัน ซึ่งมีแนวโน้มว่าจะถูกเห็นมากที่สุด
น่าเสียดายที่ในหลายกรณีไม่มีสถานที่ศูนย์กลางที่ชัดเจนสำหรับ cross-module documentation ตัวอย่างหนึ่งจากระบบ RAMCloud storage คือโค้ดสำหรับจัดการ zombie server ซึ่งคือ server ที่ระบบเชื่อว่าล่มแล้ว แต่จริงๆ แล้วยังทำงานอยู่ การทำให้ zombie server เป็นกลางจำเป็นต้องใช้โค้ดในหลาย module ที่แตกต่างกัน และโค้ดเหล่านี้ทั้งหมด depend กันและกัน ไม่มีโค้ดส่วนใดที่เป็นสถานที่ศูนย์กลางที่ชัดเจนในการวาง documentation ทางเลือกหนึ่งคือการทำซ้ำ documentation ในแต่ละสถานที่ที่ depend กับมัน แต่มันก็ยุ่งยาก และเป็นการยากที่จะทำให้ documentation ดังกล่าวทันสมัยเมื่อระบบ evolve อีกทางเลือกหนึ่งคือวาง documentation ไว้ในสถานที่ใดสถานที่หนึ่งที่ต้องการ 但在กรณีนี้ก็ไม่น่าเป็นไปได้ที่นักพัฒนาจะเห็น documentation หรือรู้ว่าจะหามันได้จากที่ไหน
เมื่อเร็วๆ นี้ฉันได้ทดลองใช้แนวทางที่ปัญหา cross-module ถูก document ไว้ในไฟล์กลางชื่อ designNotes ไฟล์ถูกแบ่งออกเป็นส่วนๆ ที่มีป้ายกำกับชัดเจน หนึ่งส่วนต่อหนึ่งหัวข้อสำคัญ ตัวอย่างเช่น นี่คือข้อความที่ตัดมาจากไฟล์:
...
Zombies
-------
A zombie is a server that is considered dead by the rest of the
cluster; any data stored on the server has been recovered and will
be managed by other servers. However, if a zombie is not actually
dead (e.g., it was just disconnected from the other servers for a
while) two forms of inconsistency can arise:
* A zombie server must not serve read requests once replacement servers have taken over; otherwise it may return stale data that does not reflect writes accepted by the replacement servers.
* The zombie server must not accept write requests once replacement servers have begun replaying its log during recovery; if it does, these writes may be lost (the new values may not be stored on the replacement servers and thus will not be returned by reads).
RAMCloud uses two techniques to neutralize zombies. First,
...
จากนั้น ในโค้ดส่วนใดก็ตามที่เกี่ยวข้องกับหนึ่งในปัญหาเหล่านี้ จะมี comment สั้นๆ ที่อ้างถึงไฟล์ designNotes :
// See "Zombies" in designNotes.
ด้วยแนวทางนี้ จะมี documentation เพียงชุดเดียวและนักพัฒนาสามารถค้นหาได้ง่ายเมื่อต้องการ 然而 ข้อเสียคือ documentation ไม่ได้อยู่ใกล้กับโค้ดส่วนใดๆ ที่ depend กับมัน ดังนั้นจึงอาจรักษาให้ทันสมัยได้ยากเมื่อระบบ evolve
13.8 Conclusion (บทสรุป)
เป้าหมายของ comment คือเพื่อให้แน่ใจว่าโครงสร้างและพฤติกรรมของระบบนั้นชัดเจนสำหรับผู้อ่าน เพื่อให้พวกเขาสามารถค้นหาข้อมูลที่ต้องการได้อย่างรวดเร็วและปรับเปลี่ยนระบบได้อย่างมั่นใจว่ามันจะทำงานได้ ข้อมูลบางอย่างนี้สามารถแสดงในโค้ดในแบบที่จะชัดเจนอยู่แล้วสำหรับผู้อ่าน แต่ก็มีข้อมูลจำนวนมากที่ไม่สามารถสรุปได้ง่ายจากโค้ด Comment จะเติมเต็มข้อมูลส่วนนี้
เมื่อปฏิบัติตามกฎที่ว่า comment ควรอธิบายสิ่งที่เห็นได้ไม่ชัดเจนจากโค้ด คำว่า "ไม่ชัดเจน" นั้นมองจากมุมมองของคนที่อ่านโค้ดของคุณเป็นครั้งแรก (ไม่ใช่คุณ) เมื่อเขียน comment พยายามใส่ตัวเองใน mindset ของผู้อ่านและถามตัวเองว่าอะไรคือสิ่งสำคัญที่เขาหรือเธอจำเป็นต้องรู้ ถ้าโค้ดของคุณกำลังถูก review และ reviewer บอกคุณว่ามีบางอย่างไม่ชัดเจน อย่าเถียงกับพวกเขา ถ้าผู้อ่านคิดว่ามันไม่ชัดเจน มันก็ไม่ชัดเจน แทนที่จะเถียง พยายามทำความเข้าใจว่าพวกเขาสับสนกับอะไรและดูว่าคุณสามารถทำให้มันชัดเจนขึ้นได้ไหม ไม่ว่าจะด้วย comment ที่ดีขึ้นหรือโค้ดที่ดีขึ้น
13.9 Answers to questions from Section 13.5 (คำตอบของคำถามจาก Section 13.5)
นักพัฒนาจำเป็นต้องรู้ข้อมูลแต่ละข้อต่อไปนี้เพื่อใช้งาน class IndexLookup หรือไม่?
-
รูปแบบของ message ที่ IndexLookup class ส่งไปยัง server ที่เก็บ index และ object ไม่: นี่คือรายละเอียดการ implement ที่ควรซ่อนอยู่ใน class
-
ฟังก์ชันเปรียบเทียบที่ใช้ในการพิจารณาว่า object ใดอยู่ในช่วงที่ต้องการ (การเปรียบเทียบใช้ integer, floating-point number หรือ string?) ใช่: ผู้ใช้ class จำเป็นต้องรู้ข้อมูลนี้
-
โครงสร้างข้อมูลที่ใช้เก็บ index บน server ไม่: ข้อมูลนี้ควรถูก encapsulate ไว้ที่ server แม้แต่การ implement ของ IndexLookup ก็ไม่ควรต้องรู้ข้อมูลนี้
-
ว่า IndexLookup ส่ง request หลายรายการไปยัง server ต่างๆ พร้อมกันหรือไม่ เป็นไปได้: ถ้า IndexLookup ใช้เทคนิคพิเศษเพื่อเพิ่มประสิทธิภาพ documentation ควรให้ข้อมูลระดับสูงเกี่ยวกับเรื่องนี้ เนื่องจากผู้ใช้อาจสนใจเรื่องประสิทธิภาพ
-
กลไกการจัดการเมื่อ server ล่ม ไม่: RAMCloud กู้คืนจาก server ล่มโดยอัตโนมัติ ดังนั้นการล่มจึงไม่สามารถมองเห็นได้จากซอฟต์แวร์ระดับแอปพลิเคชัน ดังนั้นจึงไม่จำเป็นต้องกล่าวถึงการล่มใน interface documentation สำหรับ IndexLookup ถ้าการล่มถูกส่งผลกระทบขึ้นไปถึงแอปพลิเคชัน interface documentation ก็จะต้องอธิบายว่ามันแสดงออกอย่างไร (แต่ไม่ใช่รายละเอียดของการกู้คืน)