iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 29
0
Software Development

Functional Programming in JS系列 第 29

Either Monad: 應用在真實專案

https://ithelp.ithome.com.tw/upload/images/20200929/20106426yZsB3ARaci.jpg

自己認為學會一項新技能之後最興奮的就是怎麼運用到專案了,這篇會用每一個網站都需要處理的"串接 API "為例。從發 request 去要 API 資料一直到拿完資料並整型成 UI 想要得樣子,這中間有可能發生的錯誤實在太多了,所以導入 Either Monad 我覺得非常適合。

假如今天 API 回傳資料如以下

// response
{
  data: [ 
    { 
      email: 'foo@example.com', 
      'user-name': '34Cindy', 
      phone: '#465-876?'
    },
    { 
      email: 'foo!example?dd', 
      'user-name': '3-jsindy', 
      phone: '465-876-998'
    },
    { email: 'foo@example', 
      'user-name': 'Hindy', 
      phone: '46587676'
    }
  ]
}

我們的目標是要由前端 transform 成以下,最後再 render 到 DOM 上面

{
  data: [ 
    { 
      email: 'foo@example.com', 
      userName: 'CINDY', 
      phone: '465876'
    },
    { 
      email: 'foo@example', 
      userName: 'JSINDY', 
      phone: '465876998'
    },
    { email: 'foo@example', 
      userName: 'HINDY', 
      phone: '46587676'
    }
  ]
}

一般思維

可能會用 map 然後

  • Object key 變 camelCase
  • email regex 成想要格式
  • userName regex 成想要格式
  • phone regex 成想要格式
  • 再合併成想要樣子
/*
 Part A 
*/

// ... 代表簡略 
response.map(list => {  
  // list key 變 camelCase
  ...
  // 一個一個 regex  
  let newEmail = list.email.match(regex) ...  
  let newName = list.userName.match(regex) ...  
  let newPhone = list.phone.match(regex) ...   
  return `${newName} / ${newEmail} / ${newPhone}` 
})

然後另外要考慮 Edge Case

  1. 空值狀態
  2. API 失敗
  3. 錯誤狀態
    https://ithelp.ithome.com.tw/upload/images/20200929/20106426JC8ZlMDFsH.jpg
    (↑ 沒考慮 Edge Case,一爆錯誤整個網站都壞掉; 或是畫面變很空)

https://ithelp.ithome.com.tw/upload/images/20200929/20106426fRpYkUBNuA.jpg
(↑ 有考慮 Edge Case,爆錯但可以引導使用者去別的地方)

/*
 Part B
*/ 

fetch(url).then((response) => {   
  if (response.ok) {     
    return response.json();   
  } else {   
    // 處理錯誤 1   
  } }) 
.then((res) => {  
  if(list.length > 0){    
    response.map(list => {    
      // Part A  
      // Part A 也要處理錯誤
    })  
  } else {   
    // 空值要做的事 
  }   
.catch((error) => {   
  // 處理錯誤 2 
});

我相信大家跟我一樣很熟悉以上寫法,這一包 Function 只適用當下 Page / Component,程式裡面也參雜了許多判斷式去判斷各種不同情況 (是不是空值、API 有沒有錯、API 回傳值是不是想要的格式等等)

Functional Programming 思維

讓我們先想一下需要步驟

  1. request API (成功的話往下繼續,失敗跳出)
  2. 把 key 值從 snakeCase user_name 統一變 camelCase userName
  3. validate data,驗證 API 回傳格式是對的
  4. 處理 no data state,也就是網頁通常會處理的空值,沒有資料回傳下 UI 會有一種樣子
  5. transform Data 整型成 UI 要的樣子

以上每個步驟都有可能有兩種結果

  • Success (Happy Path): 繼續往下進行下一個步驟
  • Failed (Sad Path): 跳出並執行你想要顯示的錯誤訊息

https://ithelp.ithome.com.tw/upload/images/20200929/201064261Cl8UkelQG.jpg

若從頭到尾都很順利,那就是一路運算下去
https://ithelp.ithome.com.tw/upload/images/20200929/20106426QfWPNxOZdz.jpg
若中間突然有一 Failed,例如 3. validate data,就會跳過後面步驟直接顯示客製化錯誤訊息
https://ithelp.ithome.com.tw/upload/images/20200929/20106426Ik8y6gDl1S.jpg
若一開始就 Failed,同理可證,會略過接下來所有步驟直接顯示客製化錯誤訊息
https://ithelp.ithome.com.tw/upload/images/20200929/20106426LrsER4HZej.jpg

原始碼

下面會是程式碼大概的樣子,裡面有很多 utilities 但這些都是可以不斷 reuse 的,例如 getPropcheckNoDataStatehandleError... 等,程式碼也相當好閱讀與理解,這就是 FP 吸引人的地方吧!

const validateData = data =>
    pipe(
    isEmail('email'),
    isPhoneNumber('phone'),
    isName('userName'),
)(data[0]).map(() => data);

/**
 * Validates & Transforms Data
 *
 * @param {ResponseState} response
 * @return {ResponseState}
 */
function transform (response) {
    return Box(response)
    .chain(getProp('data'))
    .chain(checkNoDataState(x => x.length === 0))
    .chain(validateData)
    .map(transformData)
    .then(
        (transforme) => ({
            ...response,
            data: transforme
        }),
        handleError(response)
    );
}

這邊有 hardcore-functional-js 課程提供串接 weather API 資料原始碼可以參考 ~~


如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您

歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。


上一篇
Either Monad: 更優雅的除錯
下一篇
Functional Programming 之未完待續心得篇
系列文
Functional Programming in JS30

尚未有邦友留言

立即登入留言