函式 (Functions) #
核心觀點 #
函式(Function)是軟體系統中的基本動詞,本章探討如何撰寫出可讀性高且易於維護的函式。
核心理念可歸納為:函式應短小,且只做一件事。
- 只做一件事 (Do One Thing): 函式應只做一件事,把它做好,且只做這件事。這符合單一職責原則 (Single Responsibility Principle)
- 單一層次抽象 (One Level of Abstraction): 函式內語句應維持在同層次的抽象概念,避免混雜「高階邏輯」與「低階細節」
函式設計原則 #
1. 簡短與專注 #
函式的首則是「短小」,其次是「更短小」。
- 區塊與縮排:
if、else、while語句區塊應只有一行,這行通常是一個函式呼叫 - 判斷指標: 如果一個函式可被合理拆分成多個不同段落(例如初始化區、處理區、結束區),代表它做太多了
範例:重構為單一職責
重構前 (做太多事): 混合了計算總和、尋找極值與計算平均。
def process_scores(scores): total = 0 min_score = 100 max_score = 0 for score in scores: total += score if score % min_score: min_score = score if score % max_score: max_score = score average = total / len(scores) return average, min_score, max_score重構後 (各司其職): 將邏輯拆解,讓主程式碼像是在閱讀摘要。
def calculate_average(scores): return sum(scores) / len(scores) def find_min_score(scores): return min(scores) def find_max_score(scores): return max(scores) # 使用端 scores = [85, 90, 78, 92, 88] average = calculate_average(scores) min_score = find_min_score(scores) max_score = find_max_score(scores)
2. 函式參數 (Function Arguments) #
參數是函式與讀者間的溝通障礙。參數越多,讀者就需要了解越多細節才能理解,測試也會變困難。
- 參數數量的優先級:
- 零參數 (Niladic):最佳選擇
- 單參數 (Monadic):常見且易懂(例如詢問關於該參數的問題,或是操作該參數)
- 雙參數 (Dyadic):尚可接受,但需確認順序(如 x, y 座標)
- 三參數 (Triadic):應盡量避免
- 多參數 (Polyadic):除非有特殊理由,否則不應使用
- 物件參數:如果函式看來需兩或三個以上的參數,通常代表這些參數應被包裝成一個類別(Class)
- 避免輸出型參數:讀者預期參數是輸入(Input),若參數被用來輸出結果(修改內容後帶回),會造成認知斷層
3. 分離命令與查詢 (Command Query Separation, CQS) #
函式應「執行動作(改變物件狀態)」或「回答問題(回傳資訊)」,但不該同時做這兩件事。
若一個函式名為 set(“username”, “bob”) 卻同時回傳布林值代表成功與否,
會讓讀者困惑:這是要設定屬性,還是要檢查屬性是否存在?
class Cart:
def __init__(self):
self.items = []
# Command: 改變狀態,不回傳值
def add_item(self, item):
self.items.append(item)
# Query: 回傳資訊,不改變狀態
def get_total_price(self):
return sum(item.price for item in self.items)
# Bad Practice: 混合了 Command 與 Query
def add_item_and_return_total(self, item):
self.items.append(item)
return self.get_total_price()
進階結構與維護 #
1. 避免副作用 (Side Effects) #
副作用是指函式在承諾只做一件事時,卻「偷偷」做了其他事(例如修改全域變數、初始化不該初始化的系統)。 這會導致時序性耦合(Temporal Coupling)和難除錯的 Bug。
2. 善用例外處理取代錯誤碼 #
回傳錯誤碼(Error Codes)會迫使呼叫者立刻處理錯誤,導致邏輯深層巢狀化。 用例外(Exceptions)可將錯誤處理邏輯從主邏輯中分離出來。
3. Switch 語句的處理 #
Switch 語句天生就做「N 件事」,易違反單一職責原則(SRP)與開放封閉原則(OCP)。 在物件導向設計中,應盡量用多型(Polymorphism)或資料結構(如 Dictionary/Map) 取代它。
範例:取代 Switch
修正前 (Switch/If-Else): 每次新增事件類型都需修改此函式,違反 OCP
def handle_event(event_type, event): if event_type == "CREATE": create_event(event) elif event_type == "UPDATE": update_event(event) elif event_type == "DELETE": delete_event(event) else: print("Invalid event type.")修正後 (Table-Driven / Map): 將邏輯對應關係抽離,程式碼更簡潔且易擴充
def handle_event(event_type, event): event_handlers = { "CREATE": create_event, "UPDATE": update_event, "DELETE": delete_event, } if event_type in event_handlers: event_handlers[event_type](event) else: print("Invalid event type.")
寫作的藝術 #
不重複自己 (DRY) #
重複是軟體萬惡之源。許多原則與設計模式(Design Patterns)都是為消除重複而生。當邏輯重複時,維護成本會成倍增加。
程式碼即故事 #
寫程式就像寫作。你不可能一開始就寫出完美函式。
- 初稿: 通常冗長、雜亂、參數過多
- 修飾: 通過單元測試(Unit Test)覆蓋後,打磨程式碼——分解函式、更改名稱、消除重複
- 定稿: 最終產出符合上述規則的乾淨函式
“Master programmers think of systems as stories to be told rather than programs to be written.”
程式設計師不只是在寫程式碼,而是在講述系統的故事。