最後更新日期: 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, "[email protected]")
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, "[email protected]")
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 = "[email protected]" 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("[email protected]")
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", "[email protected]", "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", "[email protected]", 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", "[email protected]")
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", "[email protected]")
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", "[email protected]")
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", "[email protected]")
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", "[email protected]", 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", "[email protected]")
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 = ["[email protected]", "[email protected]"]
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 = ["[email protected]", "[email protected]"]
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)為流程要素之一。而好的程式碼審查,往往是相信開發者的改動已符合功能需求,討論會聚焦於將程式碼寫得更好和更正確。這些內容大抵就是在討論重構-不改變可見行為時變動軟體內部,為增進程式碼的可讀性,降低維護成本。依據本書介紹的重構手法去給予建議,有許多好處。除了使用專業術語能加快溝通速度,弭平認知差距之外,更好的做法往往也會在一步步的重構中才逐漸顯露出來。
儘管作者已用包含多個重構步驟的經典例子來說明每個手法,掌握它們的最好途徑,還是在實際的開發任務中感受重構前後的差異,進而於其他情境中辨識出施展時機。當我們能瞭解作者所附微小提點的意思,就代表我們更像大師一些,而這實屬人生最美好的經驗之一。
