返回文章列表

軟體設計原則內聚耦合與繼承組合

本文探討軟體設計中的重要原則:高內聚、低耦合、DRY、YAGNI、繼承與組合,並以 Python 程式碼示例說明如何在實務中應用這些原則,避免常見的設計錯誤,提升程式碼的可維護性和可擴充套件性。

軟體設計 Python

軟體設計的核心在於平衡模組內部的緊密性(高內聚)和模組之間的獨立性(低耦合)。程式碼重複是設計不良的指標,應遵循 DRY 原則,只在單一位置定義知識。YAGNI 原則提醒開發者避免過度工程,專注於當前需求。繼承雖能促程式式碼重用,但應避免領域物件與實作物件混淆,優先考慮組合來降低耦合。理解並應用這些原則,能有效提升軟體品質。

軟體設計的核心原則:高內聚與低耦合

在軟體開發領域,高內聚(High Cohesion)與低耦合(Low Coupling)是兩個基本且重要的設計原則。高內聚指的是模組內部元素彼此相關性強,功能緊密相關;而低耦合則強調不同模組之間的依賴關係應盡量減少,以提高系統的可維護性和可擴充套件性。

不良耦合的後果

當程式碼(物件或方法)之間的依賴關係過於緊密時,會帶來一些不良後果:

  1. 無法重用程式碼:如果一個函式過於依賴特定的物件,或是需要過多的引數,那麼它就與該物件緊密耦合,這使得在不同的上下文中重用該函式變得非常困難。
  2. 連鎖反應:一方變動會對另一方產生影響,因為它們之間的關聯太密切。
  3. 抽象層次不清晰:當兩個函式緊密相關時,很難將它們視為不同的關注點,在不同的抽象層次上解決問題。

簡單記住良好設計的原則

本文將介紹一些能夠引匯出良好設計理念的原則。這些原則以易於記憶的首字母縮寫形式呈現,作為一種助記規則。當你記住這些單詞時,你將能夠更容易地將它們與良好的實踐聯絡起來,並且在檢視特定程式碼時,能更快地理解其背後的理念。

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)是一個在編寫解決方案時經常需要記住的理念,如果你不想過度設計它。很多開發者認為他們需要預測所有的未來需求,並建立非常複雜的解決方案,但事實往往證明那些預測的需求並未出現,或者以不同的方式出現,導致原來的程式碼難以重構和擴充套件。

內容解密:

  1. 高內聚與低耦合的重要性:強調了在軟體設計中保持模組內部高內聚和模組間低耦合的必要性,以提高軟體的可維護性和可擴充套件性。
  2. DRY/OAOO原則的應用:透過避免程式碼重複來提高可維護性,示例中透過建立score_for_student函式來消除重複計算學生分數的邏輯。
  3. 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 重要?

  1. 避免過度設計:過早地泛化或抽象化可能會導致不必要的複雜性,增加維護成本。
  2. 資源最佳化:專注於當前需求可以更有效地利用開發資源。
  3. 提高適應性:保持程式碼的靈活性,使其更容易適應未來的變化。

YAGNI 實踐範例

假設我們正在開發一個使用者管理系統,目前只需要實作一個簡單的使用者類別。根據 YAGNI 原則,我們不應該預先設計一個複雜的抽象類別或介面,除非當前需求明確要求這麼做。

class User:
    def __init__(self, id_, username):
        self.id_ = id_
        self.username = username

# 當前需求滿足,無需預先抽象化

內容解密:

  1. 這個 User 類別目前只包含 id_username 兩個屬性,滿足當前需求。
  2. 如果未來需要擴充套件更多型別的使用者,可以再考慮抽象化或使用繼承。
  3. 這種做法避免了過早抽象化可能帶來的複雜性。

KIS:保持簡單

KIS 原則提倡在設計和實作軟體時保持簡單性,避免不必要的複雜化。簡單的設計更容易理解、維護和擴充套件。

如何實踐 KIS?

  1. 最小化功能:只實作當前需求所需的功能。
  2. 簡化資料結構:使用最簡單的資料結構來解決問題。
  3. 避免過度工程:除非必要,否則避免使用複雜的設計模式或技術。

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)

內容解密:

  1. ComplicatedNamespace 使用了一個額外的類別方法 init_with_data 來初始化物件,這增加了使用的複雜性。
  2. Namespace 直接在 __init__ 方法中處理初始化邏輯,簡化了介面和實作。
  3. 簡單的設計使程式碼更易讀、易維護。

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:
        # 處理檔案不存在的情況
    

內容解密:

  1. EAFP 直接嘗試開啟檔案,如果檔案不存在,則捕捉 FileNotFoundError 異常進行處理。
  2. LBYL 先檢查檔案是否存在,如果存在則開啟,否則處理檔案不存在的情況。
  3. 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)

內容解密:

  1. 嘗試開啟檔案:程式首先嘗試開啟名為 example.txt 的檔案。
  2. 讀取檔案內容:如果檔案成功開啟,則讀取其內容。
  3. 處理例外:如果檔案不存在,捕捉 FileNotFoundError 例外並記錄錯誤訊息。

EAFP 的好處在於程式碼更直接地表達了主要的操作邏輯,而不是被大量的檢查程式碼所淹沒。這種方式使得程式碼更易讀,也減少了因檢查不周全而導致的錯誤。

繼承機制

繼承是物件導向程式設計中的一個重要概念,允許開發者建立一個新的類別,該類別繼承了父類別的屬性和方法。

何時使用繼承

  1. 程式碼重用:雖然繼承可以幫助重用程式碼,但不應該僅僅為了重用程式碼而使用繼承。正確的做法是設計高內聚、易於組合的類別。
  2. 特殊化:當需要建立一個新的類別,它是現有類別的特殊版本時,繼承是一個好的選擇。
from http.server import BaseHTTPRequestHandler, SimpleHTTPRequestHandler

# BaseHTTPRequestHandler 是一個基礎類別,定義了處理 HTTP 請求的基本介面
# SimpleHTTPRequestHandler 繼承自 BaseHTTPRequestHandler,並新增或改變了一些行為

內容解密:

  1. BaseHTTPRequestHandler:這是一個基礎類別,定義了處理 HTTP 請求的基本介面。
  2. SimpleHTTPRequestHandler:這是一個子類別,繼承自 BaseHTTPRequestHandler,並對其進行了擴充套件或修改。

反模式

  1. 過度繼承:不應該僅僅為了重用程式碼而使用繼承。如果子類別並不需要父類別的大部分方法,這可能是一個設計錯誤。
  2. 父類別定義不明確:如果父類別的責任不明確或承擔了太多的功能,這可能會導致子類別難以正確地繼承或擴充套件其行為。

總之,Python 中的例外處理和繼承機制是強大的工具,可以幫助開發者寫出更穩健、更易於維護的程式碼。正確地使用這些機制,需要深入理解其背後的原理和最佳實踐。

正確使用繼承與組合:避免將實作物件與領域物件混淆

在軟體開發中,正確地使用繼承(Inheritance)與組合(Composition)是設計良好類別結構的關鍵。不當的繼承使用不僅會導致程式碼難以理解和維護,還可能引入不必要的耦合(Coupling)。本文將透過一個保險管理系統的例子,探討如何正確地使用繼承與組合,以避免將實作物件與領域物件混淆。

繼承的誤用:將領域物件變為實作物件

在設計一個用於管理保險政策的系統時,我們需要一個類別來表示交易中的政策(TransactionalPolicy)。最初,我們可能認為透過繼承 collections.UserDict 來實作 TransactionalPolicy 是一個好主意,因為這樣可以輕易地使 TransactionalPolicy 成為一個可 subscript 的物件。

class TransactionalPolicy(collections.UserDict):
    """錯誤使用繼承的例子。"""
    # ...

然而,這種做法有兩個主要問題:

  1. 錯誤的層次結構TransactionalPolicy 繼承自 UserDict 意味著 TransactionalPolicy 是一種特殊的字典(Dictionary)。然而,在領域模型中,TransactionalPolicy 代表的是一種保險交易政策,而不是一種資料結構。

  2. 過度的耦合:由於繼承了 UserDictTransactionalPolicy 自動獲得了許多不必要的字典方法,如 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]

內容解密:

  1. __init__ 方法:初始化 _data 屬性,將傳入的 policy_dataextra_data 合併成一個字典儲存。
  2. change_in_policy 方法:根據客戶 ID 更新對應的政策資料。
  3. __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']

#### 內容解密:

  1. MRO的作用:MRO透過C3線性化演算法,確保了在多重繼承的情況下,方法呼叫的順序是明確且一致的。
  2. Mixin的應用:Mixin類別提供了一種靈活的方式來重用程式碼,透過與其他類別結合,可以實作多樣化的功能。
  3. 設計考量:在使用多重繼承和Mixin時,需要仔細設計類別的繼承關係,以避免過於複雜的繼承結構。