這次的需求是:註冊時的姓名要能支援原住民的名字。原住民的名字不一定能分成一個字的姓和後面的名,有些還會用間隔號 · 連接,所以表單改成一個「姓名」欄位,但後端的資料還是要 first_name 和 last_name 兩個值。
我的第一個想法很直覺:寫一個函式,把使用者輸入的姓名拆開。兩個月後,我把註冊流程裡的這段邏輯刪了。
當時的拆解規則
我依照常見的姓名格式寫了幾條規則:
- 有間隔號
·或空白,就從第一個符號切開。 - 開頭是中文,第一個字當姓,其他當名。
- 其他情況,整串放進同一個欄位。
寫成程式大概是這樣:
js
function splitName(name) {
const trimmed = name.trim()
const index = trimmed.search(/[·\s]/)
if (index !== -1) {
return {
surname: trimmed.slice(0, index),
givenName: trimmed.slice(index + 1),
}
}
if (/^[一-龥]/.test(trimmed)) {
return { surname: trimmed[0], givenName: trimmed.slice(1) }
}
return { surname: trimmed, givenName: '' }
}
測試時輸入「王小明」,結果完全正確,看起來沒有問題。
問題在於規則永遠寫不完
姓名不是一種能用幾條規則描述的格式:
| 輸入 | 拆解結果 | 問題 |
|---|---|---|
| 歐陽小明 | 歐 / 陽小明 | 複姓被切開 |
| 范姜小明 | 范 / 姜小明 | 複姓被切開 |
| 張李小明 | 張 / 李小明 | 無法判斷是否為雙姓 |
| 瓦旦·達路 | 瓦旦 / 達路 | 順序不一定是「姓在前」 |
| John Smith | John / Smith | 英文是名在前 |
| Mary Ann Smith | Mary / Ann Smith | 中間名怎麼算 |
當然可以繼續加規則,例如放一份複姓清單,但「張李」這種情況,光看字串根本無法判斷。程式只是在猜,而猜錯的是使用者的名字。
最後的修法:團隊討論後決定不拆
原本的方向就是把姓名拆開存。後來團隊內部討論,對照資料庫的欄位後發現其實沒有必要拆分:後端收到姓和名之後,會直接接成一個完整姓名儲存。前端辛苦拆開,後端又接回去,拆分根本沒有帶來價值。
所以註冊時改成使用者打什麼,就存什麼:
js
// 之前
const { surname, givenName } = splitName(form.name)
// 之後
const payload = {
last_name: form.name,
first_name: '',
}
這次改動刪掉了 40 多行,只留下兩行。資料庫的欄位沒有調整,所以完整姓名放在 last_name,first_name 留空。
這不是最漂亮的資料結構,但系統不用再猜哪個部分是姓,原住民的名字也能完整保存。
什麼時候才需要拆
資料還是存完整姓名,真的需要分開時才拆。例如要顯示「王先生您好」,就在使用的當下,依照調整後的規則自動拆分:
- 有間隔號
·或空白時,從第一個符號切開。 - 沒有符號的中文姓名,以第一個字當姓。
- 判斷不了的情況,就顯示完整姓名。
和原本最大的差別是,拆分結果只用來顯示,不寫回資料庫。規則就算猜錯,完整姓名也還在,之後調整規則就能修正,不會留下拆錯的資料。
如果姓和名會影響重要用途,例如要和證件資料比對,就不適合靠規則判斷,應該讓使用者自己分開填寫。
這次學到的事
- 測試資料全部通過,不代表規則是對的。我只想到了「王小明」。
- 碰到人名、地址這類真實世界的資料,要先懷疑自己的假設。
- 當規則需要一直補例外時,先問這段程式是不是根本不該存在。
- 動手前先和團隊確認資料實際怎麼使用,討論之後常常會發現不用做那麼多。
- 刪程式碼也是一種修法,而且常常是最好的那種。