# 防禦性程式設計
如有錯誤歡迎糾正,小弟正在學習中
---
## 防禦性程式
防禦性設計與[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