軟體設計中,「簡單性」與「靈活性」往往看似對立,實則互補。許多開發者傾向於過度設計,以「未來可能需要擴展」為由引入不必要的複雜度。本文透過一個具體的字串處理函數範例,深入探討如何在兩者之間做出明智抉選。
假設需將 home.example.net!{sm=1}user@other.example.net 轉為 home.example.net!user@other.example.net,即移除花括號及其內容。圖 1 採用 sscanf 搭配正則表達式格式字串分離前後段,再用 sprintf 拼接:
- void decoration_remove (char _user_name[])
- {
- char realm [NAME_MAX];
- char user [NAME_MAX];
- sscanf (_user_name, "%[^{]", realm);
- sscanf (_user_name, "%*[^}]%s", user);
- sprintf (_user_name, "%s%s", realm, &user[1]);
- return 0;
- }
雖然能達成功能,但代碼審查時被指出「實現過於複雜」。支持者辯稱「這樣更有靈活性」。
簡單性不僅是代碼行數少,更包含:
圖 1 的寫法雖行數不多,卻引入正則表達式語法,且雙重 sscanf 增加處理器負擔。
判斷一個設計是否真正具備靈活性,須自問:「採用靈活性設計的依據是什麼?」
靈活性不能憑主觀感覺,必須找到支撐它的實際需求。若需求不存在,所謂靈活性往往毫無意義,反而帶來未來的不確定性。上例中,正則表達式無法應對輸入格式變更(如花括號巢狀、多組花括號等),一旦格式改變,仍須重寫函數,故其靈活性站不住腳。
圖 2 改用指標遍歷字串,找到花括號後將後續字元前移覆蓋:
- void decoration_remove (char _user_name[])
- {
- char *p_char = _user_name, *p_from, *p_to;
- while (*p_char != 0) {
- if (*p_char == '{') {
- p_from = p_char;
- }
- else if (*p_char == '}') {
- p_to = p_char ++;
- goto found;
- }
- p_char ++;
- }
- return;
- found:
- while (*p_to ++ != 0) {
- *p_from ++ = *p_to;
- }
- }
此實現:
當簡單性與靈活性必須取捨時,問自己兩個問題:
靈活性設計常是優美的陷阱。抵禦誘惑的方法,正是持續追問上述兩個問題。切記:用直截了當的方式解決問題,不等同於「不專業」。高質量的設計,是在追求簡單的過程中,按需求逐步演進出適度靈活性的結果,而非一步到位的過度架構。