最後更新日期: 14/12/2024
Table of Contents:
🤔
軟體建構出一個世界,釐清出各元件要負的責任。這些輪廓和分工會隨著外在需求變動而對應調整。除此之外,撰寫軟體的是會犯錯、見識有限的人類,軟體品質要能提升,也得等作者提升知識與技術後才能發生。換言之,持續變動是軟體必有的行為,也是軟體的價值所在-它能像個有機體,即便長大、成熟,仍能富有彈性與韌性。
重構(Refactoring),是指不改變可見行為時變動軟體內部,為增進程式碼的可讀性,降低維護成本。作者Martin Fowler列舉出他常用的61
條重構手法,搭配詳細解釋的範例,讓讀者深刻了解這些重構手法的操作動機,做法,和注意事項。
善於重構,如同善於呈現事物簡潔美好的那一面;學習大師如何重構,就如同學習如何畫出一幅美麗畫作。對於「重構」這本書,我的推薦指數⭐⭐⭐⭐⭐,每位有志將程式碼寫好的工程師都適合買來收藏😊。
ℹ️
- ↔: 重構:改善既有程式的設計
- 👨🎨: Martin Fowler
- 🛒: 🔗
- ✍: November 20, 2018
📖1: 前言
🔖1: 重構:第一個範例
🎯:
作者使用一個簡單範例來示範如何重構,其中原則是獨立切分成小幅度的變動。在範例中的處理順序如下:
- 準備好測試程式碼:能確認重構後的程式仍運作正常
- 拆接程式碼為不同完整行為的區塊,每次皆用測試驗證功能仍正常
- 若能讓變數意思更清楚,重新命名(像把函式回傳值命為
result
) - 調整或移除區域變數,讓程式碼更易被提取
- 若能讓變數意思更清楚,重新命名(像把函式回傳值命為
- 將處理邏輯依據不同任務階段(像是計算與格式化)拆成兩個檔案
- 用資料類型(Type)區隔相異計算狀況,再用多型計算器(Calculator)處理對應任務
💡:
- 函式名稱取得好,人不需看內文就能了解其功能
- 要新增功能卻因結構複雜而困難重重時,代表要先重構
- 重構時不需考慮性能,完成後再來調整
- 判斷程式好壞的關鍵為是否清楚明瞭,而易修改
🔖2: 重構的原理
🎯:
作者討論重構的意思(what)、為何需要它(why)、何時該發生(when)、和與其相關議題(who):
- 什麼是重構:
- 名詞:不改變可見行為的前提下變動軟體內部-增進可讀性,降低修改成本
- 動詞:用一系列的手法重新架構軟體,過程中不應加入新功能
- 重構的好處:
- 改善架構設計:程式碼結構會逐漸劣化,不重構只會加快劣化速度
- 軟體更易被理解:將軟體被期待要做的,跟軟體被告知要做的保持一致
- 幫助找出bug:在過程中理解結構,釐清假設,進而找出bug
- 提高開發速度:良好的模組化能讓開發者理解一小處後就能加以修改
- 何時該重構:
- 新增功能前(最佳時機)
- 修改程式碼前(了解作用)
- 打掃程式碼(當前結構糟糕)
- 伺機性重構(與其他工作一同進行)
- 長時程重構(一段時間內碰到相關區域時都稍微重構)
- Code Review(自己重構後,就能給出更好建議)
- 重構時會遇到的難題
- 減慢功能新增速度:依據專業做對應判斷,真相往往是人們過少而非過度重構
- 不具備完整存取權限:保留舊介面並標註棄用(deprecated),之後再完整刪除
- 在分支(Branch)合併時所產生衝突(Conflicts):實施CI防止各分支版本過大差異
- 程式碼尚未具備自檢(self-testing)性質:確認每次重構皆合法,是CD要素之一
- 程式碼過於老舊:先嘗試尋找可插入測試的接縫,提高重構的信心
- 處理資料庫:將變動放入綱目(Schema),用migration script存取
💡:
- 小步驟地重構能快速寫出好程式,且不需花時間除錯
- 醜陋程式碼須重構,優秀程式碼也需大量重構
🔖3: 程式碼異味
🎯
作者列出常見的程式碼怪味,和建議的處理方法:
怪味道 | 說明 | 處理方法 | 附註 |
---|---|---|---|
Mysterious Name | 好名稱是程式碼清楚的關鍵 | 重新命名 | 若對某物苦思不出好名稱,往往代表一定程度的設計不良 |
Duplicated Code | 在不同處看到相同的程式結構 | 提取相同邏輯,統一起來 | |
Long Function | 長度不是標準,而是考量它的What與How的距離有多遠 | 拆分成小函式並給予良好名稱,你因此不需查看內容 | 小函式具備間接關係的所有好處(解釋性、共用性與選擇性) |
Long Parameter List | 將多個參數組成一個類別 | ||
Global Data | 它可在任何地方被修改,卻無法輕易找出 | 用存取函式封裝它,進而知道誰修改了它。這樣也進階限縮範圍,讓模組內的程式碼才能看到他 | |
Mutable Data | 作用域越大,風險也越大 | 用存取函式封裝 與其他邏輯清楚區隔 | 像是Functional Programming的理念就是永遠不該更改資料 |
Divergent Change | 模組常因不同原因而被用不同方式修改 | 拆分邏輯成不同階段(Phase)或類別(Class),建立明確的領域邊界 | |
Shotgun Surgery | 每次的更動都散落在各處 | 使用Inline重構,即便方法變冗長或類別變大。之後再提取拆分即可 | |
Feature Envy | 不同函式或資料間的互動過於頻繁 | 將會一起變化的放在一起 | |
Data Clumps | 資料項目成群結隊地出場 | 將它們集合成一個類別 | |
Primitive Obession | 過度使用基本型態,就無法:賦予明確意義、重用對應所需函式 | 改為具備意義的型態 | |
Repeated Switches | Swtich本身沒有壞處,但我們要加入一個子句時,就得修改每個同群件的切換邏輯 | 使用多型,它能幫你打造更文明的樣貌 | |
Loops | 往往有更好的做法來達成目的 | 用pipeline operation,像是filter和map | |
Lazy Element | 為了將來可能有用而存在的元素 | 使用Inline 函式或類別 使用繼承 | |
Speculative Generality | 函式或類別只被測試程式使用 | 移除它們 | |
Temporary Field | 類別中的一些欄位只在特定情況下被使用 | 將它們放入一個新類別或替代類別 | 類別的欄位任何情況下都該被用到 |
Message Chains | 用值處與值形成緊密的長條結構。當物件關係變化,使用方也就得做對應修改 | 初步可以重構,讓長鏈各點都能執行此動作。 最好則是先瞭解生物件的最終用途,再把過程抽取成函式 | |
Middle Man | 封裝往往伴隨著委託,而委託可能會被濫用,中間存在無意義也沒貢獻的中間人 | 用Inline函式將它們加到呼叫方,或用繼承塞到物件裡 | |
Insider Trading | 模組間過度且隱諱地交換資料 | 用第三個或中介模組來斷開緊密耦合 | |
Large Class | 類別具有太多欄位 | 依據使用方法拆解數個子類別 | |
Alternative Classes with Different Interfaces | 有些類別可藉繼承互有關係 | 更改函式簽名、 移動函式、 拆出父類別 | |
Data Class | 資料類別過於單純,沒有任何相關行為 | 封裝資料,限制存取權限 提出存取它的共同行為納為當中函式 | |
Refused Bequest | 父類別有子類別不需要的方法或資料 | * 建立旁系(sibiling)類別 * 將重複內容組成委託,取代父子類別關係 | |
Comments | 它們往往用來掩飾程式碼的低劣品質 | 將邏輯抽為函式, 或用斷言說明狀態規則 |
🔖4: 建構測試程式
🎯:
作者強調編寫自檢測試的重要:我們的測試往往不足,而非過度撰寫。
💡:
- 所有測試都該完全自動化,且能檢查結果
- 測試是強大Bug偵測器,減少尋找它們的時間
- 所有測試都得在該失敗時失敗
- 思考可能出錯的邊界條件,集中測試它們
- 別因所寫測試無法找出所有Bug而不寫
- 收到Bug報告時,先寫出能反映它的測試
📖2: 常見重構
🔖1: Extract Function(提取函式)
📋
這重構很常見:在了解一段程式碼的功用後,提取為一個函式並據其目的來命名。何時該做此重構,有人依據長度、重用性,作者認為關鍵為「分開意圖與實作」-如果我們得費心查看程式碼內容才能了解它在做什麼,那就適合提取。
基於此原則,程式碼超過六行都會讓作者考慮提取為函式,而當今編譯器也讓我們不需擔心這會影響效能。做得好,函式名稱會讓程式碼本身即為一份文件。
✏
if __name__ == "__main__": numbers = [1, 2, 3, 4, 5] sum = 0 for number in numbers: sum += number print(sum)
def calculate_sum(numbers): sum = 0 for number in numbers: sum += number return sum if __name__ == "__main__": numbers = [1, 2, 3, 4, 5] result = calculate_sum(numbers) print(result)
🔖2: Inline Function(內聯函式)
📋
必要的間接層很有幫助,沒必要的反而礙眼。當程式碼有太多間接層時(各函式僅委託工作給其他函式,讓人迷失方向),就是內聯函式的時機。
✏
def calculate_product(x, y): return x * y if __name__ == "__main__": result = calculate_product(5, 10) print(result)
if __name__ == "__main__": result = 5 * 10 print(result)
🔖3: Extract Variable(提取變數)
📋
遇到複雜且難以理解的運算式時,用區域變數來命名就能幫助了解其目的。
✏
if __name__ == "__main__": print((85 * 10) / 5 + 20)
if __name__ == "__main__": base_calculation = (85 * 10) / 5 result = base_calculation + 20 print(result)
🔖4: Inline Variable(內聯變數)
📋
若運算式本身已傳達充足資訊,就不需要一個名稱來解釋,適合將其內聯。
✏
if __name__ == "__main__": product = 85 * 10 result = product / 5 + 20 print(result)
if __name__ == "__main__": print((85 * 10) / 5 + 20)
🔖5: Change Function Declaration(修改宣告式)
📋
函示宣告式顯出程式如何互相結合,像是身體的關節,具備很大影響力。好名稱可讓人只藉它就知其函式作用,進而找出改善整體程式碼的方向。
✏
def add_numbers(x, y): return x + y if __name__ == "__main__": result = add_numbers(5, 3) print(result)
def calculate_sum(x, y): return x + y if __name__ == "__main__": result = calculate_sum(5, 3) print(result)
🔖6: Encapsulate Variable(封裝變數)
📋
相比重構函式,重構資料較為困難,我們沒法用類似轉傳函式來簡化過程。封裝資料成為移動運用廣泛資料的一種做法,我們因此能將工作從「重新組織資料」轉為較易的「重新組織函式」。
封裝資料也得到「在明確地點監視資料的變動與使用」這好處,這也是OOP強調將物件資料維持私用(減少能見度)的原因。不可變的資料不用特別封裝,因為它不會在任何過程中被改變,也沒有程式碼會取得過期資料,不變性是強大的防腐劑。
✏
config_value = 50 if __name__ == "__main__": print(config_value)
class Configuration: def __init__(self): self._config_value = 50 @property def config_value(self): return self._config_value @config_value.setter def config_value(self, value): self._config_value = value if __name__ == "__main__": config = Configuration() print(config.config_value) config.config_value = 100 print(config.config_value)
🔖7: Rename Variable(重新命名變數)
📋
具好名稱的變數能解釋它要做什麼。取錯名稱的原因很多(沒仔細思考、閱讀經驗不足導致不夠理解問題、程式碼目的會隨使用者需求而變),但我們仍得堅持住品質,這重要性取決於變數有多被廣泛使用。
✏
if __name__ == "__main__": x = "Hello, world!" print(x)
if __name__ == "__main__": message = "Hello, world!" print(message)
🔖8: Introduce Parameter Object(引入參數物件)
📋
當一群資料持續在不同函式間成群出現,就很適合組成一個資料結構。這樣做可以顯出它們的關係,也可縮短呼叫函數的參數列,更能幫助之後的深層修改。
組織起資料群與呼叫函式後,就能提升結構的抽象樣貌,大幅簡化我們對此領域的理解。這些都得先從引入參數物件開始。
✏
def process_data(name, age, email): print(f"Name: {name}, Age: {age}, Email: {email}") if __name__ == "__main__": process_data("Alice", 30, "alice@example.com")
class PersonData: def __init__(self, name, age, email): self.name = name self.age = age self.email = email def process_data(person): print(f"Name: {person.name}, Age: {person.age}, Email: {person.email}") if __name__ == "__main__": alice = PersonData("Alice", 30, "alice@example.com") process_data(alice)
🔖9: Combine Functions into Class(將函式移入類別)
📋
類別已為一種基本架構,能將資料與函式放到同個環境裡。當有組函式密切合作,處理同個資料體時,組成類別能清楚展示呼叫環境,移除對應引數,並再找出其他計算方法,重構到新類別中。
✏
def add_numbers(x, y): return x + y def subtract_numbers(x, y): return x - y if __name__ == "__main__": sum_result = add_numbers(10, 5) subtract_result = subtract_numbers(10, 5) print(f"Sum: {sum_result}, Subtract: {subtract_result}")
class Calculator: def __init__(self, x, y): self.x = x self.y = y def add(self): return self.x + self.y def subtract(self): return self.x - self.y if __name__ == "__main__": calc = Calculator(10, 5) sum_result = calc.add() subtract_result = calc.subtract() print(f"Sum: {sum_result}, Subtract: {subtract_result}")
🔖10: Combine Functions into Transform(組函式為轉換函式)
📋
面對資料被轉成衍生資訊,被多處使用的狀況,我們可將這類重複運算邏輯放在一起,組函式為轉換函式就是其一作法。
這樣避免了重複邏輯,驗證整個計算過程也只需檢查此函式。相比於將函式移入類別,此做法適用於來源資料不會被更新。
✏
def to_upper(text): return text.upper() def reverse_text(text): return text[::-1] if __name__ == "__main__": input_text = "hello" transformed_text = reverse_text(to_upper(input_text)) print(f"Transformed Text: {transformed_text}")
def transform_text(text): return text.upper()[::-1] if __name__ == "__main__": input_text = "hello" transformed_text = transform_text(input_text) print(f"Transformed Text: {transformed_text}")
🔖11: Split Phase(拆成不同階段)
📋
遇到一段處理不同事的程式碼時,拆成不同模組。這樣能在之後變動中獨立處理單一主題,不需在意另個模組細節。編譯器即應用此一原則,它不斷地接受一段文字(高階程式語言)並轉化成可執行形式(像是特定硬體的目的碼)。
這樣做的另個好處,是能凸顯出一段程式會在不同階段用不同的資料與函式。
✏
def process_order(data): quantity = data["quantity"] price = data["price"] total = quantity * price if data["type"] == "book": tax_rate = 0.1 else: tax_rate = 0.2 tax = total * tax_rate return total + tax if __name__ == "__main__": order_data = {"type": "book", "quantity": 4, "price": 150} total_cost = process_order(order_data) print(f"Total cost: {total_cost}")
def calculate_base_total(quantity, price): return quantity * price def calculate_total_with_tax(base_total, item_type): if item_type == "book": tax_rate = 0.1 else: tax_rate = 0.2 tax = base_total * tax_rate return base_total + tax if __name__ == "__main__": order_data = {"type": "book", "quantity": 4, "price": 150} base_total = calculate_base_total(order_data["quantity"], order_data["price"]) total_cost = calculate_total_with_tax(base_total, order_data["type"]) print(f"Total cost: {total_cost}")
📖3: 封裝類的重構
🔖12: Encapsulate Record(封裝記錄)
📋
儲存資料的選擇上,作者建議用物件封裝可變資料,使用者就不用在乎裡面樣貌,更改相關名稱也能階段式完成;用記錄封裝不可變資料,像是用hash
、map
、hashmap
、dictionary
等。
在小範圍用這些記錄不太會有問題,但隨著被用範圍擴大,結構隱晦這缺點就值得封裝它們為資料類別(Data Class)。
✏
employee = { "name": "John Doe", "age": 30, "department": "Finance" } if __name__ == "__main__": print(f"Employee Name: {employee['name']}, Department: {employee['department']}")
class Employee: def __init__(self, name, age, department): self.name = name self.age = age self.department = department if __name__ == "__main__": employee = Employee("John Doe", 30, "Finance") print(f"Employee Name: {employee.name}, Department: {employee.department}")
🔖13: Encapsulate Collection(封裝集合)
📋
封裝可變資料有很多好處,但面對集合類別時得避免提供get
即得到集合本身的錯誤。使用者可能藉此繞開設計干預,直接更改當中成員。
我們可以提供替代get
,回傳集合副本。這類設計的重點是保持一致性:只採取一種機制,讓任何集合存取函式被呼叫時,使用者都能習慣和預期其行為。
✏
project_tasks = ["Define requirements", "Develop prototype", "Conduct testing"] if __name__ == "__main__": project_tasks.append("Deployment") print("Project Tasks:", project_tasks)
class Project: def __init__(self): self._tasks = ["Define requirements", "Develop prototype", "Conduct testing"] def add_task(self, task): self._tasks.append(task) def get_tasks(self): return list(self._tasks) # 返回任務列表的副本以防止外部修改 if __name__ == "__main__": project = Project() project.add_task("Deployment") print("Project Tasks:", project.get_tasks())
🔖14: Replace Primitive with Object(替換基本元素為物件)
📋
初期的簡單資料,日後可能會出現對它的多種操作,這時替換它們成為一個新類別。乍看之下可能不必要也沒什麼,但對程式的基礎影響可能超乎想像。
✏
email = "john.doe@example.com" if __name__ == "__main__": if "@example.com" in email: print("Email is from example.com") else: print("Email is from another domain")
class Email: def __init__(self, address): self.address = address def is_from_domain(self, domain): return domain in self.address if __name__ == "__main__": email = Email("john.doe@example.com") if email.is_from_domain("@example.com"): print("Email is from example.com") else: print("Email is from another domain")
🔖15: Replace Temp with Query(替換暫時變數為查詢函式)
📋
暫時變數很方便:能存程式碼所生值,讓其被引用;名稱也能解釋其意義,防止重複程式碼出現。如果能把它改成函式會更好:函式減少引入一個參數,其邏輯與原始函式間也設下了明確邊界,並避免此計算邏輯在類似函式中重複出現。
此做法最能有效處理類別,因為成員都位於共用環境。但它僅適合處理「被計算一次,之後皆為讀取」的暫時變數,不適合處理快照(snapshot)變數。
✏
if __name__ == "__main__": original_price = 200 discount_rate = 0.15 discount_amount = original_price * discount_rate discounted_price = original_price - discount_amount print(f"Discounted price: {discounted_price}")
class PriceCalculator: def __init__(self, original_price, discount_rate): self.original_price = original_price self.discount_rate = discount_rate def discount_amount(self): return self.original_price * self.discount_rate def discounted_price(self): return self.original_price - self.discount_amount() if __name__ == "__main__": calculator = PriceCalculator(200, 0.15) print(f"Discounted price: {calculator.discounted_price()}")
🔖16: Extract Class(提取類別)
📋
類別初期都很守規矩,隨著時間它會責任變重,複雜到失去彈性。拆開它的時機有二:看到資料或方法該放一起、有群資料同時改變和互相依賴。
我們也可藉自問「移除某段資料或方法,是否會讓其他欄位或方法變不合理」來做提取依據。
✏
class Employee: def __init__(self, name, email, street, city, zip_code): self.name = name self.email = email self.street = street self.city = city self.zip_code = zip_code def print_employee_details(self): print(f"Name: {self.name}") print(f"Email: {self.email}") print(f"Address: {self.street}, {self.city}, {self.zip_code}") if __name__ == "__main__": employee = Employee("John Doe", "john@example.com", "123 Elm St", "Metropolis", "90210") employee.print_employee_details()
class Address: def __init__(self, street, city, zip_code): self.street = street self.city = city self.zip_code = zip_code def print_address(self): print(f"Address: {self.street}, {self.city}, {self.zip_code}") class Employee: def __init__(self, name, email, address): self.name = name self.email = email self.address = address def print_employee_details(self): print(f"Name: {self.name}") print(f"Email: {self.email}") self.address.print_address() if __name__ == "__main__": address = Address("123 Elm St", "Metropolis", "90210") employee = Employee("John Doe", "john@example.com", address) employee.print_employee_details()
🔖17: Inline Class(內聯類別)
📋
這相對於提取類別是個反向操作,會用到它的時機有二:類別因重構而空無一物、產生一對不同功能類別前的暫時做法。
✏
class Email: def __init__(self, address): self.address = address def print_email(self): print(f"Email: {self.address}") class Contact: def __init__(self, name, email): self.name = name self.email = Email(email) def print_contact_details(self): print(f"Name: {self.name}") self.email.print_email() if __name__ == "__main__": contact = Contact("John Doe", "john@example.com") contact.print_contact_details()
class Contact: def __init__(self, name, email): self.name = name self.email = email def print_contact_details(self): print(f"Name: {self.name}") print(f"Email: {self.email}") if __name__ == "__main__": contact = Contact("John Doe", "john@example.com") contact.print_contact_details()
🔖18: Hide Delegate(隱藏委託)
📋
封裝是實現良好模組化的關鍵,它讓模組不需知道系統其他部分,減少系統變化時要修改的地方。
除了常見的封裝欄位,我們也可以用某物件(Department
)封裝另個物件(Manager
)。這樣當Manager
被修改時,對應修改只會延伸到Department
為止。
✏
class Department: def __init__(self, manager): self.manager = manager class Employee: def __init__(self, name, department): self.name = name self.department = department if __name__ == "__main__": department = Department("Alice") employee = Employee("Bob", department) print(f"Manager: {employee.department.manager}")
class Department: def __init__(self, manager): self.manager = manager def get_manager(self): return self.manager class Employee: def __init__(self, name, department): self.name = name self.department = department def get_manager(self): return self.department.get_manager() if __name__ == "__main__": department = Department("Alice") employee = Employee("Bob", department) print(f"Manager: {employee.get_manager()}")
🔖19: Remove Middle Man(移除中間人)
📋
使用隱藏委託有其代價:為封裝類別加入方法時,外部類別也得新增簡單的委託方法。
我們不需永遠忍受這類轉傳機制:隨著程式碼變化,我們也可視情況再度移除中間人。
✏
class Department: def __init__(self, manager): self.manager = manager class Person: def __init__(self, name, department): self.name = name self.department = department def get_manager(self): return self.department.manager if __name__ == "__main__": department = Department("Alice") person = Person("Bob", department) print(f"Manager: {person.get_manager()}")
class Department: def __init__(self, manager): self.manager = manager class Person: def __init__(self, name, department): self.name = name self.department = department if __name__ == "__main__": department = Department("Alice") person = Person("Bob", department) print(f"Manager: {person.department.manager}")
🔖20: Substitute Algorithm(替換演算法)
📋
許多事都有替代作法,演算法也是如此。直接替換整個演算法會有難度:先拆分成方便修改的東西,之後就能輕鬆替換。
✏
def find_minimum(numbers): if not numbers: return None min_value = numbers[0] for number in numbers[1:]: if number < min_value: min_value = number return min_value if __name__ == "__main__": number_list = [5, 2, 9, 1, 5, 6] minimum = find_minimum(number_list) print(f"The minimum number is: {minimum}")
def find_minimum(numbers): return min(numbers) if numbers else None if __name__ == "__main__": number_list = [5, 2, 9, 1, 5, 6] minimum = find_minimum(number_list) print(f"The minimum number is: {minimum}")
📖4: 移動類的重構
🔖21: Move Function(移動函式)
📋
要讓軟體具備模組特質,相關元素得在一起。這項任務會動態發生:越了解要做的事,就越知道如何妥善聚集元素。持續地聚集顯出我們理解的持續提升。
移動函式會發生在其參考其他環境的元素量大於當前環境,而這樣做通常能改善封裝。這決定不容易,而越困難的選擇往往越不重要。
✏
class Employee: def __init__(self, name, email): self.name = name self.email = email def print_employee_details(self): print(f"Name: {self.name}, Email: {self.email}") def send_email(employee, message): print(f"Sending email to {employee.email} with message: '{message}'") if __name__ == "__main__": employee = Employee("John Doe", "john@example.com") employee.print_employee_details() send_email(employee, "Welcome to the team!")
class Employee: def __init__(self, name, email): self.name = name self.email = email def print_employee_details(self): print(f"Name: {self.name}, Email: {self.email}") def send_email(self, message): print(f"Sending email to {self.email} with message: '{message}'") if __name__ == "__main__": employee = Employee("John Doe", "john@example.com") employee.print_employee_details() employee.send_email("Welcome to the team!")
🔖22: Move Field(移動欄位)
📋
程式強健度取決於當中的資料結構。適當的資料結構會讓行為程式碼簡單直接,判斷方法之一是採用領域驅動設計(Domain-driven Design)。
傳遞某記錄給函式時也得傳遞另筆記錄、修改某記錄也會更動另筆記錄時,就得考慮移動欄位了。這類重構在類別環境中較易執行,因為資料都被封裝在存取方法後面。
✏
class Employee: def __init__(self, name, email, salary): self.name = name self.email = email self.salary = salary class Payroll: def __init__(self, tax_rate): self.tax_rate = tax_rate def calculate_net_salary(self, employee): return employee.salary - (employee.salary * self.tax_rate) if __name__ == "__main__": employee = Employee("John Doe", "john@example.com", 50000) payroll = Payroll(0.15) net_salary = payroll.calculate_net_salary(employee) print(f"Net Salary: {net_salary}")
class Employee: def __init__(self, name, email): self.name = name self.email = email class Payroll: def __init__(self, tax_rate, salary): self.tax_rate = tax_rate self.salary = salary def calculate_net_salary(self): return self.salary - (self.salary * self.tax_rate) if __name__ == "__main__": employee = Employee("John Doe", "john@example.com") payroll = Payroll(0.15, 50000) net_salary = payroll.calculate_net_salary() print(f"Net Salary: {net_salary}")
🔖23: Move Statements into Function(移入陳述式至函式)
📋
移除重複程式碼能有效維護程式碼健康,將每次都隨之執行的程式碼移至函式,以後修改就僅需更動一處,之後調整再移入陳述式至呼叫方即可。
✏
if __name__ == "__main__": from datetime import datetime current_date = datetime.now() formatted_date = current_date.strftime("%Y-%m-%d") print(f"Today's date is: {formatted_date}")
def get_formatted_date(): from datetime import datetime current_date = datetime.now() return current_date.strftime("%Y-%m-%d") if __name__ == "__main__": formatted_date = get_formatted_date() print(f"Today's date is: {formatted_date}")
🔖24: Move Statements to Callers(移入陳述式至呼叫方)
📋
函式是抽象的基本元素,抽象邊界會隨程式改變跟者移動。當某個行為已被多處使用,但有些地方想改變呼叫方式,我們就可把改變行為從函式移到呼叫方。
✏
def display_greeting(): greeting = "Hello, World!" print(greeting) if __name__ == "__main__": display_greeting()
def get_greeting(): return "Hello, World!" if __name__ == "__main__": greeting = get_greeting() print(greeting)
🔖25: Replace Inline Code with Function Call(替換內聯程式碼為函式呼叫式)
📋
函式可以包裝行為,用命名解釋目的,消除重複程式碼。
✏
if __name__ == "__main__": x1, y1 = 1, 2 x2, y2 = 4, 6 distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 print(f"Distance between points: {distance}")
def calculate_distance(x1, y1, x2, y2): return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 if __name__ == "__main__": distance = calculate_distance(1, 2, 4, 6) print(f"Distance between points: {distance}")
🔖26: Slide Statements(移動陳述式)
📋
放置相關東西在一起會讓程式碼更易理解,像是在用變數的程式碼前面宣告變數。
✏
if __name__ == "__main__": print("Starting process...") data = "Data loaded" calculations = data + " with calculations" print("Data initialized.") print(calculations)
if __name__ == "__main__": print("Starting process...") data = "Data loaded" print("Data initialized.") calculations = data + " with calculations" print(calculations)
🔖27: Split Loop(拆開迴圈)
📋
有時迴圈一次會做兩件事,只因剛好能同時完成,但這會增加日後修改難度。拆開迴圈後,我們每次就只需了解要修改的行為即可。
分別考慮重構與優化,先釐清(重構)再優化。即便最後拆開迴圈成為效能瓶頸,再重新放一起也不難。迭代串列往往不會成為瓶頸,但拆開迴圈能引導你做出更好優化。
✏
if __name__ == "__main__": numbers = [10, 20, 30, 40, 50] total = 0 count = 0 for number in numbers: total += number count += 1 average = total / count print(f"Total: {total}, Average: {average}")
if __name__ == "__main__": numbers = [10, 20, 30, 40, 50] total = 0 count = 0 for number in numbers: total += number for number in numbers: count += 1 average = total / count print(f"Total: {total}, Average: {average}")
🔖28: Replace Loop with Pipeline(替換迴圈為流程)
📋
過往我們會用迴圈迭代物件集合,現代語言則提供集合流程(Collection Pipeline)來改善處理:裡面每項操作(像map
、filter
)都接收與送出一個集合,讓人從上而下地了解物件流經的步驟。
✏
if __name__ == "__main__": numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] even_squares = [] for number in numbers: if number % 2 == 0: even_squares.append(number * number) print(f"Even squares: {even_squares}")
if __name__ == "__main__": numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] even_squares = [number * number for number in numbers if number % 2 == 0] print(f"Even squares: {even_squares}")
🔖29: Remove Dead Code(移除死透程式碼)
📋
沒用到的程式碼應被移除,因工程師會花額外心力理解它為何存在。即便日後需要它,我們也可用版本控制系統輕鬆將其回復。
✏
if __name__ == "__main__": a = 10 b = 20 c = a + b unused_variable = 100 print(f"Sum of a and b is: {c}")
if __name__ == "__main__": a = 10 b = 20 c = a + b print(f"Sum of a and b is: {c}")
📖5: 資料類的重構
🔖30: Split Variable(拆開變數)
📋
變數有多種用途,如果它多次被重賦值,其責任可能已不只一個。
讓變數只有一個責任,具備多重責任的變數只會讓讀者加深困惑。
✏
if __name__ == "__main__": temp = 0 for i in range(5): temp += i average = temp / 5 print(f"Average: {average}") temp = "All done" print(temp)
if __name__ == "__main__": total = 0 for i in range(5): total += i average = total / 5 print(f"Average: {average}") message = "All done" print(message)
🔖31: Rename Field(更改欄位名稱)
📋
欄位名稱很重要,它們幫助人們了解程式。隨著經驗增加,對資料的了解也增加時,將新見解植入程式就是件非常重要的事。
✏
class Book: def __init__(self, title, author, numPages): self.title = title self.author = author self.numPages = numPages def print_book_details(self): print(f"Title: {self.title}, Author: {self.author}, Number of Pages: {self.numPages}") if __name__ == "__main__": book = Book("1984", "George Orwell", 328) book.print_book_details()
class Book: def __init__(self, title, author, number_of_pages): self.title = title self.author = author self.number_of_pages = number_of_ages def print_book_details(self): print(f"Title: {self.title}, Author: {self.author}, Number of Pages: {self.number_of_pages}") if __name__ == "__main__": book = Book("1984", "George Orwell", 328) book.print_book_details()
🔖32: Replace Derived Variable with Query(替換衍生變數為查詢函式)
📋
可變資料易造成問題:當它與許多程式碼有偶合關係,修改某處就會對另處產生連鎖反應。我們應盡量減少可變資料的作用域。
替換衍生變數為查詢函式是種對應作法。如果來源資料會變或衍生資料的生命週期長,使用物件包含計算式與衍生資料是個好作法;如果來源資料不會再變或衍生資料生命週期短,那用物件或用函式直接轉出新資料都可被接受。
✏
class Inventory: def __init__(self): self.items = [] self.item_count = 0 def add_item(self, item): self.items.append(item) self.item_count += 1 def print_inventory(self): print(f"Total items: {self.item_count}") for item in self.items: print(f"Item: {item}") if __name__ == "__main__": inventory = Inventory() inventory.add_item("Laptop") inventory.add_item("Phone") inventory.print_inventory()
class Inventory: def __init__(self): self.items = [] def add_item(self, item): self.items.append(item) def get_item_count(self): return len(self.items) def print_inventory(self): print(f"Total items: {self.get_item_count()}") for item in self.items: print(f"Item: {item}") if __name__ == "__main__": inventory = Inventory() inventory.add_item("Laptop") inventory.add_item("Phone") inventory.print_inventory()
🔖33: Change Reference to Value(將參考改成值)
📋
物件中的物件/資料結構,可以是個參考或是獨立值。值物件因為不可變,所以較易理解和處理:外部物件在其改變時必會知道,整體設計也不需管理記憶體連結。
這在分散(Distributed)與並行(Concurrent)系統特別好用,但不適用於多物件引用同個物件,並想讓其變動都被其他物件看到的情境中。
✏
class Author: def __init__(self, name): self.name = name def __repr__(self): return f"Author({self.name})" class Book: def __init__(self, title, author): self.title = title self.author = author def print_book_details(self): print(f"Title: {self.title}, Author: {self.author}") if __name__ == "__main__": author = Author("George Orwell") book = Book("1984", author) book.print_book_details()
class Author: def __init__(self, name): self.name = name def __repr__(self): return f"Author({self.name})" def __eq__(self, other): if not isinstance(other, Author): return NotImplemented return self.name == other.name def __hash__(self): return hash(self.name) class Book: def __init__(self, title, author): self.title = title self.author = Author(author) def print_book_details(self): print(f"Title: {self.title}, Author: {self.author}") if __name__ == "__main__": book = Book("1984", "George Orwell") book.print_book_details()
🔖34: Change Value to Reference(將值改為參考)
📋
有些情況(像是訂單)會需要多筆記錄連到同個資料結構,為能讓他們都得到最新數值。使用多筆副本是種方法,但易缺漏更新而讓資料不一致。
此時將值改為參考就是個好選擇:設定好存放區,在其中建立物件,之後就可從任意處到存放區來讀取它。
✏
class Customer: def __init__(self, name): self.name = name def __repr__(self): return f"Customer({self.name})" class Order: def __init__(self, customer_name): self.customer = Customer(customer_name) def print_order_details(self): print(f"Order for: {self.customer}") if __name__ == "__main__": order1 = Order("John Doe") order2 = Order("John Doe") print(f"Order 1 customer: {order1.customer}") print(f"Order 2 customer: {order2.customer}")
class Customer: _instances = {} @staticmethod def get_instance(name): if name not in Customer._instances: Customer._instances[name] = Customer(name) return Customer._instances[name] def __init__(self, name): self.name = name def __repr__(self): return f"Customer({self.name})" class Order: def __init__(self, customer_name): self.customer = Customer.get_instance(customer_name) def print_order_details(self): print(f"Order for: {self.customer}") if __name__ == "__main__": order1 = Order("John Doe") order2 = Order("John Doe") print(f"Order 1 customer: {order1.customer}") print(f"Order 2 customer: {order2.customer}")
📖6: 條件式的重構
🔖35: Decompose Conditional(分解條件邏輯)
📋
複雜性往往源於複雜的條件邏輯。我們可對條件各部分,依據其目的來命名:這實屬拆分函式的一類,但因很有價值而再次強調。
✏
if __name__ == "__main__": hour = 20 # 當前小時 if hour < 12: print("Good morning!") elif hour < 18: print("Good afternoon!") else: print("Good evening!")
def is_morning(hour): return hour < 12 def is_afternoon(hour): return 12 <= hour < 18 def is_evening(hour): return hour >= 18 if __name__ == "__main__": hour = 20 if is_morning(hour): print("Good morning!") elif is_afternoon(hour): print("Good afternoon!") else: print("Good evening!")
🔖36: Consolidate Conditional Expression(合併條件式)
📋
結合條件程式碼有兩個好處:表明這些檢查相合、導出進階的拆分函式。這樣能把「我在做什麼」的意義拉升到「我為什麼要做」。
相反地,互相獨立的檢查就不該合併。
✏
def should_send_notification(day, is_holiday, is_weekend): if day == "Sunday": return False if is_holiday: return False if is_weekend: return False return True if __name__ == "__main__": notification_status = should_send_notification("Monday", False, False) print("Send notification:", notification_status)
def should_send_notification(day, is_holiday, is_weekend): if day == "Sunday" or is_holiday or is_weekend: return False return True if __name__ == "__main__": notification_status = should_send_notification("Monday", False, False) print("Send notification:", notification_status)
🔖37: Replace Nested Conditional with Guard Clauses(替換內嵌條件式為防衛敘句)
📋
條件式通常現於兩種情境:兩分支皆屬正常行為、和一分支為正常,另分支為異常。在一正常一異常的情況,就可以採用防衛敘句(提早return
),顯出異常情況非此函式的邏輯核心。
✏
def calculate_pay(employee): if employee.is_retired: if employee.age > 65: return 0 else: return employee.pension else: if employee.is_on_leave: return 0 else: return employee.salary if __name__ == "__main__": class Employee: def __init__(self, is_retired, age, pension, salary, is_on_leave): self.is_retired = is_retired self.age = age self.pension = pension self.salary = salary self.is_on_leave = is_on_leave employee = Employee(False, 34, 0, 5000, False) pay = calculate_pay(employee) print(f"Employee pay: {pay}")
def calculate_pay(employee): if employee.is_retired: if employee.age > 65: return 0 return employee.pension if employee.is_on_leave: return 0 return employee.salary if __name__ == "__main__": class Employee: def __init__(self, is_retired, age, pension, salary, is_on_leave): self.is_retired = is_retired self.age = age self.pension = pension self.salary = salary self.is_on_leave = is_on_leave employee = Employee(False, 34, 0, 5000, False) pay = calculate_pay(employee) print(f"Employee pay: {pay}")
🔖38: Replace Conditional with Polymorphism(替換條件式為多型)
📋
複雜的條件邏輯很難理解,一種改善法是拆解成不同環境,用類別與多型會讓這種分解更明確。
✏
class Vehicle: def __init__(self, vehicle_type): self.vehicle_type = vehicle_type def get_speed(self): if self.vehicle_type == "car": return 100 elif self.vehicle_type == "bicycle": return 20 elif self.vehicle_type == "boat": return 30 else: return 0 if __name__ == "__main__": car = Vehicle("car") print(f"Car speed: {car.get_speed()}") bicycle = Vehicle("bicycle") print(f"Bicycle speed: {bicycle.get_speed()}") boat = Vehicle("boat") print(f"Boat speed: {boat.get_speed()}")
class Vehicle: def get_speed(self): return 0 class Car(Vehicle): def get_speed(self): return 100 class Bicycle(Vehicle): def get_speed(self): return 20 class Boat(Vehicle): def get_speed(self): return 30 if __name__ == "__main__": car = Car() print(f"Car speed: {car.get_speed()}") bicycle = Bicycle() print(f"Bicycle speed: {bicycle.get_speed()}") boat = Boat() print(f"Boat speed: {boat.get_speed()}")
🔖39: Introduce Special Case(引入特例)
📋
當某資料結構被用時,大都檢查其中某值並採取同樣行動,使用特例元素就能把這類情境換成簡單呼叫式。
特例能有多種方式呈現:
- 某物件只會被讀取,就可設為物件常值(Literal),放入所需值
- 需要存值還要其他行為,就建立一個特殊物件來包含
- Null Object就是種特例
✏
class Customer: def __init__(self, name, plan, is_active): self.name = name self.plan = plan self.is_active = is_active def get_billing_plan(self): if not self.is_active: return "basic" return self.plan if __name__ == "__main__": active_customer = Customer("Alice", "premium", True) inactive_customer = Customer("Bob", "premium", False) print(f"Active customer plan: {active_customer.get_billing_plan()}") print(f"Inactive customer plan: {inactive_customer.get_billing_plan()}")
class Customer: def __init__(self, name, plan, is_active): self.name = name self.plan = plan self.is_active = is_active def get_billing_plan(self): return self.plan class InactiveCustomer(Customer): def __init__(self): super().__init__(name="N/A", plan="basic", is_active=False) def get_billing_plan(self): return "basic" if __name__ == "__main__": active_customer = Customer("Alice", "premium", True) inactive_customer = InactiveCustomer() print(f"Active customer plan: {active_customer.get_billing_plan()}") print(f"Inactive customer plan: {inactive_customer.get_billing_plan()}")
🔖40: Introduce Assertion(引入斷言)
📋
有些程式只該在條件滿足時才執行,但這些條件往往不會具體說明,只能藉由閱讀演算法來推導。比起使用註解,用斷言明言假設是更好的技術做法。
斷言永遠假定為真,所以系統其他部分不該檢查失敗情況,移除所有斷言程式仍該能正常運作。斷言是個寶貴的溝通工具,雖然用逐步縮小範圍的單元測試會有更好表現,但它們仍提供系統執行到當前會有的假定狀態。
✏
def calculate_area(length, width): return length * width if __name__ == "__main__": length = 10 width = -5 area = calculate_area(length, width) print(f"Area: {area}")
def calculate_area(length, width): assert length > 0, "Length must be positive" assert width > 0, "Width must be positive" return length * width if __name__ == "__main__": length = 10 width = -5 area = calculate_area(length, width) print(f"Area: {area}")
📖7: API類的重構
🔖41: Separate Query from Modifier(分離查詢與修改函式)
📋
只提供值且無可見副作用的函式,非常寶貴,這也是命令查詢分離原則的精神。之所以強調可見,是因像快取這類雖會改變物件狀態,仍讓每個查詢都回傳相同結果就是被允許的。
✏
def send_and_confirm_email(emails, message): sent_emails = [] for email in emails: print(f"Sending email to {email}") sent_emails.append(email) return sent_emails if __name__ == "__main__": emails = ["john@example.com", "jane@example.com"] sent_emails = send_and_confirm_email(emails, "Hello!") print(f"Sent emails: {sent_emails}")
def send_email(emails, message): for email in emails: print(f"Sending email to {email}") def confirm_sent_emails(emails): return emails if __name__ == "__main__": emails = ["john@example.com", "jane@example.com"] send_email(emails, "Hello!") sent_emails = confirm_sent_emails(emails) print(f"Sent emails: {sent_emails}")
🔖42: Parameterize Function(參數化函式)
📋
當有不同函式具備相似邏輯,僅差在用不同常數值,則可合成同個函式,用參數顯出差異,移除重複。
✏
def print_hello(): print("Hello!") def print_goodbye(): print("Goodbye!") if __name__ == "__main__": print_hello() print_goodbye()
def print_greeting(message): print(message) if __name__ == "__main__": print_greeting("Hello!") print_greeting("Goodbye!")
🔖43: Remove Flag Argument(移除旗標引數)
📋
旗標引數用來指示函式該執行哪段邏輯,但這種設計會加深理解難度,用明確函式代表我想做的事較易讓人理解。
但是如果有多個旗標引數,則不該直接移除,提升複雜度。先建立簡單函式,並用它們組合出原先邏輯。
✏
def print_message(is_hello): if is_hello: print("Hello!") else: print("Goodbye!") if __name__ == "__main__": print_message(True) print_message(False)
def print_hello(): print("Hello!") def print_goodbye(): print("Goodbye!") if __name__ == "__main__": print_hello() print_goodbye()
🔖44: Preserve Whole Object(保留整個物件)
📋
當函式需要物件中某些值時,作者喜歡提供整個物件,讓函式自行取用。參數列長度會減少,日後引數更動也不用再做大幅度變動。若發現很多地方都僅用部分記錄呼叫不同函式,就代表這類邏輯為重複,可搬為一個整體邏輯。有時不要做這類重構,當函式與整個記錄處於不同模組,希望它們不互相依賴。
從單一物件拉出值並執行邏輯,通常代表這邏輯應移入整體邏輯;若有些程式都只用同物件的同組子功能,就代表該提取類別了
✏
class Range: def __init__(self, start, end): self.start = start self.end = end def is_in_range(value, start, end): return start <= value <= end if __name__ == "__main__": range = Range(1, 10) value = 5 if is_in_range(value, range.start, range.end): print(f"{value} is within the range.") else: print(f"{value} is not within the range.")
class Range: def __init__(self, start, end): self.start = start self.end = end def contains(self, value): return self.start <= value <= self.end if __name__ == "__main__": range = Range(1, 10) value = 5 if range.contains(value): print(f"{value} is within the range.") else: print(f"{value} is not within the range.")
🔖45: Replace Parameter with Query(替換參數為查詢程式)
📋
參數列應避免重複和保持簡短,若有資訊能自己取得,且不新增依賴關係,就不用當成引數傳入。
讓函式具備引用透明性很重要(同參數會得到同行為),這類函式易理解與測試,所以別用會變的全域變數來取代引入參數。
✏
class Employee: def __init__(self, name, department): self.name = name self.department = department def calculate_bonus(self, performance_rating): if self.department == "Sales": if performance_rating > 3: return 1000 return 500 else: if performance_rating > 3: return 500 return 200 if __name__ == "__main__": employee = Employee("John Doe", "Sales") bonus = employee.calculate_bonus(4) print(f"Bonus for {employee.name}: ${bonus}")
class Employee: def __init__(self, name, department, performance_rating): self.name = name self.department = department self.performance_rating = performance_rating def calculate_bonus(self): if self.department == "Sales": if self.performance_rating > 3: return 1000 return 500 else: if self.performance_rating > 3: return 500 return 200 if __name__ == "__main__": employee = Employee("John Doe", "Sales", 4) bonus = employee.calculate_bonus() print(f"Bonus for {employee.name}: ${bonus}")
🔖46: Replace Query with Parameter(替換查詢式為參數)
📋
當函式有不理想的依賴時,適合採取這手法。「將東西都轉為參數」與「共享大量範圍」是兩個極端,我們能藉知識增長而調整出好的平衡。
替換查詢式為參數,就是請呼叫方自己提供此值,這容易違反介面應方便被用的原則。如何在程式中安排責任是個關鍵,這不容易也非不可變,這也是我們得熟悉它與替換參數為查詢程式的原因。
✏
class Customer: def __init__(self, name, location): self.name = name self.location = location def get_location(self): return self.location def calculate_shipping(): customer = Customer("Alice", "Taipei") if customer.get_location() == "Taipei": return 10 else: return 20 if __name__ == "__main__": shipping_cost = calculate_shipping() print(f"Shipping Cost: ${shipping_cost}")
class Customer: def __init__(self, name, location): self.name = name self.location = location def calculate_shipping(location): if location == "Taipei": return 10 else: return 20 if __name__ == "__main__": customer = Customer("Alice", "Taipei") shipping_cost = calculate_shipping(customer.location) print(f"Shipping Cost: ${shipping_cost}")
🔖47: Remove Setting Method(移除Set
方法)
📋
物件具備set
就代表此欄位可被改變,移除它則代表此物件被建構後就不可被改變。
✏
class Account: def __init__(self, id): self.id = id def set_id(self, id): self.id = id if __name__ == "__main__": account = Account(123) print(f"Initial Account ID: {account.id}") account.set_id(456) print(f"Updated Account ID: {account.id}")
class Account: def __init__(self, id): self.id = id if __name__ == "__main__": account = Account(123) print(f"Account ID: {account.id}")
🔖48: Replace Constructor with Factory Function(替換建構式為工廠函式)
📋
建構式有許多限制,也無法用名稱表達意涵,但工廠函式可以。
✏
class Book: def __init__(self, title, author, genre): self.title = title self.author = author self.genre = genre if __name__ == "__main__": book = Book("1984", "George Orwell", "Dystopian") print(f"Book: {book.title} by {book.author}, Genre: {book.genre}")
class Book: def __init__(self, title, author, genre): self.title = title self.author = author self.genre = genre @classmethod def create_book(cls, title, author, genre): return cls(title, author, genre) if __name__ == "__main__": book = Book.create_book("1984", "George Orwell", "Dystopian") print(f"Book: {book.title} by {book.author}, Genre: {book.genre}")
🔖49: Replace Function with Command(替換函式為命令物件)
📋
將函式封裝成命令物件有時很有用,它大多僅有一種方法,其功能就是對這方法的請求與執行。相比於一般函式,命令更加靈活,但也會帶來更多複雜度。
通常選擇函式即可,在簡單作法仍無法提供所需功能時才改用命令。
✏
def print_details(name, age): print(f"Name: {name}, Age: {age}") if __name__ == "__main__": print_details("Alice", 30)
class PrintDetailsCommand: def __init__(self, name, age): self.name = name self.age = age def execute(self): print(f"Name: {self.name}, Age: {self.age}") if __name__ == "__main__": cmd = PrintDetailsCommand("Alice", 30) cmd.execute()
🔖50: Replace Command with Function(替換命令物件為函式)
📋
命令善於複雜的計算,因它能具備不同方法,並讓欄位共享狀態。但若你只要函式去做它該做的,就不用去維護命令對應的複雜度,轉成一般函式即可。
✏
class PrintDetailsCommand: def __init__(self, name, age): self.name = name self.age = age def execute(self): print(f"Name: {self.name}, Age: {self.age}") if __name__ == "__main__": cmd = PrintDetailsCommand("Alice", 30) cmd.execute()
def print_details(name, age): print(f"Name: {name}, Age: {age}") if __name__ == "__main__": print_details("Alice", 30)
📖8: 繼承類的重構
🔖51: Pull Up Method(提升方法)
📋
移除重複程式碼很重要,其功能再完善也只是孳生Bug的溫床。提升方法因此重要,但需要穩健的測試來驗證。通常我們會需先做參數化函式或提升欄位,才會再採用此重構。
✏
class Vehicle: pass class Car(Vehicle): def start_engine(self): print("Car engine started.") class Truck(Vehicle): def start_engine(self): print("Truck engine started.") if __name__ == "__main__": car = Car() truck = Truck() car.start_engine() truck.start_engine()
class Vehicle: def start_engine(self): print("Engine started.") class Car(Vehicle): pass class Truck(Vehicle): pass if __name__ == "__main__": car = Car() truck = Truck() car.start_engine() truck.start_engine()
🔖52: Pull Up Field(提升欄位)
📋
若兩子類別有名稱相似欄位,被使用的方式也相似時,就可採用此手法。這樣除了移除重複的資料宣告外,也將對應行為統一移至超類別中。
✏
class Vehicle: pass class Car(Vehicle): def __init__(self): self.vehicle_type = "Car" class Truck(Vehicle): def __init__(self): self.vehicle_type = "Truck" if __name__ == "__main__": car = Car() truck = Truck() print(f"This is a {car.vehicle_type}.") print(f"This is a {truck.vehicle_type}.")
class Vehicle: def __init__(self, vehicle_type): self.vehicle_type = vehicle_type class Car(Vehicle): def __init__(self): super().__init__("Car") class Truck(Vehicle): def __init__(self): super().__init__("Truck") if __name__ == "__main__": car = Car() truck = Truck() print(f"This is a {car.vehicle_type}.") print(f"This is a {truck.vehicle_type}.")
🔖53: Pull Up Constructor Body(提升建構式內文)
📋
建構式不是普通方法,所以有許多限制。若當下情境用此手法會過於複雜,可試著採用替換建構式為工廠函式。
✏
class Vehicle: pass class Car(Vehicle): def __init__(self, make, model): self.make = make self.model = model self.wheels = 4 class Truck(Vehicle): def __init__(self, make, model): self.make = make self.model = model self.wheels = 6 if __name__ == "__main__": car = Car("Toyota", "Corolla") truck = Truck("Ford", "F-150") print(f"Car: {car.make} {car.model}, Wheels: {car.wheels}") print(f"Truck: {truck.make} {truck.model}, Wheels: {truck.wheels}")
class Vehicle: def __init__(self, make, model, wheels): self.make = make self.model = model self.wheels = wheels class Car(Vehicle): def __init__(self, make, model): super().__init__(make, model, 4) class Truck(Vehicle): def __init__(self, make, model): super().__init__(make, model, 6) if __name__ == "__main__": car = Car("Toyota", "Corolla") truck = Truck("Ford", "F-150") print(f"Car: {car.make} {car.model}, Wheels: {car.wheels}") print(f"Truck: {truck.make} {truck.model}, Wheels: {truck.wheels}")
🔖54: Push Down Method(下移方法)
📋
若超類別中有個方法只與某子類別有關,那下移它是個好選擇,但僅限於呼叫方已知它要用哪個特定子類別時。
✏
class Vehicle: def start_engine(self): print("Engine started.") class Car(Vehicle): pass class Motorcycle(Vehicle): pass
class Vehicle: pass class Car(Vehicle): def start_engine(self): print("Car engine started.") class Motorcycle(Vehicle): def start_engine(self): print("Motorcycle engine started.")
🔖55: Push Down Field(下移欄位)
📋
若欄位只被一個子類別使用,那就下移吧。
✏
class Vehicle: def __init__(self, name): self.name = name class Car(Vehicle): def display_name(self): print(f"This car is named: {self.name}") class Motorcycle(Vehicle): def display_name(self): print(f"This motorcycle is named: {self.name}")
class Vehicle: pass class Car(Vehicle): def __init__(self, name): self.name = name def display_name(self): print(f"This car is named: {self.name}") class Motorcycle(Vehicle): def __init__(self, name): self.name = name def display_name(self): print(f"This motorcycle is named: {self.name}")
🔖56: Replace Type Code with Subclasses(替換型別程式碼為子類別)
📋
軟體中常會遇到需要描述相似的東西:初期可用型別區分,但更好的做法是使用子類別。這樣可以善用多型功能,也可讓某些欄位或函式限縮在特定子類別中。
要替換的對象是整體類別還是當中型別?我們可先將型別換成物件類別,再對其用此手法。
✏
class Employee: def __init__(self, name, type_code): self.name = name self.type_code = type_code def get_role(self): if self.type_code == 0: return "Manager" elif self.type_code == 1: return "Engineer"
class Employee: def __init__(self, name): self.name = name class Manager(Employee): def get_role(self): return "Manager" class Engineer(Employee): def get_role(self): return "Engineer"
🔖57: Remove Subclass(移除子類別)
📋
子類別具備提供資料結構的變體與多型行為等優點。但隨著時間演變,它可能已失去其變異性(Difference),移除它,轉為超類別欄位才能有效降低程式碼複雜度。
✏
class Animal: pass class Dog(Animal): def bark(self): print("Bark!") class Cat(Animal): def meow(self): print("Meow!")
class Animal: def __init__(self, animal_type): self.animal_type = animal_type def make_sound(self): if self.animal_type == "dog": print("Bark!") elif self.animal_type == "cat": print("Meow!")
🔖58: Extract Superclass(提取超類別)
📋
若有兩類別都做相同事,將相似處拉到超類別是個好主意。我們不用在設計初期就依據真實世界的架構來定義繼承關係,在程式演變的過程中逐漸實作也很不錯。
提取類別(委託)也是種做法,而提取超類別(繼承)比較簡單。我們可以先執行後者,日後再用替換子類別為委託類別來解決繼承長期會衍生的混亂。
✏
class Employee: def __init__(self, name): self.name = name def report_hours(self): print("Reporting hours") class Manager: def __init__(self, name): self.name = name def create_report(self): print("Creating report")
class StaffMember: def __init__(self, name): self.name = name def report_hours(self): print("Reporting hours") class Employee(StaffMember): pass class Manager(StaffMember): def create_report(self): print("Creating report")
🔖59: Collapse Hierarchy(摺疊類別階層)
📋
發現某類別與其父類別差異已小到沒必要分開時,合併它們吧。
✏
class Person: def __init__(self, name): self.name = name def greet(self): print(f"Hello, my name is {self.name}") class Employee(Person): def work(self): print("Working...")
class Person: def __init__(self, name): self.name = name def greet(self): print(f"Hello, my name is {self.name}") def work(self): print("Working...")
🔖60: Replace Subclass with Delegate(替換子類別為委託類別)
📋
繼承有其缺點:它只能順著單一路線演化,且會因關係緊密而難以修改。
委託可以解決這類問題,有人因此更偏好它。但多數情況下繼承都已能妥善解決問題,之後將其換成委託也毫無困難,作者建議採用「先繼承後改委託」這策略來設計軟體。
✏
class Renderer: def render(self, content): print(f"Rendering: {content}") class HTMLRenderer(Renderer): def render(self, content): print(f"<html>{content}</html>") class JSONRenderer(Renderer): def render(self, content): print(f"{{'content': '{content}'}}")
class Renderer: def __init__(self, rendering_strategy): self.rendering_strategy = rendering_strategy def render(self, content): self.rendering_strategy.render(content) class HTMLRendering: def render(self, content): print(f"<html>{content}</html>") class JSONRendering: def render(self, content): print(f"{{'content': '{content}'}}")
🔖61: Replace Superclass with Delegate(替換超類別為委託類別)
📋
繼承強大而簡單,但實務上常會帶來混亂與複雜。像是堆疊(Stack
)曾被做為串列(List
)的子類別,但實應把串列當成堆疊的欄位,將必要操作委託給它即可,因為許多串列的操作並不屬於堆疊範疇。
使用委託可以明確表達它是個獨立、僅保留某功能的東西。權衡之下,作者建議先用繼承,出問題時再用此重構改為委託。
✏
class Logger: def log(self, message): print(f"Log: {message}") class FileLogger(Logger): def log_to_file(self, message): print(f"Writing to file: {message}")
class Logger: def log(self, message): print(f"Log: {message}") class FileLogger: def __init__(self): self.logger = Logger() def log_to_file(self, message): self.logger.log(message) print(f"Writing to file: {message}")
🚀
一個好的軟體開發團隊,程式碼審查(Code Review)為流程要素之一。而好的程式碼審查,往往是相信開發者的改動已符合功能需求,討論會聚焦於將程式碼寫得更好和更正確。這些內容大抵就是在討論重構-不改變可見行為時變動軟體內部,為增進程式碼的可讀性,降低維護成本。依據本書介紹的重構手法去給予建議,有許多好處。除了使用專業術語能加快溝通速度,弭平認知差距之外,更好的做法往往也會在一步步的重構中才逐漸顯露出來。
儘管作者已用包含多個重構步驟的經典例子來說明每個手法,掌握它們的最好途徑,還是在實際的開發任務中感受重構前後的差異,進而於其他情境中辨識出施展時機。當我們能瞭解作者所附微小提點的意思,就代表我們更像大師一些,而這實屬人生最美好的經驗之一。