軟體設計的核心在於平衡模組內部的緊密性(高內聚)和模組之間的獨立性(低耦合)。程式碼重複是設計不良的指標,應遵循 DRY 原則,只在單一位置定義知識。YAGNI 原則提醒開發者避免過度工程,專注於當前需求。繼承雖能促程式式碼重用,但應避免領域物件與實作物件混淆,優先考慮組合來降低耦合。理解並應用這些原則,能有效提升軟體品質。
軟體設計的核心原則:高內聚與低耦合
在軟體開發領域,高內聚(High Cohesion)與低耦合(Low Coupling)是兩個基本且重要的設計原則。高內聚指的是模組內部元素彼此相關性強,功能緊密相關;而低耦合則強調不同模組之間的依賴關係應盡量減少,以提高系統的可維護性和可擴充套件性。
不良耦合的後果
當程式碼(物件或方法)之間的依賴關係過於緊密時,會帶來一些不良後果:
- 無法重用程式碼:如果一個函式過於依賴特定的物件,或是需要過多的引數,那麼它就與該物件緊密耦合,這使得在不同的上下文中重用該函式變得非常困難。
- 連鎖反應:一方變動會對另一方產生影響,因為它們之間的關聯太密切。
- 抽象層次不清晰:當兩個函式緊密相關時,很難將它們視為不同的關注點,在不同的抽象層次上解決問題。
簡單記住良好設計的原則
本文將介紹一些能夠引匯出良好設計理念的原則。這些原則以易於記憶的首字母縮寫形式呈現,作為一種助記規則。當你記住這些單詞時,你將能夠更容易地將它們與良好的實踐聯絡起來,並且在檢視特定程式碼時,能更快地理解其背後的理念。
DRY/OAOO:避免重複
不要重複自己(DRY)和一次性且唯一性(OAOO)的理念密切相關。程式碼中的知識應該只被定義一次且位於單一位置。當需要對程式碼進行更改時,應該只有一個正確的位置進行修改。未能做到這一點是系統設計不良的跡象。
def score_for_student(student):
return student.passed * 11 - student.failed * 5 - student.years
def process_students_list(students):
# 一些處理...
students_ranking = sorted(students, key=score_for_student)
# 更多處理...
for student in students_ranking:
print("Name: {0}, Score: {1}".format(student.name, score_for_student(student)))
YAGNI:你不需要它
YAGNI(You Ain’t Gonna Need It)是一個在編寫解決方案時經常需要記住的理念,如果你不想過度設計它。很多開發者認為他們需要預測所有的未來需求,並建立非常複雜的解決方案,但事實往往證明那些預測的需求並未出現,或者以不同的方式出現,導致原來的程式碼難以重構和擴充套件。
內容解密:
- 高內聚與低耦合的重要性:強調了在軟體設計中保持模組內部高內聚和模組間低耦合的必要性,以提高軟體的可維護性和可擴充套件性。
- DRY/OAOO原則的應用:透過避免程式碼重複來提高可維護性,示例中透過建立
score_for_student函式來消除重複計算學生分數的邏輯。 - YAGNI原則的指導意義:提醒開發者不要過度設計,應根據實際需求進行開發,避免預測未來需求而導致的複雜設計。
軟體設計原則:YAGNI、KIS、EAFP 與 LBYL
在軟體開發過程中,遵循適當的設計原則對於建立可維護、可擴充套件的系統至關重要。本文將探討四個重要的設計原則:YAGNI(You Ain’t Gonna Need It)、KIS(Keep It Simple)、EAFP(Easier to Ask Forgiveness than Permission)與 LBYL(Look Before You Leap),並透過例項與程式碼範例來闡述這些原則的實際應用。
YAGNI:不預先設計不需要的功能
YAGNI 原則強調的是避免在當前需求之外預先設計或實作功能。這個原則的核心思想是,開發者不應該試圖預測未來的需求,而是應該專注於滿足當前需求,並確保程式碼具有足夠的彈性,以便在未來需求變更時能夠輕易調整。
為什麼 YAGNI 重要?
- 避免過度設計:過早地泛化或抽象化可能會導致不必要的複雜性,增加維護成本。
- 資源最佳化:專注於當前需求可以更有效地利用開發資源。
- 提高適應性:保持程式碼的靈活性,使其更容易適應未來的變化。
YAGNI 實踐範例
假設我們正在開發一個使用者管理系統,目前只需要實作一個簡單的使用者類別。根據 YAGNI 原則,我們不應該預先設計一個複雜的抽象類別或介面,除非當前需求明確要求這麼做。
class User:
def __init__(self, id_, username):
self.id_ = id_
self.username = username
# 當前需求滿足,無需預先抽象化
內容解密:
- 這個
User類別目前只包含id_和username兩個屬性,滿足當前需求。 - 如果未來需要擴充套件更多型別的使用者,可以再考慮抽象化或使用繼承。
- 這種做法避免了過早抽象化可能帶來的複雜性。
KIS:保持簡單
KIS 原則提倡在設計和實作軟體時保持簡單性,避免不必要的複雜化。簡單的設計更容易理解、維護和擴充套件。
如何實踐 KIS?
- 最小化功能:只實作當前需求所需的功能。
- 簡化資料結構:使用最簡單的資料結構來解決問題。
- 避免過度工程:除非必要,否則避免使用複雜的設計模式或技術。
KIS 實踐範例
比較兩個不同的實作方式,一個是複雜的,另一個是簡單的:
# 複雜實作
class ComplicatedNamespace:
ACCEPTED_VALUES = ("id_", "user", "location")
@classmethod
def init_with_data(cls, **data):
instance = cls()
for key, value in data.items():
if key in cls.ACCEPTED_VALUES:
setattr(instance, key, value)
return instance
# 簡單實作
class Namespace:
ACCEPTED_VALUES = ("id_", "user", "location")
def __init__(self, **data):
for attr_name, attr_value in data.items():
if attr_name in self.ACCEPTED_VALUES:
setattr(self, attr_name, attr_value)
內容解密:
ComplicatedNamespace使用了一個額外的類別方法init_with_data來初始化物件,這增加了使用的複雜性。Namespace直接在__init__方法中處理初始化邏輯,簡化了介面和實作。- 簡單的設計使程式碼更易讀、易維護。
EAFP 與 LBYL:錯誤處理策略
EAFP 和 LBYL 是兩種不同的錯誤處理策略。EAFP 主張直接執行操作,並在出錯時處理異常;而 LBYL 則建議在執行操作前先進行檢查,以避免錯誤。
EAFP vs. LBYL
- EAFP:
try: with open(filename) as f: # 操作檔案 except FileNotFoundError: # 處理檔案不存在的情況 - LBYL:
if os.path.exists(filename): with open(filename) as f: # 操作檔案 else: # 處理檔案不存在的情況
內容解密:
- EAFP 直接嘗試開啟檔案,如果檔案不存在,則捕捉
FileNotFoundError異常進行處理。 - LBYL 先檢查檔案是否存在,如果存在則開啟,否則處理檔案不存在的情況。
- EAFP 在某些情況下更符合 Python 的哲學(例如,當檢查和操作之間可能發生變化時)。
Python中的例外處理與繼承機制
在軟體開發過程中,如何有效地處理錯誤和設計類別架構是兩個重要的課題。Python作為一種物件導向的程式語言,提供了強大的例外處理機制和繼承功能,可以幫助開發者寫出更穩健、更易於維護的程式碼。
例外處理:EAFP vs. LBYL
在處理可能出現的錯誤時,Python 社群傾向於採用 EAFP(Easier to Ask for Forgiveness than Permission)策略,而不是 LBYL(Look Before You Leap)。EAFP 的核心思想是先嘗試執行某個操作,如果失敗了,再處理例外狀況。
try:
with open('example.txt', 'r') as file:
content = file.read()
except FileNotFoundError as e:
logger.error(e)
內容解密:
- 嘗試開啟檔案:程式首先嘗試開啟名為
example.txt的檔案。 - 讀取檔案內容:如果檔案成功開啟,則讀取其內容。
- 處理例外:如果檔案不存在,捕捉
FileNotFoundError例外並記錄錯誤訊息。
EAFP 的好處在於程式碼更直接地表達了主要的操作邏輯,而不是被大量的檢查程式碼所淹沒。這種方式使得程式碼更易讀,也減少了因檢查不周全而導致的錯誤。
繼承機制
繼承是物件導向程式設計中的一個重要概念,允許開發者建立一個新的類別,該類別繼承了父類別的屬性和方法。
何時使用繼承
- 程式碼重用:雖然繼承可以幫助重用程式碼,但不應該僅僅為了重用程式碼而使用繼承。正確的做法是設計高內聚、易於組合的類別。
- 特殊化:當需要建立一個新的類別,它是現有類別的特殊版本時,繼承是一個好的選擇。
from http.server import BaseHTTPRequestHandler, SimpleHTTPRequestHandler
# BaseHTTPRequestHandler 是一個基礎類別,定義了處理 HTTP 請求的基本介面
# SimpleHTTPRequestHandler 繼承自 BaseHTTPRequestHandler,並新增或改變了一些行為
內容解密:
BaseHTTPRequestHandler:這是一個基礎類別,定義了處理 HTTP 請求的基本介面。SimpleHTTPRequestHandler:這是一個子類別,繼承自BaseHTTPRequestHandler,並對其進行了擴充套件或修改。
反模式
- 過度繼承:不應該僅僅為了重用程式碼而使用繼承。如果子類別並不需要父類別的大部分方法,這可能是一個設計錯誤。
- 父類別定義不明確:如果父類別的責任不明確或承擔了太多的功能,這可能會導致子類別難以正確地繼承或擴充套件其行為。
總之,Python 中的例外處理和繼承機制是強大的工具,可以幫助開發者寫出更穩健、更易於維護的程式碼。正確地使用這些機制,需要深入理解其背後的原理和最佳實踐。
正確使用繼承與組合:避免將實作物件與領域物件混淆
在軟體開發中,正確地使用繼承(Inheritance)與組合(Composition)是設計良好類別結構的關鍵。不當的繼承使用不僅會導致程式碼難以理解和維護,還可能引入不必要的耦合(Coupling)。本文將透過一個保險管理系統的例子,探討如何正確地使用繼承與組合,以避免將實作物件與領域物件混淆。
繼承的誤用:將領域物件變為實作物件
在設計一個用於管理保險政策的系統時,我們需要一個類別來表示交易中的政策(TransactionalPolicy)。最初,我們可能認為透過繼承 collections.UserDict 來實作 TransactionalPolicy 是一個好主意,因為這樣可以輕易地使 TransactionalPolicy 成為一個可 subscript 的物件。
class TransactionalPolicy(collections.UserDict):
"""錯誤使用繼承的例子。"""
# ...
然而,這種做法有兩個主要問題:
錯誤的層次結構:
TransactionalPolicy繼承自UserDict意味著TransactionalPolicy是一種特殊的字典(Dictionary)。然而,在領域模型中,TransactionalPolicy代表的是一種保險交易政策,而不是一種資料結構。過度的耦合:由於繼承了
UserDict,TransactionalPolicy自動獲得了許多不必要的字典方法,如pop()、items()等。這些方法對於TransactionalPolicy的使用者來說可能是無關或甚至有害的。
正確的設計:使用組合
正確的做法是使用組合(Composition)來設計 TransactionalPolicy。我們可以讓 TransactionalPolicy 內部持有一個字典物件,並透過代理(Proxy)的方式實作必要的介面。
class TransactionalPolicy:
"""重構後使用組合的例子。"""
def __init__(self, policy_data, **extra_data):
self._data = {**policy_data, **extra_data}
def change_in_policy(self, customer_id, **new_policy_data):
self._data[customer_id].update(**new_policy_data)
def __getitem__(self, customer_id):
return self._data[customer_id]
內容解密:
__init__方法:初始化_data屬性,將傳入的policy_data和extra_data合併成一個字典儲存。change_in_policy方法:根據客戶 ID 更新對應的政策資料。__getitem__方法:代理_data字典的取值操作,使TransactionalPolicy物件可以像字典一樣被存取。
多重繼承與方法解析順序(MRO)在Python中的應用
在探討Python中的物件導向程式設計時,多重繼承是一個強大但需要謹慎使用的功能。正確使用多重繼承可以帶來許多好處,如提高程式碼的重用性和靈活性,但如果處理不當,也可能導致程式設計上的複雜性和維護困難。
多重繼承的問題與解決方案
當一個類別從多個基礎類別繼承,而這些基礎類別又分享共同的祖先類別時,就會出現所謂的「菱形問題」(diamond problem)。這個問題的核心在於,當子類別呼叫一個方法時,Python需要決定應該使用哪個基礎類別中的方法實作。
方法解析順序(MRO)
Python透過一種稱為C3線性化或MRO(Method Resolution Order)的演算法來解決這個問題。MRO定義了一種確定性的方式來解析方法呼叫的順序,從而避免了多重繼承可能帶來的混淆。
class BaseModule:
module_name = "top"
def __init__(self, module_name):
self.name = module_name
def __str__(self):
return f"{self.module_name}:{self.name}"
class BaseModule1(BaseModule):
module_name = "module-1"
class BaseModule2(BaseModule):
module_name = "module-2"
class ConcreteModuleA12(BaseModule1, BaseModule2):
"""Extend 1 & 2"""
# 測試MRO
print(str(ConcreteModuleA12("test"))) # 輸出:module-1:test
print([cls.__name__ for cls in ConcreteModuleA12.mro()])
# 輸出:['ConcreteModuleA12', 'BaseModule1', 'BaseModule2', 'BaseModule', 'object']
Mixin類別
Mixin是一種特殊的基礎類別,它封裝了一些通用的行為,以便於在多個類別中重用程式碼。Mixin類別通常不獨立使用,而是透過多重繼承與其他類別結合使用。
class UpperIterableMixin:
def __iter__(self):
return map(str.upper, super().__iter__())
class BaseTokenizer:
def __init__(self, str_token):
self.str_token = str_token
def __iter__(self):
yield from self.str_token.split("-")
class Tokenizer(UpperIterableMixin, BaseTokenizer):
pass
# 測試Tokenizer
tk = Tokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0")
print(list(tk)) # 輸出:['28A2320B', 'FD3F', '4627', '9792', 'A2B38E3C46B0']
#### 內容解密:
- MRO的作用:MRO透過C3線性化演算法,確保了在多重繼承的情況下,方法呼叫的順序是明確且一致的。
- Mixin的應用:Mixin類別提供了一種靈活的方式來重用程式碼,透過與其他類別結合,可以實作多樣化的功能。
- 設計考量:在使用多重繼承和Mixin時,需要仔細設計類別的繼承關係,以避免過於複雜的繼承結構。