← 開發日常

避免隱晦的程式邏輯 - Index

Article image

在開發過程中,我們會使用各種資料結構來表示不同類型的資料,例如 List、Set、Map 等。List 作為一種有序的元素集合,允許重複元素,並提供基於索引(Index)的存取方式,適用於需要保持元素順序的場景。在程式中,我們經常需要與 List 互動,例如取出所有資料來顯示,或對 List 進行篩選 (where) 與轉換 (map) 等操作。

大多數情況下,對 List 的操作並不複雜,程式碼的可讀性也不會受到影響。然而,某些特定的寫法可能會降低可讀性,尤其是當我們使用索引來存取 List 時,可能會帶來潛在的問題。今天我們就來看看這些情況。

使用 Index 表示資料

在較舊的開發方式中,為了避免 API 直接暴露資料的屬性,可能會讓 API 回傳一個 JSON 陣列,其中每個位置代表某個特定的資料屬性。例如:

plain text
[123, "Jonh", 27, "[email protected]", "male"]

當客戶端接收到這樣的回應時,必須透過 Index 來解析資料:

plain text
Future<User> getUser() async {
  final data = await api.get("/user").data;
  return User.from(
    id: data[0],
    name: data[1],
    age: data[2],
    email: data[3],
    gender: data[4],
  );
}

類似地,當某個方法需要回傳多個值時,我們可能會選擇用 List 來存放結果,讓呼叫端透過 Index 來取出對應的值:

plain text
Widget build(BuildContext context) {
  final results = summarizeFee(orders);
  return Column(
    children: [
      Text("Price ${result[0].toString()}"),
      Text("Fee ${result[1].toString()}"),
    ],
  );
}

List<int> summarizeFee(List<Order> orders) {
  var price = 0;
  var fee = 0;

  for (var order in orders) {
    price += order.price;
    fee += order.fee;
  }

  return [price, fee];
}

在這些例子中,當我們閱讀 data[0] 或 results[0] 時,完全無法從字面上理解它們的意義,必須對照命名參數或其他使用處,才能弄清楚這些 Index 所代表的資訊。

那要如何解決呢?

以第一個例子來說,即便使用 List 避免了直接暴露屬性名稱,但這種方法終究是「防君子不防小人」,沒有太大的實際意義,就盡量避免使用。而對於回傳多個值的情境,我們可以改用更具可讀性的方式,例如使用具名類別或 Record 來改善可讀性:

plain text
({int price, int fee}) summarizeFee(List<Order> orders) {
  var price = 0;
  var fee = 0;
  for (var order in orders) {
    price += order.price;
    fee += order.fee;
  }
  return (price: price, fee: fee);
}

接著我們來看看另一個 Index 造成的問題。

Index 判斷

在以下程式碼中,我們根據 index 是否為 0 來決定是否要顯示標題:

plain text
Widget build(BuildContext context) {
  return Column(
    children: users.mapIndexed((index, user) {
      return Column(
        children: [
          if (index == 0)
            Text("User List Title"),
          Text(user.name),
        ]
      );
    }).toList(),
  );
}

乍看之下,這段程式碼可能不容易理解為何會有這個判斷條件。但細看後會發現,這是為了讓標題只顯示一次。

與其依賴 Index 來決定顯示標題的時機,我們可以直接將標題作為 Column 的第一個子元素,如下所示:

plain text
Widget build(BuildContext context) {
  return Column(
    children: [
      Text("User List Title"),
      ...users.map((user) {
        return Text(user.name);
      }),
    ],
  );
}

這樣的寫法不僅更直覺,也更清楚地表達了「標題應該只顯示一次,並位於使用者名稱清單的最上方」。

然而,這仍然不完全等價於原始程式碼。原始版本的邏輯還包括「當 users 為空時,不應顯示標題」。因此,我們可以進一步調整程式碼,使其更明確地表達這一邏輯:

plain text
Widget build(BuildContext context) {
  if (users.isEmpty) {
    return SizedBox.shrink();
  }

  return Column(
    children: [
      Text("User List Title"),
      ...users.map((user) {
        return Text(user.name);
      }),
    ],
  );
}

如此一來,讀者能夠一眼看出程式的意圖:標題僅在 users 非空時顯示,並且只會出現一次。

小結

使用 Index 並非絕對不妥,而是應視情境而定。我們應該考慮索引是否真正表達了程式的意圖,還是讓讀者需要額外推理才能理解它的用途。透過適當的資料結構與語法特性,我們可以讓程式碼更易讀、更容易維護。