2011年2月19日 星期六

DRY 的缺點以及測試碼的衝突

這篇是看到 Pylons (Pyramid) 的 《Unit Testing Guidelines》 後寫的心得。

以前我覺得每件事都有標準答案,或是所謂的「Best practice」。後來才發覺這是很嚴重的錯誤認知。因為希望能簡單地處理事情,而一廂情願地認定有「標準答案」,結果忽略了許多反面的訊息。《百人百觀》系列裡道出我的心態轉變。

以 DRY (Don’t Repeat Yourself) 來說,這是資訊人奉為終旨的鐵則,可應用到各種情境。這裡我們先縮小範圍,討論 DRY 對於寫程式的影響。它的優點顯而易見,只需要改一處程式,不會因漏改程式而產生 bug。重覆的程式碼容易造成 bug,複製貼上是常見的主因,甚至有 paper (CP-Miner) 提出方法自動偵測這種 bug。

但是 DRY 的缺點呢?造成 client codes 之間的相依性,迫使所有 client codes 共用同一介面,這帶來不少問題:

  • 寫錯共享程式時,影響不止一份程式。
  • 最簡單的情境得配合最複雜的情境使用,增加簡單情境的維護成本。即使介面設計的很完善,不需更改呼叫方式,執行時勢必多了一些檢查手續,或在空間上做了些妥協,提高時間和空間的成本,各種 framework 是最好的例子。
  • 承上,像 Django 的 session 為了能存各種 object,選擇以 dict 表示 session,直接序列化 session 物件存到資料庫或檔案裡。為了簡化實作並提供無限的空間存 session 資料,用 MySQL 時選擇用 LONGTEXT 以儲存無限制大小的資料,造成每次取資料都要從 disk 讀。在大量使用者連入的時候,這會是個問題。
  • 變更一處 client codes 的需求,可能會影響共享程式的介面。選擇相下向容的話,介面會變複雜,可能會多一些選擇性參數。邏輯變複雜,共享程式容易寫錯,client code 使用方式也變複雜。
  • 承上,選擇改變介面的話,需找出影響到的 client codes。對 dynamic typing 的語言來說,這是件苦差事,甚至無法 100% 保證沒有遺漏。

如同《Problem Solving 的技巧》裡說的,每個方法都有帶來的好處,也有帶來的壞處,也有針對壞處所做的後續修補。關鍵在於弄清楚現在的需求,明白各項設計的優缺點,配套作出一連串的設計,以獲得整體的最大效益。比方說用 VCS 切 branch 可以減少介面相容問題,不過會多出維護 branch 的成本,那是另一個議題了。

自從意識到 DRY 帶來的成本後,我覺得有些困惑,因為它不再是 100% 正確、用了一定好的原則。在寫測試碼時,我感到更困惑,若測試碼也變複雜,之間有相依性,那誰來保證測試碼是正確的?更何況一個具有完備測試碼的專案,測試碼和產品碼的比例將近 1:1,在量如此大的情況下,測試碼的邏輯太複雜的話,測試碼容易出錯,會造成不少問題。我體驗過測試碼寫太複雜而造成測試碼有錯,因測試碼出錯而誤以為產品碼有錯,結果費了更多力氣才找出錯誤 (程式碼變兩倍)。也體驗過在 setUp 或其它初始化部份出錯,造成訊息混亂,無法掌握錯誤的源頭。後來就不知不覺地將測試碼寫得很簡單,也漸漸減少犯這些錯的機會。

昨天看到 Pylons (Pyramid) 的 《Unit Testing Guidelines》後,才串起過去的經驗,發覺問題的源頭在於 DRY 並不適合用在測試碼,但是 DRY 已成為根深蒂固的習慣,壓根兒就不會想到將重覆程式碼抽出整理成跨 method / class / module 的行為,反而是妨礙測試碼品質的元兇。該篇文章有精闢的說明和例子,推薦大家參考。其中有些規則,現在還不能掌握使用後的優缺點,之後再抽時間讀讀 Pyramid 的原始碼,應該能學到一些東西。