iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 4
5

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

Linus Torvalds 生生 在 TED 的分享中[1] 提到了對於程式碼品味的事。在此節錄一段較關鍵的內容

It does not have the if statement. And it doesn't really matter -- I don't want you understand why it doesn't have the if statement, but I want you to understand that sometimes you can see a problem in a different way and rewrite it so that a special case goes away and becomes the normal case. And that's good code. But this is simple code. This is CS 101. This is not important -- although, details are important.[name=Linus, 15:34, TED]

簡單的說,if 是「糙點發生的出發點」。

好的工程師,會懂得到很多個需求中,找出一致的邏輯。
但是,這完全取決於「整體概念性」,而不只整理語法的結果。

物件導向減少使用 if

用一個物件導向的例子來說明吧。

這是一個偵測器,量測 Lv 值,所以只有 get 各種的值

const sensor = new Sensor("simulation");
const Lv = sensor.Lv();
const x  = sensor.x();
const y  = sensor.y();

但是,這看似平靜的使用方式,其實每一個實作裡充滿的糙 code 呀!!

class Sensor {
  constructor (mode) {
    this.mode = mode;
  }
  get Lv () {
    if (mode == 'simulation')
      return Math.random() * 100;
    else
      return this._Lv;
  }
  get x () {
    if (mode == 'simulation')
      return Math.random() * 10;
    else
      return this._x;
  }
  get y () {
    if (mode == 'simulation')
      return Math.random() * 10;
    else
      return this._y;
  }
}

看到了滿滿的 if 了嗎?

是不是會有漏寫的?寫反寫錯,如果模式很多是不是會亂掉,不照順序寫會不會更糙?

如何降低糙點?

建立一個 simSensor 和 realSensor 並且繼承 Sensor 這個基礎類別。

class simSensor extends Sensor {
  get Lv () { return Math.random() * 100; }
  get x () { return Math.random() * 10; }
  get y () { return Math.random() * 10; }
}
class realSensor extends Sensor {
  get Lv () { return super._Lv; }
  get x () { return super._x; }
  get y () { return super._y; }
}

使用上只有改變一點點

// const sensor = new Sensor("simulation");
// 改成下面這樣寫
const sensor = new simSensor();
// const sensor = new realSensor();
const Lv = sensor.Lv();
const x  = sensor.x();
const y  = sensor.y();

這樣是不是維護起來舒適多了呢?

Array 去掉重複值

在處理 Array 元素重複問題時,常常會寫 if 並且走訪元素找重複。
好一點的,會使用 Array.prototype.includes

var arr = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
var arr2 = []
arr.forEach(item => {
  if (!arr2.includes(item)) {
    arr2.push(item)
  }
})
arr2  // [1, 2, 3, 4, 5]

但是,這很糙!!!
其實,只要換容器,就可以輕鬆做到這件事。

var arr = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
var arr2 = [...(new Set(arr))]  // [1, 2, 3, 4, 5]

你看!沒有任何的 if 這樣的品味才對

複雜的邏輯

在看下一段的解釋之前,先看看這一段 code

// origList = []
query = origList.filter(item => 
  (id.isChecked && item.id.includes(id.text)) &&
  (name.isChecked && !!item.name && item.name.includes(name.text) || 
  !!item.englishName && item.englishName.includes(name.text)).split("")
);

偷瞄到了嗎?看不懂再移動視線到上面看看這段 code 吧!

解釋解釋這段在寫什麼鬼條件。

它原本不是 js 的程式,為了避免侵權的問題,在此改寫成 js 的 code。
不影響它的糙點!

這是一個,畫面要做 filter 的例子。

使用方式:

  • 要先勾選要過濾的欄位,再填上關鍵字
  • 有 id, name, englishName 要過濾

發現,這麼複雜的邏輯一次曝露出太多的「程式細節」了。
有時,我們只是想知道,它考慮某個條件的邏輯是如何如何,或者想知道有沒有考慮某個條件。

如何降低糙點?

只要能夠用一個名稱來稱呼這些邏輯,就可以用具名的 function 包起來。

function filterId (item) {
  return item.id.includes(id.text)
}

function filterName (item) {
  return !!item.name && item.name.includes(name.text)
}

function filterEnglistName (item) {
  return !!item.englishName && item.englishName.includes(name.text)
}

// origList = []
query = origList.filter(item => 
  id.isChecked && filterId(item) &&
  name.isChecked && filterName(item) || filterEnglistName(item)).split("")
);

在此之前,很難發現,它把 name 和 englishName 當作相同的欄位一起 filter 對吧?
現在,發現了這些檢查的邏輯其實很類似,可以讓它們合併合併。

並且,可以相容 (或修正) id 欄位的檢查邏輯

function isKeyword (word, inputWord) {
  return !!word && word.includes(inputWord)
}

// origList = []
query = origList.filter(item => 
  id.isChecked && isKeyword(item.id, id.text) &&
  name.isChecked && isKeyword(item.name, name.text) || isKeyword(item.englishName, name.text)).split("")
);

這樣也可以跟別人說「所有欄位的檢查邏輯是不是相同的」。

如果這段用在 if

if 裡面會常出現複雜的判斷邏輯,我們把剛剛的 filter 改寫改寫。

// 原本的寫法
if ((id.isChecked && item.id.includes(id.text)) &&
    (name.isChecked && !!item.name && item.name.includes(name.text) || 
    !!item.englishName && item.englishName.includes(name.text)).split("")) {
  return item;
}

這麼一坨,真的是一坨呀!!! (和剛剛一樣的東西)
寫這種東西是你的驕傲嗎?

改成這樣

if (id.isChecked && isKeyword(item.id, id.text) &&
  name.isChecked && isKeyword(item.name, name.text) || isKeyword(item.englishName, name.text)) {
  return item;
}

看上去的 DX[2] 是不是就舒適多了?

De Morgan's laws (笛摩根定理)[3]

意思是

寫成 JavaScript 的話,就是

且 = AND
或 = OR

!(p && q) === (!p || !q)
!(p || q) === (!p && !q)

邏輯加上 NOT

p -> !p
q -> !q
&& -> ||
||-> &&

所以,遇到 if () 裡的邏輯很複雜時,not 加得很亂時,就可以考慮使用 De Morgan’s laws 整理,也許可以換個角度思考或理解,也可以在邏輯上達到等價[4]。

以剛剛的例子,加上 not 就變這樣

// 原本的寫法
if ((!id.isChecked || !item.id.includes(id.text)) ||
    (!name.isChecked || !item.name || !item.name.includes(name.text) && 
    !item.englishName || !item.englishName.includes(name.text)).split("")) {
  // nothing
}
else {
  return item;
}

if 裡的 float 要如何判斷

var a = 3.2
if (a + 0.1 === 3.3) {
  //...
}

a + 0.1 === 3.3 判斷的結果是 false

console.log(a + 0.1)
// 3.3000000000000003

有沒有想過,你的判斷試會正確,完全是碰巧的 (走運)。

var a = 3.2;
var b = a + 0.1;
if (a + 0.1 === b) {
  //...
}

其實是 3.3000000000000003 === 3.3000000000000003 呀!!

float 最早是由 IEEE 754[5] 來的,之後也許有其它更進步的標準,但是還是可以透過它來了解「浮點誤差」。簡單的說 float 是一種 科學記號的儲存方式[6] 造成的誤差現象。

所以,floatif 裡,就不能直接使用 ===== 這類「偵測相等」的判斷方式。

必須要用精度夾擊(寫到這一刻想到的詞)。

若想偵測的精度是小數點以下一位

var a = 3.2
if (a + 0.1 > 3.29 && a + 0.1 < 3.31) {
  //...
}

a + 0.1 > 3.29 && a + 0.1 < 3.31 判斷的結果是 true

而且,這樣寫符合精度。

參考資料

[1]: Linus Torvalds: The mind behind Linux | TED Talk
[2]: 工程師心中最軟的一塊:談前端開發者體驗(Developer Experience)
[3]: De Morgan's laws
[4]: 邏輯等價 - 維基百科,自由的百科全書
[5]: IEEE 754 - 維基百科,自由的百科全書
[6]: 浮點數 - 維基百科,自由的百科全書


上一篇
過度使用全域變數
下一篇
宣告與定義太遙遠
系列文
可不可以不要寫糙 code30

2 則留言

0
王郁翔
iT邦新手 5 級 ‧ 2018-10-19 10:17:52

啊~~ 是 cleanCode 的 Encapsulate Conditionals!!!

Chris iT邦新手 5 級‧ 2018-10-19 11:11:31 檢舉

/images/emoticon/emoticon12.gif

0
Arel
iT邦新手 5 級 ‧ 2018-10-19 20:29:32

讚讚長知識,還有講到集合論,真是專業!

我要留言

立即登入留言