EP.04砍掉重練筆記

砍掉重練 #1:一個角色的生與死

上一篇結尾我說,好設計不是想出來的,是砍出來的。

這句話聽起來像金句,但它是我付過學費才換到的。一開始的「設計」,其實是在「猜」—— 猜使用者要什麼、猜哪個做法是對的、猜這個彈性以後用得到。而真正的設計,是在這些猜測撞上現實之後,我有沒有勇氣把猜錯的部分砍掉。留下來的,才叫設計。

第一刀,我砍的是一個我自己發明的角色:CO_EDITOR。它活了一陣子,然後我殺了它。

它是怎麼誕生的

事情從一個很單純的需求開始:計劃書要能多人一起編。

一個計劃案不會只有一個人碰。主持人要寫、可能要找人幫忙補資料、客戶那邊也要參與。我想著:「這簡單,做一個『共同編輯者』角色就好,主持人可以把任何人加進來,一起編這份計劃書。」

聽起來很合理,而且它很通用。不管誰要進來幫忙,通通給他「共同編輯者」,非常彈性。

於是我把整套做了出來:可以指派、可以列出有哪些人、可以移除;後端有專屬權限,前端有一整個分頁,資料庫的角色種類裡多了一個 CO_EDITOR。我甚至把「一個案子要開始行政預審」的前置條件,設成「至少要有一位共同編輯者」。

一切看起來很完整。真是令人滿意。

但現實不是這樣運作的

沒有人來糾正我。是我自己在一次又一次使用的過程裡,慢慢想通的。

真正的協作關係,根本不是「一個主持人,拉進一堆權責都一樣的共同編輯者」。實際流程裡,每個人的角色是明確而不對等的:

  • 有負責整個案子的主持人(PI)
  • 有內部的研究主持(SD),他是行政預審能不能啟動的關鍵關卡
  • 有客戶端的成員,他們的權限跟內部人員本來就不一樣

而我發明的那個「共同編輯者」,聽起來很萬用,卻對不上現實裡的任何一個人。研究主持不是「一個普通的共同編輯者」,客戶也不是。用一個籠統的標籤,蓋掉了真實世界裡重要的權責差異,這是錯誤的。

它太通用了。而過度通用,往往是「我還不夠了解這個問題」的遮羞布。當我不確定真實流程長怎樣,我就發明了一個什麼都能裝的空盒子,然後騙自己「有彈性」。

現在的角色,是怎麼切的

想通之後,我把那個空盒子拆掉,改成照真實權責一刀一刀切。這部分我想講細一點,因為「範圍怎麼切」才是這一刀真正的產出。

第一刀:把「是不是成員」和「負責什麼」分開。 計劃書的成員清單裡,現在只剩兩種身分:PI(主持人) 和 CLIENT(委託人)。而 SD(研究主持)不在成員清單裡,他是計畫上的一個欄位,一個案子只會有一位,這個差別很重要!成員是「一群人」,SD 是「一個責任歸屬」。我當初把責任歸屬硬塞進成員清單,才會需要發明一個模糊的角色來裝它。

第二刀:把「能看」和「能改」分開。 草稿階段能動筆的,只有 PI、SD、系統管理員。CLIENT 和內部的執行秘書,對草稿是唯讀的,看得到,但改不了。我一開始沒分這麼細,覺得「反正都是自己人」。後來才懂,權限設計的價值不在擋壞人,而在讓每個人清楚知道「這份文件現在該由誰負責」。

第三刀:把「協調」和「編輯」分開。 執行秘書和管理員可以幫忙指派 SD(這是行政協調),但同一個人不能因此就去改標題、改表單、改日期(那是內容編輯)。這兩件事以前綁在同一道權限閘後面,現在拆成兩道,能協調的,不代表能動內容。

第四刀:把「變更申請」收得更緊。 案子核准後要改東西,得走變更申請。這件事只有 PI 能做,SD 也不行。因為變更申請是對外的正式承諾,責任必須落在一個明確的人身上。

我們會發現,這四刀切完之後,「共同編輯者」該做的事全部有了歸屬。內部的協作由 SD 接手,對外的參與是 CLIENT,而沒有留下任何空缺。一個設計如果砍掉之後什麼都不缺,那它本來就不該存在。

砍掉它,比做出來還難

發現錯了,那就砍。但砍,比當初做出來還費工。

把「共同編輯者」從系統裡連根拔除:拿掉角色定義、刪掉指派/列出/移除的所有功能與後端程式,刪掉它專用的權限、砍掉前端那整個分頁,並把「行政預審要先有共同編輯者」的規則,改成「要先指派研究主持」。

而且這一刀得分兩次砍:第一次先把它的編輯權收回來(讓它變成一個沒有實權的空頭銜),第二次才真的從資料庫裡把這個角色連根拔掉。一個活著的角色底下可能還掛著真人資料,不能一刀切下去就讓系統當場說「查無此角色」。

光是最後那一次,就動到 34 個檔案,刪掉七百多行、只補回一百多行,淨刪六百多行。而當初做它的時候,大概只覺得是「加一個角色」而已。

最後那一刀還碰到一個很有意思的技術細節:資料庫裡「角色有哪幾種」是一個列舉型別,而 PostgreSQL 不支援直接從列舉裡拿掉一個值。所以要刪掉 CO_EDITOR,得把整個型別重蓋一次 —— 把舊的改名、建一個只含 PICLIENT 的新型別、把資料欄位整欄轉過去、再把舊的丟掉。連刪掉一個選項,都得把整個型別重砌一遍。

我覺得這件事本身就是個很好的隱喻:一個設計一旦長進系統裡,它會在權限、流程、資料、通知裡到處生根,拔的時候每一處都得跟著動。這也是為什麼很多人明知道一個設計錯了,卻不敢砍:因為砍的成本是當下就要付的,而留著的爛,是以後才慢慢還的。

寧可當下痛一次。留著一個對不上現實的角色、假裝它有用,只會讓後面每一個接手的人都被它絆一次。

我後來想:砍掉的勇氣,其實比做出來的能力更稀缺。做出來只需要一股衝勁,砍掉卻要先承認自己錯了,還得當下就付錢。

一道刪不掉的疤:歷史怎麼保住

這裡是這整件事最想分享給二代們的部分。

以為「連根拔除」就是全部刪光。但有一種東西,不能刪。那是歷史。

在那個角色還活著的那段時間,系統忠實記錄了「某某某在某時、以共同編輯者的身分做了某事」。這些是稽核紀錄,是要面對查核的。如果為了刪乾淨,把跟這個角色有關的紀錄型別也一起砍掉,那些舊紀錄就會讀不出來、當場壞掉。

所以最後的處理是:面向未來,這個角色死了、不能再指派;面向過去,所有跟它有關的歷史紀錄原封不動保留。系統裡到今天還留著「指派共同編輯者」「移除共同編輯者」這兩種事件型別,只讀不寫,專門用來讀懂那段已經結束的歷史。它們就是那道疤。

既然講到這,順便把「操作紀錄到底怎麼做」講清楚。這是二代們接手系統時最容易輕忽、被查核時卻最要命的一塊。

第一層:每一筆都記,而且只能往後加。 誰、在什麼時間、對哪一筆資料、做了什麼、改前長怎樣、改後長怎樣,全部落成一列紀錄。這張表只進不出。資料庫層直接掛了兩道防線,任何人想更新或刪除既有紀錄,資料庫會直接把操作擋回去並丟出錯誤。不是靠程式碼自律,是靠資料庫本身不給這個選項。為什麼要做到資料庫層?因為應用程式的保護,只擋得住「從正門進來的人」。真正該防的是有人拿著資料庫帳號直接下指令,手滑也好、被入侵也好,最後一道防線必須設在資料本身旁邊。

第二層:每一筆紀錄,都跟前一筆綁在一起。 每寫一列紀錄,系統會拿這列的內容加上「前一列的指紋」,一起算出這列自己的指紋。一列扣一列,串成一條鎖鏈。這樣做的用意是:如果有人偷偷改了中間某一列(或抽掉一列),那一列之後的指紋就全部對不上,鏈會斷在他動手的地方。這讓資料沒辦法只改一列而不被發現,除非把之後所有的紀錄全部重算,而那需要一把只有系統握著的鑰匙。

這條鏈的真正價值,不是「防止被改」,是「改了一定看得出來」。合規要的從來不是絕對安全,是可驗證。系統另外有一支專門的驗證程式,可以隨時把整條鏈從頭走一遍,回報有沒有斷點。

第三層:電子簽章比照辦理。 簽下去的東西完全不能刪,核心欄位也不能改。唯一允許的動作是「作廢」。而作廢本身也會留下一筆新紀錄,寫明是誰、什麼時候、為什麼作廢。可以推翻一個簽章,但不能假裝它沒發生過。

回到那個被我殺掉的角色:正因為有這三層,我才敢放手砍。因為我知道,不管我把系統改成什麼樣子,它做過的事都還在紀錄裡,誰也擦不掉。我們可以殺死一個設計的未來,但殺不掉它的過去。而在合規的世界裡,我們的責任不是把疤抹平,是讓它清清楚楚地留著。

我從這一刀學到的三件事

  1. 當發明一個現實裡不存在的角色,通常是還沒搞懂現實。動手做之前,先把真實流程裡「到底有哪些人、各自能做什麼」問清楚。這正是我現在最常叫 AI 幫我做的事:先問我問題,把需求問清楚,別急著寫。
  2. 「通用、有彈性」不一定是優點,有時候是逃避。一個什麼都能裝的設計,常常代表還沒想清楚它到底要裝什麼。具體,往往比通用更誠實。
  3. 敢砍的前提,是歷史留得住。如果操作紀錄是可以被隨手改掉的,那每一次重構都會變成一次信任的流失。先把稽核的地基打好,才有本錢一刀一刀往下砍。

一個獸醫,為什麼要會這些?也許答案就是:因為只有親手做過、也親手砍過,我才真的懂了「設計」這兩個字。

好設計是砍出來的。而我,還會繼續砍。

← 回到文章列表