# 防禦性程式設計 如有錯誤歡迎糾正,小弟正在學習中 --- ## 防禦性程式 防禦性設計與[DbC合約設計](https://hackmd.io/LCnn8R9GQxGQgKN2sAtocg#Design-by-Contract-DbC)有些不同,防禦性設計專注於程式上的保護,但這兩項設計並沒有相斥,是可以互補的 ### Value substitution (值的替換) 當輸入非預期值必須要處理的情境 例子: api interface定義article_id要傳遞的是int,但client卻輸入str,像這種情況就需要處理,即使`DbC`合約上沒講也應該要防呆 ``` json /api/v1/articles/{article_id}/comments ``` 遇到這種情境通常會使用`Value substitution`來解決,直接舉例API常見的情況,當`client`傳遞參數到後端時,需要做非預期值的處理,將錯誤轉成HTTP Code 告知 client為錯誤格式請求,避免讓程式直接`error` ``` python from fastapi import FastAPI, HTTPException import uvicorn app = FastAPI() db = {1: {"article_id": 1, "comments": ["Great post!", "Very informative."]}} @app.get("/api/v1/articles/{article_id}/comments") async def get_comments(article_id: int): if not isinstance(article_id, int): raise HTTPException(status_code=400, detail="Invalid article_id. It must be a positive integer.") return db[article_id] if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) ``` 如果沒有做`Value substitution`來解決會直接看到500 error這到還好,千萬不要讓以下情況發生,連Traceback訊息直接噴給client,反而把程式資訊都洩漏出去了,這種情竟你一定有在一些處理不好的網站遇到過 ``` python File "D:\python_program\pythonProject\Auth\dd.py", line 15, in get_comments return db[article_id] KeyError: 12 ``` ### Exception handling(例外處理) 需要設想可能會發生的錯誤,以及設想不可能發生的處理,當錯誤發生時應回覆當前資訊,而不是忽略或嘗試進到下個程式處理流程 例子1: 在設計get api時,就應該設想拿不到資源的可能性,透過Exception 訊息暴露出去給client,讓client明確知道錯誤與現在狀況, ``` python from fastapi import FastAPI, HTTPException import uvicorn app = FastAPI() db = {1: {"article_id": 1, "comments": ["Great post!", "Very informative."]}} @app.get("/api/v1/articles/{article_id}/comments") async def get_comments(article_id: int): if not isinstance(article_id, int): raise HTTPException(status_code=400, detail="Invalid article_id. It must be a positive integer.") if not (query_result := db.get(article_id)): raise HTTPException(status_code=404, detail="Resource not found.") return query_result if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) ``` 例子2 當非預期錯誤發生時,應該要做以下三點處理 - Value substitution 將錯誤替換成500 Error HTTP Status Code - Exception handling raise excpetion 告知client目前錯誤情況 - Error logging 透過log做非預期錯誤的紀錄 ``` python import logging from fastapi import FastAPI, HTTPException import uvicorn app = FastAPI() db = {1: {"article_id": 1, "comments": ["Great post!", "Very informative."]}} @app.get("/api/v1/articles/{article_id}/comments") async def get_comments(article_id: int): if not isinstance(article_id, int): raise HTTPException(status_code=400, detail="Invalid article_id. It must be a positive integer.") try: if not (query_result := db.get(article_id)): raise HTTPException(status_code=404, detail="Resource not found.") return query_result except Exception as e: logging.info(f'server error information:{e}') raise HTTPException(status_code=500, detail="An unexpected error occurred. Please try again. If the issue persists, please contact customer support.") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) ``` Exception 可以做到防禦程式設計但也請不要濫用 > 💡切記try catch 請慎重處理錯誤,須明確讓人知道錯誤,如果不是特殊情境也不要隱蓋錯誤,或者可透過logging進行紀錄 ### 請不要使用Exception來處理業務邏輯中的常規條件 `Exception`處理正常的業務邏輯流程混再一起 例子: 使用 ValueError 來處理年齡驗證是一種不良做法,因為年齡驗證是業務邏輯的一部分,而不是異常狀況。應該使用條件語句來進行這種驗證。 ``` python class User: def __init__(self, name, age): self.name = name self.age = age def register_user(user): try: if user.age < 18: raise ValueError("User must be at least 18 years old") # 正常處理註冊邏輯 print(f"User {user.name} registered successfully") except ValueError as e: print(f"Registration error: {e}") # 使用例外來控制流程 user1 = User("Alice", 16) register_user(user1) # 這裡會引發例外,但這應該是業務邏輯的正常判斷 ``` 業務邏輯要與實際程式邏輯區隔開來,不要混再一起增加邏輯複雜度與維護困難 你應該這麼做: 使用 if 條件語句來驗證年齡,並返回布林值表示註冊是否成功。在處理失敗情況時,可以根據需要執行額外的操作,例如聯繫客服。 ``` python class User: def __init__(self, name, age): self.name = name self.age = age def register_user(user): if user.age < 18: print(f"Registration failed: User must be at least 18 years old") return False # 正常處理註冊邏輯 print(f"User {user.name} registered successfully") return True # 使用條件語句進行年齡驗證 user1 = User("Alice", 16) success = register_user(user1) if not success: # 處理註冊失敗的情況與處理業務邏輯部分 raise ValueError("Please contact support for further assistance.") ``` ### 請不要在function中使用Exception來處理所有條件 過度依賴`Exception`處理以及暴露呼叫者不需關注的訊息 例子: 所有的條件檢查都使用了`Exception`。應該使用條件語句來驗證輸入的合法性,並在需要時返回錯誤狀態,而不是引發`Exception`。 ``` python def calculate_discount(price, discount): try: if price < 0: raise ValueError("Price cannot be negative") if discount < 0 or discount > 100: raise ValueError("Discount must be between 0 and 100") return price * (1 - discount / 100) except ValueError as e: print(f"Error calculating discount: {e}") # 使用例外來處理所有條件 discounted_price = calculate_discount(-100, 20) # 這裡會引發例外,應該使用條件語句處理這些檢查 ``` 你應該這麼做: 外部代碼根據返回的錯誤信息來處理錯誤情況,而不是依賴例外,以及`raise` 呼叫者所關注的`Exception` ``` python def calculate_discount(price, discount): if price < 0: return None, "Price cannot be negative" if discount < 0 or discount > 100: return None, "Discount must be between 0 and 100" return price * (1 - discount / 100), None # 使用條件語句處理所有條件 discounted_price, error = calculate_discount(-100, 20) if error: raise ValueError(f"Error calculating discount: {error}") else: print(f"Discounted price: {discounted_price}") ``` > 💡過多的Exception會弱化封裝,因為一直暴露內部訊息,如果遇到過多Exception請將分解多個function或內聚Exception職責,如DB只會有DB相關Exception,業務邏輯function只會有業務邏輯Exception,驗證function只會有專注驗證的Exception