閱讀筆記:Good Code, Bad Code Chapter 7 Make code hard to misuse

總結

此篇是 Good Code, Bad Code: Think like a software engineer 第七章(Make code hard to misuse)的閱讀筆記,本章提出以下幾點「讓程式碼難以被誤用」的實作建議:

  1. 使物件不可被修改(immutable)
  2. 避免過於籠統的型別定義
  3. 謹慎對應時間資料
  4. Single source of truth: 資料與邏輯

筆記

使物件不可被修改

以 React 專案為例,如果要實作此建議,常見做法為搭配 lodash/cloneDeep 或透過 immer/produce 來避免「直接更動某一份資料的內容」。

參考以下範例:

import React, { memo, useState, useCallback, useEffect } from 'react';
import cloneDeep from 'lodash';

interface InputValue {
  id: string;
  value: string;
}

function InputComponent(): React.ReactElement {
  /* States */
  const [inputValue, setInputValue] = useState<InputValue[]>();

  /* Functions */
  const onInputChange = useCallback(
    (id: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
      const cloneState = cloneDeep(inputValue);
      const inputIndex = cloneState.findIndex((input) => input.id === id);
      if (inputIndex > -1) {
        cloneState[inputIndex].value = e.target.value;
      } else {
        cloneState.push({ id, value: e.target.value });
      }
      setAnswer(cloneState);
    },
    [inputValue]
  );

  /* Hooks */
  useEffect(() => {
    // 根據 inputValue 執行其他操作...
  }, [inputValue]);

  /* Main */
  // 渲染 input 元件...
}

當工程師需要透過 setInputValue 修改 inputValue 時,會先對 inputValue 執行一次深拷貝取得 cloneState,接下來所有的改動都是發生在變數 cloneState 上,原本的 inputValue 不參與「被拷貝」以外的任何工作。

這能確保此元件 useEffect 所依賴的 inputValue 不會在 onInputChange 被執行的途中被意外修改內容,導致預料外的 side effect 發生。

避免過於籠統的型別定義

書中範例為「二維座標資料陣列」:

type LocationInfo = number[];

const location1: LocationInfo[] = [
  [42, 37],
  [6, 24],
  [], // 無效座標,但 TypeScript 檢查不出來
  [92, 40, 3], // 無效座標,但 TypeScript 檢查不出來
];

本書認為 LocationInfo 這樣的型別定義過於寬鬆,基本上這個型別無法提供「關於座標這類資料的具體訊息」。其他工程師看到這個型別只知道 LocationInfo 會是一組數字陣列,卻無法認知到「一組座標陣列必須且只能包含兩個數字(分別代表 X 與 Y 軸)」這個需求。

建議:只要有可能,就使用更精確的型別定義內容。

type LocationInfoBetter = [number, number];

const location2: LocationInfoBetter[] = [
  [42, 37],
  [6, 24],
  [], // 現在 TypeScript 會報錯
  [92, 40, 3], // 現在 TypeScript 會報錯
];

謹慎對應時間資料

本書關於「處理時間類型資料」的建議如下:

Single source of truth: 資料與邏輯

本章指出程式碼中的資料可歸納為以下兩種:

以上述銀行帳戶資料為例,一個理想中的資料結構應該只需要包含存款與欠款(初級資料),不需要再立一個 class field 來保存餘額(衍伸資料)。工程師應只有在需要的時候,直接透過計算「存款減欠款」來得到餘額資料。

在計算十分耗能的情況下,本書建議導入 cache 以及 lazy calculate 等機制來避免不必要的效能浪費。


而除了資料上的 single source of truth 以外,本書也提醒工程師在開發時也要注意維持「邏輯上的 single source of truth」。亦即:一段功能(比如針對特定資料的 formatter 邏輯)是否也有保持單一來源準則。

舉例:假設今天前端在兩個元件中都需要使用一段後端回傳、且需要經過加工整理(format)的資料,這個資料的 formatter 就可以透過 custom hook 或是 util tool 的形式包裝,而不是直接把資料加工的邏輯散落在各個元件中。

良好的抽象層通常也能協助維持邏輯的 single source of truth 原則。

參考文件