把問題放在動手前面#

預重構的核心精神是「左移」(shift left):在開始寫程式碼前,先想清楚需求、抽象、架構、測試與安全。流程不是瀑布式的線性走完,而是分析(analysis)與設計(design)持續互相回饋的迭代循環。

在 Sam 的範例中,當設計階段才意識到 CDDiscCDRelease 必須區分時,分析階段的需求自然得到澄清——這正是設計回饋分析的具體展現。

從使用案例描繪大局#

開始任何系統前,先用使用案例(use cases)整理出主要互動,並排出可推進的最小版本:

  • MFS(Minimum Feature Set,最小功能集):第一個正式版本應只包含必要功能。例如 Sam 的系統如果只有 check-out 而沒有 check-in,就完全無法使用,所以兩者都要納入 MFS。
  • 使用案例之間的關係:透過 extends 描述附加情境(例如 Show_availability 擴充 Search_catalogProvide_discounts 擴充 Checkout_a_CDDisc)。
  • 避免實作細節綁死:寫使用案例時應描述使用者意圖與系統反應,而非畫面控件或資料庫類型。

Figure 4-1. Use cases for Sam's system

不要陷入分析麻痺#

分析麻痺(analysis paralysis)指過度追求需求完備,永遠在「再多了解一點」中卡住。設計麻痺(design paralysis)則是堅持要把所有類別細節都想清楚才肯動手。

兩者的成因常見於:

  • 害怕遺漏:擔心遺漏某個情境或細節而不敢前進。
  • 失敗成本太高:組織文化讓人不敢冒險。
  • 流程強迫前置決策:把本應在後期才考量的問題硬塞到前期。

強行控制每個未知,會把焦慮包進流程,而不是真的減少風險。

化解之道:

  • 判斷哪些細節重要:界面文字、資料庫品牌通常不重要;概念之間的數量關係(一對一、一對多、多對多)通常很重要。
  • 從經驗校準感覺:回顧過往專案中「事後才浮現、卻造成大改」的細節,下次提早處理同類資訊。
  • 快速演練使用案例:用 UML 序列圖或 CRC 卡(Class-Responsibility-Collaboration)走一遍,能跑得通就先動手。

初始設計的方法#

名詞動詞法#

從需求或使用案例中找名詞做為類別或屬性,動詞做為責任或方法。但要小心:同一動作可以是名詞(rental)或動詞(rents),不要被語法綁住。

CRC 卡#

CRC 卡上記載:

  • 類別名稱。
  • 它的責任。
  • 它需要協作的其他類別。

可選地補上一行類別描述、潛在屬性或之前討論到的線索。CRC 卡之後可直接轉寫成介面,但 UML 類別圖比手寫更精確、更容易交流。

Figure 4-2. CRC cards for Sam's CD rental system

用 ADT 描述屬性#

初始設計時就用抽象資料型別(ADT, Abstract Data Type)標示屬性,例如:

CDRelease
    CommonString title
    UPCCode upc_code

CDDisc
    PhysicalID physical_id
    CDRelease cd_release
    rent(Customer customer)
    return_a_rental()

Customer
    CustomerID id
    Name name
    Address address
    PhoneNumber day_phone_number

這層抽象讓初始設計能直接表達「應該存什麼資料」「需要哪些操作」,而不被特定語言或資料庫綁住。

Figure 4-3. Class structure for Sam's CD rental system

隨著類別細節補齊,會延伸出更完整的系統類別圖:

Figure 7-1. Class diagram for the system so far

並對應每個 use case 的序列圖,作為實作前的最後檢查:

Figure 7-2. Checkout_a_CDDisc

Figure 7-3. Checkin_a_CDDisc

全局規畫,局部設計#

完成初版類別後,仍需簡單檢視其他使用案例,確認設計骨架能撐得住未來需求。

  • Search_catalog 與 Show_availability:主要影響 CDReleaseCDDisc,類別本身可能要改但骨架不動。
  • Report_when_CDDiscs_are_overdue:報表通常較簡單,只是讀取狀態,不會大幅改動類別。
  • Charge_CDDisc 與 Provide_discounts:與計費相關,會牽動現有類別,但延後實作可降低初版風險。

Plan Globally, Develop Locally——漸進式實作必須能放回整體大局。每一個短週期的工作都要對應到最終目標的某段路。

報表常常是最能揭露「使用者真正需要什麼資訊」的入口(reviewer Graham Oakes 的觀察)。資訊導向的系統,不妨先從報表規畫整體設計。

測試策略左移#

同步草擬功能測試#

設計階段就為每個使用案例草擬:

  • 正常情境(happy path):例如「正常租借」「正常歸還」。
  • 誤用情境(misuse cases):例如「重複租借」「客戶 ID 不存在」「未租借就還」「逾期歸還」。

If It Can’t Be Tested, Don’t Require It——任何功能需求若無法被測試,就無從驗證是否達成。

但「易用」「可維護」這類非功能性需求(the ilities)難以一刀切地寫測試,這條指南只限於功能性測試。

規格要可達#

測試規格不只要清楚,還要在環境條件下做得到。例:

  • 規格寫「兩秒內完成搜尋」,但網路頻寬只有 9600 baud,根本送不出結果——後來改成「兩秒內出現第一個字元,後續依頻寬」才合理。

軟體中的碎形#

同樣的設計模式會在不同尺度上重現(碎形原理):

  • 系統層級的 Input-Process-Output,可一直拆到子模組與類別。
  • 系統層級的 use case,對應到類別層級的「work case」(描述類別需要完成的工作)。
  • 系統層級的功能測試,會自然衍生出類別層級的測試。

Plan for Testing——預先規畫測試策略可帶出更好的設計。如果一個類別連 work case 都寫不出來,這個類別大概沒有存在意義。

測試的脈絡依賴#

某些方法只能在情境中測試。例如:

  • 只測 open() 沒有意義,必須搭配後續 read()write() 才知道是否真的開啟成功。
  • Rental 的 check-in 必須在已 check-out 的脈絡下測試。

測試的回饋循環#

回饋不只發生在開發,也發生在測試:

  • 收到 bug 後,先補一條測試以避免回歸。
  • 分析 bug 為何漏掉,例如「使用者同時開太多程式造成資源不足」,下一輪納入低資源環境的測試。

探索性測試補充用例覆蓋#

使用案例測試回答「能不能跑得起來」(capability),但「在各種輸入、狀態與長期條件下還能不能正常」(reliability)才是品質關鍵。

  • 用例測試是必要但不充足的。
  • 自動化測試也鮮少能單獨保證可上線。
  • James Bach 提倡「探索性測試」(exploratory testing):測試人員在過程中根據資訊持續設計新測試。

安全要從第一天考慮#

安全不能是事後補丁。設計伊始就應該做風險分析:

  • 列出可能的威脅(存取保護、攻擊防護、資料備份、資料安全)。
  • 評估發生機率與成本。
  • 依風險決定要投入多少防護資源。

Sam 的例子:

  • 最壞情況:丟失租借記錄。
  • 緩解:每日備份 + 紙本租借合約留存,影響可控。
  • 真正應該防範的:客戶自助 check-in 後拿走 CD——需透過實體保護或登入密碼。

If You Forget Security, You’re Not Secure——安全應在所有開發階段被納入考量;如果加上密碼登入流程,相關 use case 與類別責任也須同步調整。