搞錯了資料結構,寫的就不是程式碼,是日後要爆炸的地雷。
Bad programmers worry about the code. Good programmers worry about data structures.
爛程式碼都在擔心邏輯,好程式碼都在擔心資料結構。
當你的資料結構設計正確時,邏輯就會變得簡單;反之,你的資料結構設計錯誤時,邏輯就會變得複雜。
不知道用什麼,就先用個陣列,這是典型的懶人思維。
後果就是接下來寫的每一個函式,都得為這個不好的決定付出代價。
class UserManager {
constructor() {
// 用陣列來存需要頻繁查找的資料,這是個災難的開始。
this.users = [];
}
// 每一個函式都必須做一次 O(n) 的全陣列掃描。
// 使用者少的時候沒感覺,使用者一多,你的系統就等著癱瘓吧。
findUserById(id) {
for (let i = 0; i < this.users.length; i++) {
/* 掃描... */ }
}
updateUser(id, updates) {
for (let i = 0; i < this.users.length; i++) {
/* 又是掃描... */ }
}
deleteUser(id) {
for (let i = 0; i < this.users.length; i++) {
/* 掃描掃描掃描...有點煩.... */ }
}
}
上面每一行 for
迴圈,都是在為一開始的錯誤決策擦屁股。
你需要根據 ID 快速查找?
那就用一個以 ID 為鍵的 Map (雜湊表),做好資料結構與索引結構。
// 使用 Map 儲存使用者資料,邏輯變得簡單
class UserManager {
constructor() {
// 需要用 ID 查?準備一個用 ID 當 key 的 Map。
this.usersById = new Map();
// 需要用 Email 查?再準備一個用 Email 當 key 的 Map。
this.usersByEmail = new Map();
}
addUser(user) {
// 寫入時多花一點點力氣,維護好索引。
this.usersById.set(user.id, user);
this.usersByEmail.set(user.email, user);
}
// 現在查找操作變成了 O(1),瞬間完成。
findUserById(id) {
return this.usersById.get(id) || null;
}
findUserByEmail(email) {
return this.usersByEmail.get(email) || null;
}
// 更新和刪除也一樣,先快速定位,再操作。
// 看看這裡,還有 for 迴圈嗎?沒有了。
// 簡單的邏輯是正確資料結構的自然結果。
deleteUser(id) {
const user = this.usersById.get(id);
if (!user) return false;
this.usersById.delete(id);
this.usersByEmail.delete(user.email);
return true;
}
}
忘掉那些空泛的「原則」。
在定義任何類別或函式之前,先盯著你的資料,回答下面這幾個問題:
最頻繁的操作是什麼?
是查找、插入、還是遍歷?如果你需要頻繁地根據某個 key 查找一個項目,還用陣列,那你就是在自找麻煩。
一旦你把同一個資訊存在兩個地方,你就已經為自己埋下了一顆 Bug 定時炸彈。
總有一天,你會在更新一個地方時,忘了更新另一個。
永遠不要儲存可以被計算出來的衍生資料。
CPU 很快,計算 array.length
的成本,遠比你花費數小時去除錯一個資料不同步的 Bug 要低得多。
🔴 錯誤:手動同步狀態
一個訂單列表,又單獨存一個 orderCount
變數
class OrderSystem {
constructor() {
this.orders = [];
this.orderCount = 0; // 這個變數就是一顆地雷。
}
addOrder(order) {
this.orders.push(order);
this.orderCount++; // 如果忘了這行呢?
}
}
🟢 正確:從唯一事實來源計算
class OrderSystem {
constructor() {
this.orders = new Map(); // 單一、可靠的資料來源。
}
// 需要數量?直接從來源計算。永遠正確。
getOrderCount() {
return this.orders.size;
}
}
資料不是孤立存在的。評論屬於文章,訂單項目屬於訂單。
你的資料結構必須反映這種關係。
如果你的 posts
和 comments
是兩個獨立的大陣列,那麼當你刪除一篇文章時,就極有可能忘記刪除它下面的所有評論,製造出一堆沒人要的「孤兒資料」。
🔴 錯誤:分離的資料
class BlogSystem {
constructor() {
this.posts = [];
this.comments = [];
}
// 刪除文章時,很容易忘記去 comments 陣列裡清理垃圾。
deletePost(postId) { /* ... */ }
}
🟢 正確:結構反映關係
class BlogSystem {
constructor() {
this.posts = new Map(); // post.id => { postData, comments: [] }
}
// 把評論直接放在它所屬的文章物件裡。
addComment(postId, comment) {
this.posts.get(postId)?.comments.push(comment);
}
// 刪除文章時,所有相關的評論也跟著被一併刪除。不可能產生孤兒資料。
deletePost(postId) {
this.posts.delete(postId);
}
}
如果說複雜的函式是程式碼的「臭味」,那混亂的資料結構就是產生臭味的「腐敗源頭」。
先處理源頭,臭味自然消除。