在 Python 軟體開發中,良好的架構設計至關重要,而 SOLID 原則提供了一套有效指引。本文著重探討單一職責原則(SRP)和開閉原則(OCP)的實踐,並輔以程式碼範例說明如何透過重構改善程式碼品質。首先,我們會看到不符合 SRP 的程式碼案例,其問題在於單一類別承擔過多職責,導致程式碼難以維護。接著,我們將透過重構,將不同職責拆分至專屬類別,達成 SRP 的目標。此外,我們還會探討 OCP 的重要性,說明如何運用多型和抽象基底類別,讓程式碼對擴充套件開放,對修改關閉,以提升軟體的彈性和可擴充套件性。

SOLID 原則

在本章中,我們將繼續探索應用於 Python 的乾淨設計概念。特別是,我們將回顧 SOLID 原則以及如何在 Python 中以 Pythonic 方式實作它們。這些原則包含一系列良好的實踐,以實作更高品質的軟體。

單一職責原則

單一職責原則(SRP)指出,一個軟體元件(通常是一個類別)必須只有一個職責。類別具有單一職責的事實意味著它負責做一件具體的事情,因此,我們可以得出結論,它只有一個改變的理由。

只有當領域問題中的某件事情發生變化時,才需要更新類別。如果我們必須因為不同的原因對類別進行修改,這意味著抽象是不正確的,並且該類別具有太多的職責。這可能表明至少缺少一個抽象:需要建立更多的物件來處理當前類別過載的額外職責。

正如第 2 章《Pythonic 程式碼》中所介紹的,這個設計原則幫助我們構建更具凝聚力的抽象——物件只做一件事,並且只做好一件事,遵循 Unix 哲學。我們在所有情況下都希望避免的是具有多個職責的物件(通常稱為上帝物件,因為它們知道太多,或者比它們應該知道的要多)。這些物件將不同的(大多數無關的)行為分組在一起,從而使它們更難以維護。

同樣,類別越小越好。

SRP 與軟體設計中的凝聚力概念密切相關,我們在第 3 章《良好程式碼的一般特徵》中討論了軟體中的關注點分離。我們努力實作的是,類別的屬性和屬性大多被其方法使用。當這種情況發生時,我們知道它們是相關的概念,因此將它們歸在同一個抽象下是有意義的。

在某種程度上,這個想法與關係資料函式庫設計中的正規化概念有些類別似。當我們檢測到屬性分割槽時…

內容解密:

這段話主要闡述了單一職責原則的核心思想和其在軟體設計中的重要性。SRP 要求一個類別或模組應該只負責一項任務或功能,這樣可以提高程式碼的可維護性和可擴充套件性。這裡強調了小而專注的類別的重要性,並與 Unix 哲學相互呼應。同時,也提到了與凝聚力和關注點分離等軟體設計原則的關聯。

圖表示例

此圖示闡述了單一職責原則與軟體元件內聚力之間的關係,以及它如何影響軟體的可維護性和可擴充套件性。

內容解密:

此圖表呈現了遵循單一職責原則對軟體設計的正面影響,以及違反該原則可能導致的問題。它透過視覺化的方式說明瞭保持軟體元件專注於單一任務的重要性。

本章繼續探討 SOLID 原則的其他部分,包括開放/封閉原則、里氏替換原則、介面隔離原則和依賴倒置原則。每個原則都對構建可維護、可擴充套件和穩健的軟體系統提供了重要的指導。

單一職責原則(SRP)與開放/關閉原則(OCP)的實踐與軟體設計

在軟體開發領域中,設計原則對於建立可維護、可擴充套件且穩定的系統至關重要。其中,單一職責原則(Single Responsibility Principle, SRP)與開放/關閉原則(Open/Closed Principle, OCP)是兩大核心原則,它們有助於開發者設計出更具彈性與可維護性的軟體系統。

單一職責原則(SRP)

SRP指出,一個類別(或模組)應該僅有一個引起其變更的原因。換言之,一個類別應該只有一個職責。這意味著,如果一個類別有多個職責,那麼它就有多個變更的原因,這將使得該類別變得脆弱且難以維護。

不符合SRP的設計案例

考慮一個名為SystemMonitor的類別,它負責從某個來源讀取事件資訊、解析這些事件並將它們傳送到外部代理。這個類別的多個方法對應於不同的職責,例如load_activityidentify_eventsstream_events。這些職責是正交的,也就是說,每一個都可以獨立於其他職責進行修改。

# srp_1.py
class SystemMonitor:
    def load_activity(self):
        """從來源取得事件以進行處理"""
        
    def identify_events(self):
        """將來源的原始資料解析為事件(領域物件)"""
        
    def stream_events(self):
        """將解析後的事件傳送到外部代理"""

這個設計的問題在於,它將多個不同的職責混合在一個類別中。每當任何一個職責需要變更時,都需要修改這個類別。這使得SystemMonitor類別變得僵化、脆弱且難以維護。

符合SRP的設計改進

為瞭解決上述問題,我們可以將每個職責分配給不同的類別。例如,可以建立ActivityLoaderEventIdentifierEventStreamer三個類別,每個類別負責一個特定的職責。

# srp_2.py
class ActivityLoader:
    def load_activity(self):
        """載入活動資料"""
        
class EventIdentifier:
    def identify_events(self, activity_data):
        """識別事件"""
        
class EventStreamer:
    def stream_events(self, events):
        """傳送事件"""

這種設計使得每個類別都具有單一職責,從而提高了程式碼的可維護性和可重用性。如果需要變更事件載入的邏輯,只需修改ActivityLoader類別,而無需觸及其他類別。

開放/關閉原則(OCP)

OCP指出,一個模組(或類別)應該對擴充套件開放,但對修改關閉。這意味著,我們應該能夠在不修改現有程式碼的情況下擴充套件其功能。

OCP的實踐意義

在設計類別時,我們應該封裝實作細節,以便於維護。這樣做可以讓我們的程式碼對擴充套件開放,同時對修改關閉。換句話說,我們希望我們的程式碼能夠適應新的需求或領域問題的變化,而無需修改現有的程式碼。

內容解密:

  1. SRP的重要性:透過將不同的職責分配給不同的類別,可以提高程式碼的可維護性和可重用性。
  2. OCP的實踐:透過封裝實作細節並使程式碼對擴充套件開放,可以減少因需求變化而導致的程式碼修改。
  3. 軟體設計的演進:軟體設計是一個演進的過程,不太可能一開始就完美。因此,遵循SRP和OCP等原則,可以幫助我們設計出更具彈性的軟體系統。

開閉原則(Open/Closed Principle, OCP)在軟體設計中的應用

開閉原則是軟體設計中的一個基本原則,指出軟體實體(類別、模組、函式等)應該對擴充套件開放,對修改關閉。這意味著當需求變更時,我們應該能夠透過新增程式碼來擴充套件系統的功能,而不是修改現有的程式碼。

不遵循OCP的維護性問題

讓我們考慮一個例子,系統中有一個元件負責識別事件。該元件根據從其他系統收集的資料來判斷事件的型別。最初的設計如下所示:

# openclosed_1.py
from dataclasses import dataclass

@dataclass
class Event:
    raw_data: dict

class UnknownEvent(Event):
    """無法從資料中識別的事件型別。"""

class LoginEvent(Event):
    """代表使用者登入系統的事件。"""

class LogoutEvent(Event):
    """代表使用者登出系統的事件。"""

class SystemMonitor:
    """識別系統中發生的事件。"""

    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        if (
            self.event_data["before"]["session"] == 0
            and self.event_data["after"]["session"] == 1
        ):
            return LoginEvent(self.event_data)
        elif (
            self.event_data["before"]["session"] == 1
            and self.event_data["after"]["session"] == 0
        ):
            return LogoutEvent(self.event_data)
        return UnknownEvent(self.event_data)

內容解密:

  • Event類別是所有事件的基礎類別,包含原始資料。
  • UnknownEventLoginEventLogoutEvent是具體的事件型別,繼承自Event
  • SystemMonitor類別負責根據提供的資料識別事件型別。
  • identify_event方法根據資料的特定條件傳回相應的事件物件。

這個設計存在幾個問題:

  1. 事件型別的判斷邏輯集中在SystemMonitor類別的identify_event方法中。
  2. 當需要新增事件型別時,需要修改identify_event方法,這違反了開閉原則。

重構事件系統以遵循OCP

為瞭解決上述問題,我們需要對設計進行重構,使其遵循開閉原則。關鍵在於將事件型別的判斷邏輯分散到各個事件類別中,而不是集中在SystemMonitor類別中。

圖示:遵循OCP的設計

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Python 軟體架構 SOLID 原則實踐

package "物件導向程式設計" {
    package "核心概念" {
        component [類別 Class] as class
        component [物件 Object] as object
        component [屬性 Attribute] as attr
        component [方法 Method] as method
    }

    package "三大特性" {
        component [封裝
Encapsulation] as encap
        component [繼承
Inheritance] as inherit
        component [多型
Polymorphism] as poly
    }

    package "設計原則" {
        component [SOLID] as solid
        component [DRY] as dry
        component [KISS] as kiss
    }
}

class --> object : 實例化
object --> attr : 資料
object --> method : 行為
class --> encap : 隱藏內部
class --> inherit : 擴展功能
inherit --> poly : 覆寫方法
solid --> dry : 設計模式

note right of solid
  S: 單一職責
  O: 開放封閉
  L: 里氏替換
  I: 介面隔離
  D: 依賴反轉
end note

@enduml

程式碼實作

# openclosed_refactored.py
from dataclasses import dataclass

@dataclass
class Event:
    raw_data: dict

    def matches(self, data: dict) -> bool:
        """檢查事件是否與提供的資料相符。"""
        raise NotImplementedError

class UnknownEvent(Event):
    def matches(self, data: dict) -> bool:
        return True  # 預設匹配,用於未知事件

class LoginEvent(Event):
    def matches(self, data: dict) -> bool:
        return data["before"]["session"] == 0 and data["after"]["session"] == 1

class LogoutEvent(Event):
    def matches(self, data: dict) -> bool:
        return data["before"]["session"] == 1 and data["after"]["session"] == 0

class SystemMonitor:
    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        for event_cls in (LoginEvent, LogoutEvent):
            if event_cls(self.event_data).matches(self.event_data):
                return event_cls(self.event_data)
        return UnknownEvent(self.event_data)

內容解密:

  • 每個事件類別現在都實作了matches方法,用於判斷事件是否與提供的資料相符。
  • SystemMonitor類別的identify_event方法遍歷所有已知的事件型別,並使用matches方法來找出匹配的事件。
  • 這種設計使得新增事件型別變得容易,只需新增新的事件類別而無需修改現有的程式碼,從而遵循了開閉原則。

開閉原則(Open-Closed Principle)在軟體設計中的應用

開閉原則是軟體設計中的一個基本原則,旨在確保軟體系統的可維護性和可擴充套件性。其核心思想是:軟體實體(類別、模組、函式等)應該對擴充套件開放,對修改關閉。

透過多型實作開閉原則

在軟體設計中,多型是一種強大的工具,可以幫助我們實作開閉原則。透過定義抽象基底類別或介面,我們可以實作多型,使得軟體系統更加靈活和可擴充套件。

class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @staticmethod
    def meets_condition(event_data: dict) -> bool:
        return False

class LoginEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 0
            and event_data["after"]["session"] == 1
        )

class LogoutEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 1
            and event_data["after"]["session"] == 0
        )

class SystemMonitor:
    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        for event_cls in Event.__subclasses__():
            try:
                if event_cls.meets_condition(self.event_data):
                    return event_cls(self.event_data)
            except KeyError:
                continue
        return UnknownEvent(self.event_data)

內容解密:

  • Event 類別是抽象基底類別,定義了 meets_condition 方法,讓子類別實作具體的條件判斷邏輯。
  • LoginEventLogoutEventEvent 的子類別,分別實作了登入和登出事件的條件判斷邏輯。
  • SystemMonitor 類別負責識別事件型別,透過 Event.__subclasses__() 方法取得所有 Event 的子類別,並檢查是否符合事件條件。

擴充套件事件系統

當新的事件型別出現時,我們只需要新增一個新的類別,繼承自 Event 並實作其 meets_condition 方法,而不需要修改現有的程式碼。

class TransactionEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return event_data["after"].get("transaction") is not None

內容解密:

  • TransactionEvent 是新增的事件型別,用於表示交易事件。
  • 透過實作 meets_condition 方法,判斷事件資料中是否包含交易資訊。

開閉原則的實質

開閉原則的實質是,當領域問題出現新變化時,我們只需要新增程式碼,而不需要修改現有的程式碼。這樣可以減少軟體系統的維護成本,提高可擴充套件性。

與里氏替換原則的關聯

開閉原則與里氏替換原則(LSP)密切相關。LSP 規定了物件型別必須滿足的一系列屬性,以保持其設計的可靠性。開閉原則則是透過多型的有效使用,實作軟體系統的可擴充套件性和可維護性。