點燈坊

失くすものさえない今が強くなるチャンスよ

如何降低程式碼複雜度 ?

Sam Xiao's Avatar 2019-11-25

if elseswitch case 是最基本的邏輯判斷方式,但卻也是 複雜度 的元兇,實務上善用 ECMAScript 語言特性與 Higher Order Function 降低複雜度,讓程式碼可讀性更高,也更容易維護。

Version

macOS Catalina 10.15.1
VS Code 1.40.1
Quokka 1.0.259
ECMAScript 2015+

&&

let fn = val => {
  if (val === 'iPhone')
    return val;
  else
    return false;
}

fn() // ?
fn('iPhone') // ?

若為 true 則直接回傳,否則回傳 false

complex015

let fn = val => val === 'iPhone' && val

fn() // ?
fn('iPhone') // ?

可使用 &&,若為 false 直接回傳 false,若為 true 才會繼續執行 && 右側部分。

complex016

||

let fn = val => {
  if (val === 'iPhone')
    return true;
  else
    return val;
}

fn() // ?
fn('iPad') // ?
fn('iPhone') // ?

若需求反過來,只有 false 直接回傳,否則回傳 true

complex017

let fn = val => val === 'iPhone' || val

fn() // ?
fn('iPad') // ?
fn('iPhone') // ?

可使用 ||,若為 true 直接回傳 true,若為 false 才會繼續執行 || 右側部分。

complex018

Or

let fn = val => {
  if (val === 'iPhone' || val === 'iPad' || val === 'Apple Watch')
    return true
  else
    return false
}

fn('Apple Watch') // ?
fn('Macbook') // ?

常見的需求,|| 若其中一個條件成立就回傳 true,否則回傳 false

complex000

let fn = val => ['iPhone', 'iPad', 'Apple Watch'].includes(val)
  
fn('Apple Watch') // ?
fn('Macbook') // ?

Array.prototype.includes() 的原意是判斷 item 是否在 array 中,若有則回傳 true,否則傳回 false

利用 includes() 這個特性,可將所有要判斷的項目改寫在 array 中,改用 includes() 判斷,不只清爽且可讀性也很高。

complex001

let fn = val => ['iPhone', 'iPad', 'Apple Watch'].some(x => x === val)
  
fn('Apple Watch') // ?
fn('Macbook') // ?

也可使用 Array.prototype.some()includes()some() 的差異是 includes() 的 argument 為 value,而 some() 為 function。

complex018

Guard Clause

let fn = (product, quantity) => {
  let apples = ['iPhone', 'iPad', 'Apple Watch']
  
  if (product) {
    if (apples.includes(product)) {
      console.log(`${product} is Apple product`)

      if (quantity > 10)
        console.log('big quantity')
    }
  } else
    throw new Error('No Apple product')
}

fn('iPhone')
fn('iPad', 20)
fn()

若都使用 正向判斷,可能會造成 nested if else 而難以維護。

complex002

let fn = (product, quantity) => {
  let apples = ['iPhone', 'iPad', 'Apple Watch']
  
  if (!product) throw new Error('No Apple product');

  if (!apples.includes(product)) return

  console.log(`${product} is Apple product`)

  if (quantity > 10) console.log('big quantity')
}

fn('iPhone')
fn('iPad', 20)
fn()

可使用 guard clause 將反向邏輯提早 return,讓所有 if 判斷都扁平化只有一層,這也是為什麼有些 Lint 會警告你不能寫 else,因為使用 guard clause 之後,就不會再出現 else 了。

complex003

Array.prototype.every()

let data = [
  { name: 'iPhone', color: 'white' },
  { name: 'iPad', color: 'black' },
  { name: 'Macbook', color: 'silver' }
]

let fn = val => arr => {
  let result = true

  for (let x of arr) {
    if (!result) break
    result = x.color === val
  }

  return result
}

fn('white')(data) // ?

若要 全部條件 都成立,實務上我們也會將資料與條件全部先放在 array 中。

最直覺方式就是透過 for loop 判斷。

complex008

let data = [
  { name: 'iPhone', color: 'white' },
  { name: 'iPad', color: 'black' },
  { name: 'Macbook', color: 'silver' }
]

let fn = val => arr => arr.every(x => x.color === val)

fn('white')(data) // ?

也可使用 Array.prototype.every(),則所有條件都為 true 才會回傳 true

complex009

Array.prototype.some()

let data = [
  { name: 'iPhone', color: 'white' },
  { name: 'iPad', color: 'black' },
  { name: 'Macbook', color: 'silver' }
]

let fn = val => arr => {
  let result = false

  for (let x of arr) {
    if (result) break
    result = x.color === val
  }

  return result
}

fn('white')(data) // ?

若只要 有一個條件 成立即可,實務上我們也會將資料與條件全部先放在 array 中。

最直覺方式就是透過 for loop 判斷。

complex020

let data = [
  { name: 'iPhone', color: 'white' },
  { name: 'iPad', color: 'black' },
  { name: 'Macbook', color: 'silver' }
]

let fn = val => arr => arr.some(x => x.color === val)

fn('white')(data) // ?

若只要 有一個條件 成立即可,可使用 Array.prototype.some()

也可使用 Array.prototype.some(),只要 有一個條件 成立就回傳 true

complex010

Default Parameter

let fn = (product, quantity) => {
  if (!product) return

  quantity = quantity || 1

  return `We have ${quantity} ${product}!`
}

fn() // ?
fn('iPad') // ?
fn('iPhone', 2) // ?

ES5 function 並沒有提供 default parameter,因此會透過判斷是否為 undefined|| 小技巧設定預設值。

但由於 0''false 也視為 falsy value,若你的需求是能接受 0''false,就不能使用此技巧

complex004

let fn = (product, quantity = 1) => {
  if (!product) return

  return `We have ${quantity} ${product}!`
}

fn() // ?
fn('iPad') // ?
fn('iPhone', 2) // ?

ES6 提供了 default parameter 後,語意更加清楚,也不用判斷 undefined 了。

complex005

Object Destructing

let fn = product => {
  if (product && product.name)
    return product.name
  else
    return 'unknown'
}

fn() // ?
fn({}) // ?
fn({ name: 'iPhone', color: 'white' }) // ?

若 parameter 為 object,在 ES5 為避免 parameter 為 undefinednull,又避免根本無 property,必須小心翼翼的判斷 object 與 property。

complex006

let fn = ({ name } = {}) => name || 'unknown'

fn() // ?
fn({}) // ?
fn({ name: 'iPhone', color: 'white' }) // ?

透過 ES6 的 default parameter 與 object destructing,可一次解決判斷 object 與 property 問題。

  • 當 parameter 為 undefined 時,會使用預設值 {}
  • 當 object 沒有 property 時, Object Destructing 拆解後為 undefined,都是 unknown
  • 否則會正常取得 property 值

complex007

If / else if / else

let sendLog = val => console.log(val)

let jumpTo = val => console.log(`Jump to ${val}`)

let fn = status => {
  if (status === 1) {
    sendLog('processing')
    jumpTo('index')
  } else if (status === 2) {
    sendLog('fail')
    jumpTo('error')
  } else {
    sendLog('others')
    jumpTo('default')
  }
};

fn(1)

實務上常會遇到 if ... else if ... else 都呼叫相同的 function,只是 argument 不相同。

complex012

Switch

let sendLog = val => console.log(val)

let jumpTo = val => console.log(`Jump to ${val}`)

let fn = status => {
  switch(status) {
    case 1:
      sendLog('processing')
      jumpTo('index')
      break
    case 2:
      sendLog('fail')
      jumpTo('error')
      break
    default:
      sendLog('others')
      jumpTo('default')
      break
  }
}

fn(1)

整理成 switch() 後 ,可讀性稍可。

complex013

Object

let sendLog = val => console.log(val)

let jumpTo = val => console.log(`Jump to ${val}`)

let actions = {
  '1': ['processing', 'index'],
  '2': ['fail', 'error'],
  'default': ['others', 'default']
}

let fn = status => {
  let [logName, pageName] = actions[status] || actions['default']
  
  sendLog(logName)
  jumpTo(pageName)
}

fn(1)

可將 switch 的判斷值整理成 object 的 key,function 的 argument 以 array 成為 object 的 value,如此則可使用 object 的 [] 取代 switch,default 值則以 || 實現。

object 的回傳值還可使用 array destructuring 加以拆解。

complex014

Nested If

let jumpTo = val => console.log(`Jump to ${val}`)

let fn = (identity, status) => {
  if (identity === 'admin') {
    if (status === 1) jumpTo('admin_1')
    else if (status === 2) jumpTo('admin_2')
    else jumpTo('all_default')
  } else if (identity === 'member') {
    if (status === 1) jumpTo('member_1')
    else if (status === 2) jumpTo('member_2')
    else jumpTo('all_default')
  }
  else jumpTo('all_default')
}

fn('admin', 1)
fn('admin', 3)

巢狀 if 也是常見複雜度怪獸。

complex021

Object

let jumpTo = val => console.log(`Jump to ${val}`)

let admin_1 = () => jumpTo('admin_1') 

let admin_2 = () => jumpTo('admin_2')

let member_1 = () => jumpTo('member_1')

let member_2 = () => jumpTo('member_2')

let all_default = () => jumpTo('all_default')

let actions = {
  admin_1,
  admin_2,
  member_1,
  member_2,
  all_default
}

let fn = (identity, status) =>
  (actions[`${identity}_${status}`] || actions['all_default'])()

fn('admin', 1)
fn('admin', 3)

可將巢狀 if 判斷條件整理以 _ 整理成 function 名稱,以 ES6 的 object shorthand 塞進 object,一樣使用 []|| 取代 switch

complex022

Conclusion

  • 並不是所有的判斷都只能用 if elseswitch case,透過一些 ECMAScript 的語言特性與 higher order function,可以有效降低程式碼複雜度

Reference

Jecelyn Yeen, 5 Tips to Write Better Conditionals in JavaScript
JavaScript 复杂判断的更优雅写法