2

避免過度的 Defensive Programming 防禦性程式設計

 2 years ago
source link: https://blog.niclin.tw/2019/07/18/defensive-programming/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Nic Lin's Blog

喜歡在地上滾的工程師

防禦性程式發生在程式設計師不相信輸入的參數,所以對其做檢查,有可能在呼叫者(caller)和被呼叫者(callee)都做了相同的檢查來避免出錯,一般來說我們不需要測試輸入的資料來確保功能正常。系統中大部分的程式碼應該可以假設輸入的資料是正確的,只要在資料輸入進系統時被檢查過,應該就可以假設他是正確的。

多餘的檢查看似可以提升穩定性,但也隨之而來的是

  • 增加維護難度

防禦性設計的出發點是「不信任」,我們當然要避免可能發生的邊界情況,以 Public API 來說有兩種策略應付不乖的參數

  1. 修正或略過資料的錯誤(defensive, compensate)
  2. 丟出錯誤(offensive, Fail-fast)
# defensive programming
# 如果參數不符合條件,就略過或修正它
def generate_account(currency)
  if currency && currency.include?(["btc", "eth"]) # 檢查 currency 種類,如果傳進來的不是預期的就跳過
    # create account
  else
    # ignore, nothing happend.
  end  
end

# non-defensive programming
def generate_account(currency)
  # create account
end

# offensive programming (fail fast)
# early return,檢查完出錯直接噴錯讓你知道,而且有明確的訊息
def generate_account(currency)
  raise "currency: #{currency} is invalid." if currency.blank? || !currency.include?(["btc", "eth"])
  
  # create account
end

過度的防禦性設計更易造成問題的掩蓋,為什麼可以傳不在預期的 value 進來卻沒事,或是只記一條 Log?

這應該是要去 trace 的問題,因為這樣長久下來整個系統除了多了一大堆檢查的冗餘 code 以外,不確定性越來越多也就越來越難以維護。

另一種情況是濫用 rescue 捕捉 exception,如果程式碼裡面經常看到 begin rescue,這就是一種 bad smell。

在調用外部接口時,一定要抓 exception,但有一部份是新手常犯的錯誤,會在自己的邏輯外在套上 begin rescue,而且不明確捕捉類型,當你問他要抓哪種錯誤的時候通常是答不出,對於自己寫的程式碼邏輯錯誤不應該一次全家桶的捕獲,而是查出錯誤來源,從源頭解決問題。

# 不明確捕捉 exception, 任何錯誤情況都回 false
def add_funds!(account, amount)
  begin
    account.update(amount: amount)
  rescue
    false
  end
end

在 call 這個 method 時,我拿到 false 回應時我可能會去看為什麼?

然後直到我看到這段 code 時我更無法確定

  1. update 失敗?
  2. 沒傳 account?
  3. 沒傳 amount?
  4. amount 數字過大或過小?
  5. amount 型態錯誤?

此例中,無論發生什麼錯誤都不會有事不會 crash,只是徹底把問題隱藏起來。

所以說 Overly Defensive Programming 基本上就是 Hide the Problem Programming

然而 Fail-fast 的作法應該是盡快的將錯誤 exception,拋給 caller

既然是 caller 亂傳參數或是參數有問題,當然請 caller 根治問題,而我執行方也就直接 fail 掉不跟你囉唆這樣。

def add_funds!(account, amount)
  raise "amount is valid" if amount <= 0
  raise "account is locked" if account.is_locked?

  account.update(amount: amount)
end

雖然有些人也會認為 Fail-fast 的手段也是 defensive programming 的一種,但我覺得防禦性設計本身沒有問題,有問題的部分是在於「隱藏問題」,而很常見的原因都是 exception handling 亂用導致。

避免 Overly Defensive Programming,請用 Fail-fast 來增加找到錯誤的效率。

  1. 系統邊界內,盡量相信輸入,不做檢查
  2. 需要檢查的部分,採用錯誤直接中斷的作法,避免隱藏問題

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK