今天是 function 時空旅行第二天,昨天我們學會了如何收拾背包(?),也學會不要讓背包裡的東西變質(?)。。。總之就是學會了關於 function 參數的另一種使用方式,提高可維護性。
今天要來談談關於 function 的拆解與命名:
跟昨天一樣,先來個範例看看吧:
// 送出編輯個人資訊的表單
/*
 * name  : 姓名
 * age   : 年齡
 * gender: 性別
*/
const requiredFields = [
    'name', 
    'age', 
    'gender'
];
const submitForm = (value) => {
    const formFields = Object.keys(value);
    const valid = requiredFields.every(key => {
        return formFields.includes(key) && typeof value[key] !== 'undefined'
    });
    
    if(!valid) {
        console.log('尚有必填欄位未填');
        return;
    }
    
    const nameSplitList = value.name.split(' ');
    const submitValue = { 
        firstName: nameSplitList[0],
        lastName: nameSplitList[1],
        age: Number(value.age),
        gender: value.gender
    };
    
    fetch('my-backend-API', {
        method: 'POST',
        body: JSON.stringify(submitValue)
    });
};
const formValue = {
    name: 'yc chiu',
    age: '20',
    gender: 'male'
};
submitForm(formValue);
這是模擬編輯個人資訊的表單,可以填上姓名、年齡、性別三個欄位,按下送出表單的時候,會做到以下三件事:
只有三個欄位,其實算是一個相對簡單的範例,但可以看到要送出表單時,submitForm 多達 22 行(而且 fetch 還被我偷懶簡化過)。
如果同樣的目的,我們將 submitForm 拆解,分別用以下的 function 來處理:
validateFormData
prepareSubmitData
postPersonData
// 送出編輯個人資訊的表單
/*
 * name  : 姓名
 * age   : 年齡
 * gender: 性別
*/
const requiredFields = [
    'name', 
    'age', 
    'gender'
];
const validateFormData = (formData) => {
    const formFields = Object.keys(formData);
    return requiredFields.every(key => {
        return formFields.includes(key) && typeof formData[key] !== 'undefined'
    });
};
const prepareSubmitData = (formData) => {
    const nameSplitList = formData.name.split(' ');
    return { 
        firstName: nameSplitList[0],
        lastName: nameSplitList[1],
        age: Number(formData.age),
        gender: formData.gender
    };
};
const postPersonData = (submitData) => {
    fetch('my-backend-API', {
        method: 'POST',
        body: JSON.stringify(submitData)
    });
};
const submitForm = (value) => {
    const valid = validateFormData(value);
    if(!valid) {
        console.log('尚有必填欄位未填');
        return ;
    }
    const submitData = prepareSubmitData(value);
    postPersonData(submitData);
};
const formValue = {
    name: 'yc chiu',
    age: '20',
    gender: 'male'
};
submitForm(formValue);
沒錯,改完之後程式碼更長了(傻眼),但我們換到什麼呢?
雖然程式碼更多了,但對於剛接手這份 code 的人來說,其實讀懂的速度更快了。
重點在於我們將 submitForm 這個 function 裡面,原本混雜沒有界線的邏輯,使用幾個小 function 切割開來,強迫這些邏輯拆散,就不再是一大坨程式要一行一行讀。
不過要強調的是,增加可讀性不是一定要拆 function 才做得到,比較懶人一點做法,可以加上註解:
const submitForm = (value) => {
    // 檢核必填欄位
    const formFields = Object.keys(value);
    const valid = requiredFields.every(key => {
        return formFields.includes(key) && typeof value[key] !== 'undefined'
    });
    
    if(!valid) {
        console.log('尚有必填欄位未填');
        return;
    }
    
    // 將欄位值轉換為後端需要的欄位與格式
    const nameSplitList = value.name.split(' ');
    const submitValue = { 
        firstName: nameSplitList[0],
        lastName: nameSplitList[1],
        age: Number(value.age),
        gender: value.gender
    };
    
    // 發送 API request 到後端
    fetch('my-backend-API', {
        method: 'POST',
        body: JSON.stringify(submitValue)
    });
};
這是用註解也辦不到的事,是 function 的一大賣點,如果同樣或相似的邏輯,出現兩次就該考慮是否寫成 function 了,出現三次就要寫檢討報告了(?)
因為 function 的可重用性,可以大幅減少不必要的重複 code,feature 修改時只要修改一個地方,不會漏掉;要做測試的時候,也能保證結果相同,同時也提升了可維護性。
重點來了,什麼時候會需要放到 function 重用?
其實。。。單純就是。。。出現太多次的時候((拖走
比方說常用來發送 API request 的 axios 或 fetch,就非常適合包進 function 裡面重用(部分大寫變數,不是本次重點,可自行體會XD):
const callApi = async(endpoint, method = 'get', body) => {
  const requestUrl = `${API_URL}/${endpoint}`;
  const options = {
    headers: {
      'content-type': 'application/json',
      Authorization: TOKEN,
    },
    method
  };
  
  if (body) {
    options.body = JSON.stringify(body);
  }
  
  try {
    const response = await fetch(requestUrl, options);
    if (response.ok) {
      const json = await response.json();
      return json;
    }
    return Promise.reject(response);
  } catch (error) {
    throw new Error(error);
  }
};
呼叫時都只要一行
// GET
const productList = await callApi(`/product`);
// POST
const productResult = await callApi(`/product`, 'post', data);
當然,光是拆散還不夠,如果把這些小 function 命名成 apple、banana 之類的名字,肯定也是看不懂的(應該說更加不懂),因此好的命名絕對是非常加分的!
而 function 的命名,某種程度上算是一種團隊風格,只要團隊中成員都能夠好讀、讀懂。唯一的衡量標準應該就是 predictable,容易預測、容易猜到這個 function 要做什麼,就是好命名。
而我自己遵守的主要是以下幾點:
這點算是非常好理解也容易上手,駝峰式就像是駱駝的背一樣,凹下去凸起來凹下去凸起來,小寫大寫小寫大寫,所以比起全小寫還容易閱讀。
其實照這樣說,手握拳的時候,手指根部的四個關節也是凹下去凸起來啊,怎麼不叫指關節式命名(?),是因為多一個字嗎(?)
若是遇到縮寫,則可以考慮使用底線(_),雖然沒有很建議,但也是個辦法。
所以大概是這樣:
applePie
bananaFish(?)
toDoList
HTML_Parser
function 本身就是用來「執行」一些任務的,所以必然是動詞開頭,而後面接名詞則構成一個基本的語句(主詞大概是 user 吧!)。如果 V. + N. 的組合還不夠清楚,還可以加上一些修飾詞:
validateFormData
getProductList
findUserById
確保每個動詞都意義一致,尤其是一些翻成中文相似的動詞:
patch 用於部分更新
put 用於替換
fetch 用於發送 request
get 取得的萬用字(?)
function 經過拆解之後,重新命名給予邏輯意義,雖然功能都一樣,但是當程式規模愈大,就愈能看出這樣做的好處,下次當你有以下的感覺,不妨好好考慮「拆解+命名」吧!
離開了熟悉的家鄉
改名換姓
在銀河的另一端相遇
認識一些常見的動詞,對於命名 function 還蠻有幫助的!很不錯的分享 :)
太棒了~ 不過命名在這篇算是一小部分而已,我有點想要拉出去特別寫一篇跟大家討論,覺得真的很重要!
名字取得好,看 code 沒煩惱XD