Learn Coding 7: Be An Artist - Coding Convention and Reuse
如何把程式寫得像故事書一樣好讀? int a 跟 int apple 哪一種讀起來比較有意義? 怎麼讓一段程式碼可以不斷被使用? 我也曾經以為掌握基本語法就夠了,沒想到這五項技巧改變了我的程式碼品質。今天我想跟大家分享五個讓程式更好讀、寫起來更有效率的技巧。這些也是我在 coding 時不斷要求自己做的事。如果你想讓你的程式能力突破到下一個階段,點開這篇文章來免費獲得這五項技巧!
int studentheight = 156; int student_height = 156;
這裡我想要你先跟我一起想想,如果要用英文書寫「學生的身高」,我們會寫「student height」,用空白來把兩個單字隔開。然而,空白卻不準出現在變數名稱裡面。為了讓程式碼更貼近文章,我們可以用底線「_」來代替空白把單字隔開來,讓讀者在看「student_height」的時候,產生一種分別看到 student 跟 height 這兩個單字的視覺效果。這比起沒有把單字隔開的 studentheight 好讀多了! 這種規定可以用底線「_」把單字隔開,而且所有字元都要保持小寫的規則叫「Snake Case - 蛇形命名法」,是一種程式碼的「命名規則 (naming convention)」。C、C++ 跟 Python 的變數名稱就都應該遵循 Snake Case 命名規則。
除此之外,有另一種很常見的命名規則叫做「Camel Case - 小駝峰式命名法」。這種規則規定: 除了變數名稱的第一個字元以外,每個「單字」的第一個字元要用大寫呈現,其它字元都要保持小寫。例如 int criticalPoint、String firstName、double bodyFatPercentage。Java、C#、JavaScript 的變數就都遵循這種命名法。這裡有三種最常見的命名規則讓你參考。接著,我要跟你分享該怎麼讓讀者一眼看出每個 if statement 的區段、for statement 的區段、跟 function 等等的區段。
voidbubble_sort(int arr[], int length) { if (length < 0) { throw std::invalid_argument("Length is set to negative integer"); }
if (length <= 1) { return; }
bool is_sorted = false;
do { is_sorted = false;
for (int i = 0; i < length; i++) { if (arr[i] > arr[i + 1]) { int temp = arr[i + 1]; arr[i + 1] = arr[i]; arr[i] = temp; is_sorted = true; } } } while (is_sorted); }
在這段 function 內,每行程式碼前面都插入了「四個空格」當作縮排。這才是好讀的排版方式。這麼做,會讓程式碼看起來更有段落,可以讓讀者一眼看出 function body 的區域。我們甚至可以輕易得看出每個 if、while、for statement 的區域。接著讓我來談談如何用精簡、好讀、又精準的名稱包裝一段複雜的程式碼。
到這裡,我們把施放技能的程式碼用 unleash_skill() 這個 function 包裝起來。我想要你看看 sample code 的第 30 行。光看 function name 就已經幾乎可以知道: 這是一段跟施展技能有關的程式碼片段,而且完全不用去讀 unleash_skill() function 裡面的程式碼。這是我最喜歡的一種技巧,而且並不像大多數人認為的那樣複雜。適當得幫變數或 function 命名不只可以讓名稱更有意義、更容易讓人的腦袋解讀,甚至可以用來包裝複雜的邏輯,讓程式碼的可讀性更上一層樓! 接下來我想跟你分享一個更進階的技巧…
if (!is_dead) { if (character->hp == 0) { character->dead(); } else { if (!character->is_attacking) { run_attack_logic(); } else { if (input->is_jump_key_pressed() && !character->is_jumping()) { character->jump(); } } } }
上面的程式碼是模擬遊戲裡在每一幀的更新邏輯,裡面的 if statements 一層又一層的出現。大部分的人不理解為什麼要避免寫出太多層的 if statement。在我看來除了不好閱讀、不好追溯執行的路徑、分支組合會非常多以外,也不好設計測試案例來跑過功能裡的每條分支,維持好的測試覆蓋率。想像一下,如果我們的 if statements 出現了 5 ~ 6 層,這份程式碼肯定不怎麼好讀。我可以理解有時很難避免寫出朝狀判斷式,但如果我們把程式碼改成這樣……
if (character->hp == 0) { character->dead(); return; }
if (!character->is_attacking) { run_attack_logic(); return; }
if (input->is_jump_key_pressed() && !character->is_jumping()) { character->jump(); }
告訴我你看到了些什麼? 很明顯的,這份 code 裡面的巢狀判斷式已經全部消失,變成一段一段單層的 if 判斷式。我總是想辦法讓 if 區段像上面的範例一樣保持簡單,避免讓讀者看過一層又一層的 code。在這次的範例中,判斷式的內容執行完會後用 return 中斷這段 function 回傳出去。這是一種簡化巢狀 if statements 的方法,但用這種方法得小心,必須要確定每段 if 的內容區段執行完後不會因為中斷離開而漏掉後面該被執行的程式碼。
技巧四: 控制 Function 長度跟職責
你知道嗎? 除了命名規則以外,寫程式還有一種 function 長度必須保持在 25 行的文化! 讓 function 長度保持在 25 行不只可以花比較少時間來讀一份 function,甚至因為功能比較精簡,而可以避免寫出潛在的 bug。至於為什麼是 25 行,我想請你試試看,讓下面這段程式碼在螢幕內一次全部顯示出來:
voiddisplay_message() { printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); printf("A function with 25 lines of code."); }
竟然完全沒有問題! 幾乎所有大家慣用的解析度,都可以讓 25 行的 function 全部顯示在畫面上。除此之外,我們在設計 function 的時候還有一個更進階的技巧 -「單一職責原則」。這是一種頂尖程式設計師都奉行的概念。讓我先給你一段範例的遊戲主迴圈程式碼:
voidupdate_scene() { for (int i = 0; i < scene->ui_comps.size(); i++) { ui_comps.at(i)->update(); }
for (int i = 0; i < scene->objs.size(); i++) { scene->objs.at(i)->update(); }
for (int i = 0; i < scene->npcs.size(); i++) { scene->npcs.at(i)->update(); }
for (int i = 0; i < scene->enemies.size(); i++) { scene->enemies.at(i)->update(); } }
voidupdate_player() { if (!player->is_dead) { if (player->health == 0) { player->die(); scene->on_player_die(); } else { player->update(); } } }
voidgame_loop() { update_input();
update_scene(); update_player(); }
非常好! 三個步驟的細節已經全部被從主迴圈搬到三個 functions 裡面。第一個 function 只執行跟輸入輸出有關的指令、第二個 function 只執行跟場景有關的指令、第三個 function 只執行跟玩家有關的指令。每個 function 各司其職,完全不做跟目的沒關係的事。主迴圈因為只呼叫這三個 functions,變得非常簡單易懂,而且可以藉由 function names 一眼看出每個步驟的目的。最後,我們要談的是今天最進階的主題,也是高價值工程師都奉行的 reuse 概念。