---
# System prepended metadata

title: 防禦性程式設計

---

# 防禦性程式設計
如有錯誤歡迎糾正，小弟正在學習中

---
## 防禦性程式
防禦性設計與[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