iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 8
8

良好程式碼的優點大同小異。
不好的程式碼的糙點卻各有巧妙之處。

註解

程式碼撰寫,最終的目標是希望可以記載知識,提供閱讀與維護。
好的程式碼,可以像文件化 (文件一樣,自己解釋自己),不過程式語言中的語法,有它的極限,只是人類腦中想表達的方式卻是各式各樣的。

在程式碼中,註解的正確使用方式,是能夠在最關鍵時刻,提供提示的內容。
但是有個重要的前提,就是要先懂得盡可能的使用程式語言的語法表達,也盡可能的讀懂程式語言的語法與語意。

這是一段可以自我說明的 PL/I 程式 + 它的註解[1]
語法本身無法表達的程式流程,在最後用註解辛苦的畫上箭頭,提示開發者重要的訊息。

不適合使用註解的情境

該用 function name 取代註解

來看看一段程式碼[2],給自己五秒鐘看看是不是可以一眼就看出這在做什麼?

function main() {
  //openImage
  const img = document.querySelector("img");
  const canvas = document.querySelector("#draw");
  var ctx = canvas.getContext("2d");
  const height = img.height;
  const width = img.width;
  canvas.width = width;
  canvas.height = height;
  ctx.drawImage(img, 0, 0, width, height);
  const image = ctx.getImageData(0, 0, width, height);

  //imageProcess
  const threshold_input = document.querySelector('#threshold');
  threshold = threshold_input.value;
  for(let i = 0; i < image.data.length; i += 4) {
    let sum = image.data[i + 0] + image.data[i + 1] + image.data[i + 2];
    let color = sum / 3;
    if(color > threshold) {
      color = 255;
    }
    else {
      color = 0;
    }
    image.data[i + 0] = color; // RED
    image.data[i + 1] = color; // GREEN
    image.data[i + 2] = color; // Blue
  }

  // report
  let [countW, countB] = countWB(image);
  showReport(countW, countB);
  console.log(countW / (countW + countB) * 100, countB / (countW + countB) * 100);

  // drawImage
  const canvas = document.querySelector("#draw");
  var ctx = canvas.getContext("2d");
  ctx.putImageData(image, 0, 0);
}

img.addEventListener('load', main);

看得出來嗎?
還是你喜歡看這個版本?

function main() {
  let image = openImage("img", "#draw");
  image = imageProcess(image);
  report(image);
  drawImage(image, "#draw");
}

先前有提過,曝露太多複雜性的問題,而複雜有段落感、有層次感需要表達。
在此的第一個例子,就是用註解呈現段落感,但是這並不是一個使用註解的好時機,段落感最好的做法,用 function name 取代註解。

而簡要的說明自己的程式碼在做什麼,在這個例子,就是要把主程式的內容說一次。

做了四件事情

  1. 打開圖檔
  2. 影像處理演算法
  3. 顯示影像處理結果的量化資料
  4. 顯示影像處理結果的圖形資料

分別再往內細講每一個步驟在做什麼。

  1. 打開圖檔
    • 宣告好 canvas
    • 取得 img tag 的影像來源
  2. 影像處理演算法
    • 設定閥值
    • 處理每一個 pixel
      • 比閥值大,就變白,不然變黑
  3. 顯示影像處理結果的量化資料
    • 計數黑白值各多少
    • 顯示計算結果到畫面上
  4. 顯示影像處理結果的圖形資料
    • 影像結果顯示在 canvas 上

是不是很像用文書編輯時的大綱模式,這就是層次感
程式語言要寫得不糙,最基本的,就是要注意層次與段落的安排。
在此,不要靠註解。

「註解會說謊,原始碼不會」
--感謝海綿寶寶的補充

註解不要解釋語法

如果你的程式註解,總是這樣寫,代表幾件事

  1. 你沒有進步。
  2. 很勤勞 (但不是好的那種)
  3. 不擅長與人有效的溝通
function openImage(img_tag, canvas_id) {
  const img = document.querySelector(img_tag); //取得 img
  const canvas = document.querySelector(canvas_id); // 取得 canvas
  var ctx = canvas.getContext("2d"); // 宣告出 canvas 的 2d context 
  // 決定 img 長寬
  const height = img.height;  
  const width = img.width;
  // 定義 canvas 長寬與 img 相同
  canvas.width = width;
  canvas.height = height;
  // context 由 canvas 的左上開始,畫出 img 的長寬的大小,畫出 img  內容
  // 就是顯示 img 到 canvas
  ctx.drawImage(img, 0, 0, width, height);
  // 回傳 img 的 pixel data
  return ctx.getImageData(0, 0, width, height);
}

這些註解都是語法本身,也就是說,所有的內容都重複了兩次,對於語法純熟的開發者來說,只是贅字[3]而認真要說,是一種語病,要小心!!!

召喚神獸

呃...我就不在這貼了,避免被以為是灌水文
有興趣的人可以到 Github 看看

這種神獸註解,如果真的覺得一定要使用,那麼請問問自己相信的是不是自己的 debug 技術。
這是信念的問題呀!

如果只是好玩,請貼在一個閱讀率非常低的地方,例如 service 初始化或主程式宣告的地方。
原因在於,避免開發者讀到時花很多時間捲過去,而且,這樣對神獸才有真正崇敬之心。

版本控制

// Copyright (c) 2000-2013, Chris Systems Corporation. All rights reserved.
//
// History:
// Date         Reference       Person          Descriptions
// ----------   --------------  --------------  --------------------------------------------
// 2018/10/16   R20181016.01    Chris           Initial

如果看見這樣的註解,相信你已經進入了一間沒有版本控制的公司了。
好好禱告,神會讓事情好轉的

  1. 讓公司有版本控制,至少自己要有
  2. 讓你自己的下一間公司有版本控制
  3. 讓你自己習慣沒有版本控制的世界 (哦!GOD!!你在現代不用電看看)

簽名

簽名!天哪!
這是舊時器時代的行為吧?現在已經「已知用燈了」別再用火照明了好嗎?
版本控制軟體的進步,已經不需要再這樣為程式碼負責了。
而且 git blame[4] 也可以讓你「就算不想負責也不行」,所以這種~~脫褲子放...~~的行為就不用了。
程式碼只會更混亂

下面的範例,很聰明的,有單行簽名和多行簽名,相信這種習慣確實造成不必要的困擾,才會出現多行的簽名(但是還是有困擾)

function openImage(img_tag, canvas_id) {
  const img = document.querySelector(img_tag);// 20181016 Chris
  const canvas = document.querySelector(canvas_id);
  var ctx = canvas.getContext("2d");
  // 20181016 Chris Start
  const height = img.height;  
  const width = img.width;
  canvas.width = width;
  canvas.height = height;
  ctx.drawImage(img, 0, 0, width, height);
  return ctx.getImageData(0, 0, width, height);
  // 20181016 Chris End
}

註解程式碼

相信這非常的常見,非常非常的....沒有節操
來看一段...因為太多了,我用縮圖 + 馬賽克

從 45~257 都是註解。最後只剩下一個右大括號 } 而已。

不刪除的原因,也許過去只有一個人開發,那就像是一個人住的房間一樣,是不是要收拾乾淨,全看個人習慣。
另外,這可是人生的問題了呢[5],

紀念品
紀念品送回老家,在老家生活的人,如何怦然心動呢?
唯有用手觸摸帶著回憶的物品,並且決定去向,才可以面對過去的回憶。

照片
空間的使用並不是為了過去的自己,而是為了邁向未來的現在的自己。
整理到這,培養出來的「心動判斷法」已經很準確了。(一定要經過上述的整理過程,才可以整理照片)

整理方法:一張一張的從相本拿出來看,每一個當下的瞬間,是否還讓你心動。

關於小孩的紀念品→不心動就丟!
過去戀情的紀念品(情書)→丟!若不這麼做,將會錯失新的機會。

重要的不是過去的回憶,而是經歷了過往而存在於當下的自己。

出現寫這樣程式碼的朋友,就該好好的讓他面對過去,才可以迎向未來,否則就算寫出不糙的 code 也許會人格分裂

好的註解

好的註解,其實每一本程式碼品質的書都會詳細介紹

  1. 《Clean Code》, Ch4 註解,整個章節都在介紹註解。
    對於不好的註解的介紹有 68~81 頁之多
  2. 《The Art of Readable Code》 ,Ch5 認識註解, Ch6 讓註解精確與簡潔。
    花了兩個章節介紹
  3. 《Code Complete 2/e》 ,Ch9 虛擬碼程式設計流程。
    介紹如何寫完程式碼就擁有良好註解。
  4. 《You Don't Know JS: Up & Going》, Chapter 1: Into Programming, Code Comments
    給出了一個簡單的標準: 寫 why 不寫 what,偶爾寫寫 how。

在此給的建議,會如同 YDKJS 的標準一般: 「寫 why 不寫 what,偶爾寫寫 how。」

建議註解一段 HTML 的時機

解釋自己做了什麼的註解,尤其是拼出了什麼字串

  1. 拼出 HTML 結構
  2. router 拼出網址字串

在前端有時會需要用 JavaScript 拼出一段 HTML。[6]
若這段 HTML 很複雜,又很難一眼看出來,建議在拼完的結果,將它註解一份出來。
(也許有更好的做法...暫時還沒有想到)

  var doneItem = document.createElement("div");
  var doneItemText = document.createElement("div");
  var doneItemIcons = document.createElement("div");
  var doneItemIconsTrash = document.createElement("div");
  var doneItemIconsDone = document.createElement("div");
  //添加class
  doneItem.className="done__item";
  doneItemText.className="done__item__text";
  doneItemIcons.className="done__item__icons";
  doneItemIconsTrash.className="done__item__icons__trash";
  doneItemIconsDone.className="done__item__icons__done";
  //添加完成事項內容
  doneItemText.innerHTML=doneContent;
  //添加icon
  doneItemIconsTrash.innerHTML="<i class="+'"fas fa-trash-alt"'+"></i>";
  doneItemIconsDone.innerHTML="<i class="+'"fas fa-check-circle"'+"></i>";
  //添加元素,形成父子關係
  doneItemIcons.appendChild(doneItemIconsTrash);
  doneItemIcons.appendChild(doneItemIconsDone);
  doneItem.appendChild(doneItemText);
  doneItem.appendChild(doneItemIcons);
  // doneItem => 
  // <div class="done__item">
  //   <div class="done__item__text">test text</div>
  //   <div class="done__item__icons">
  //   <div class="done__item__icons__trash">
  //     <i class="fas fa-trash-alt"></i>
  //   </div>
  //   <div class="done__item__icons__done">
  //     <i class="fas fa-check-circle"></i>
  //   </div>
  //   </div>
  // </div>

PPP 開發法[7]

簡單說,就是這樣做

  1. 先寫下你要寫程式的註解
// 1. 打開圖檔
//     - 宣告好 canvas
//     - 取得 img tag 的影像來源
// 2. 影像處理演算法
//     - 設定閥值
//     - 處理每一個 pixel
//         - 比閥值大,就變白,不然變黑
// 3. 顯示影像處理結果的量化資料
//     - 計數黑白值各多少
//     - 顯示計算結果到畫面上
// 4. 顯示影像處理結果的圖形資料
//     - 影像結果顯示在 canvas 上
  1. 在註解下寫下程式碼
  // 1. 打開圖檔
  //     - 宣告好 canvas
  const canvas = document.querySelector("#draw");
  //     - 取得 img tag 的影像來源
  const img = document.querySelector("img");
  var ctx = canvas.getContext("2d");
  const height = img.height;
  const width = img.width;
  canvas.width = width;
  canvas.height = height;
  ctx.drawImage(img, 0, 0, width, height);
  const image = ctx.getImageData(0, 0, width, height);


  // 2. 影像處理演算法
  //     - 設定閥值
  const threshold_input = document.querySelector('#threshold');
  threshold = threshold_input.value;
  //     - 處理每一個 pixel
  for(let i = 0; i < image.data.length; i += 4) {
    let sum = image.data[i + 0] + image.data[i + 1] + image.data[i + 2];
    let color = sum / 3;
    //         - 比閥值大,就變白,不然變黑
    if(color > threshold) {
      color = 255;
    }
    else {
      color = 0;
    }
    image.data[i + 0] = color; // RED
    image.data[i + 1] = color; // GREEN
    image.data[i + 2] = color; // Blue
  }
  // 3. 顯示影像處理結果的量化資料
  //     - 計數黑白值各多少
  let [countW, countB] = countWB(image);
  //     - 顯示計算結果到畫面上
  showReport(countW, countB);
  console.log(countW / (countW + countB) * 100, countB / (countW + countB) * 100);

  // 4. 顯示影像處理結果的圖形資料
  //     - 影像結果顯示在 canvas 上
  const canvas = document.querySelector("#draw");
  var ctx = canvas.getContext("2d");
  ctx.putImageData(image, 0, 0);
  1. 將適合成為 function 的部份歸納成 function

在此就不細列出各個 function 內的樣子了

function main() {
  // 1. 打開圖檔
  let image = openImage("img", "#draw");
  // 2. 影像處理演算法
  image = imageProcess(image);
  // 3. 顯示影像處理結果的量化資料
  report(image);
  // 4. 顯示影像處理結果的圖形資料
  drawImage(image, "#draw");
}
  1. 看看有沒有什麼要刪掉的註解。
function main() {
  let image = openImage("img", "#draw");
  image = imageProcess(image);
  report(image);
  drawImage(image, "#draw");
}

為什麼要這樣做?

  1. 開發者不想補註解
  2. 註解先寫會幫助思考問題,而不是依賴程式碼執行結果反推問題
  3. 最後刪註解比較輕鬆

這是一個練習方式但是個人是不建議每次都這麼做啦。

參考資料

[1]: 《The Mythical Man-Month: Essays on Software Engineering》, Ch15 The Other Face
[2]: ImageBinarization - Github
[3]: 別讓語言癌纏上你!
[4]: 【狀況題】等等,這行程式誰寫的?
[5]: 怦然心動的人生整理魔法//讀書心得(3)
[6]: UnaLai/todo-list - Github
[7]: 《Code Complete 2/e》 ,Ch9 虛擬碼程式設計流程。
介紹如何寫完程式碼就擁有良好註解。


上一篇
不用前額葉的命名
下一篇
不要造神 (神一般的物件)
系列文
可不可以不要寫糙 code30

2 則留言

0
棉花
iT邦新手 4 級 ‧ 2018-10-23 11:29:18

目前待的公司也是用註解做版控....而SVN被當成網路磁碟機
除了開頭的修改清單外,還要求程式碼修改要在旁邊註解是哪天、哪時、改了什麼
程式碼刪除只能用註解,不能直接砍掉

神獸的話,蠻多網站打開開發工具看原始碼都能發現有神獸出沒

Chris iT邦新手 5 級‧ 2018-10-23 11:52:24 檢舉

SVN 當作備份 source code 用的。等於沒有版本控制。QQ

3
海綿寶寶
iT邦超人 1 級 ‧ 2018-10-26 09:02:25

少了一句網路名言

註解會說謊,原始碼不會
/images/emoticon/emoticon10.gif

Chris iT邦新手 5 級‧ 2018-10-28 07:58:41 檢舉

比較適合放在「用 function name 取代註解」的段落

我要留言

立即登入留言