軟體設計原則是確保程式碼品質的關鍵。本文從組合、介面、鬆散耦合到 SOLID 原則,提供全面的程式設計策略。組合和繼承是兩種程式碼複用方式,組合的靈活性更高,更能適應需求變化。介面設計則讓程式碼更具彈性,易於擴充和維護。鬆散耦合降低模組間的依賴性,提升系統穩定性。最後,SOLID 原則提供更進階的程式碼設計指導,包含單一職責、開放封閉、里氏替換、介面隔離和依賴反轉,讓程式碼更具可讀性、可維護性和可擴充性。

組合優於繼承原則的實踐與優勢

在軟體設計中,選擇組合(Composition)而非繼承(Inheritance)是一種重要的設計原則。這種方法提供了多項優勢,包括更高的靈活性、可重用性和易於維護性。

組合的優勢

採用組合優於繼承的設計原則,可以帶來以下幾點好處:

  • 靈活性:組合允許在執行時改變物件的行為,使程式碼更具適應性。
  • 可重用性:較小、較簡單的物件可以在應用程式的不同部分重複使用,促程式式碼的重用。
  • 易於維護:透過組合,可以輕易地替換或更新個別元件,而不會影響整體系統,避免邊界效應。

組合的技術實踐

在 Python 中,組合通常透過物件導向程式設計(OOP)實作,將其他類別的例項包含在一個類別中。這種關係有時被稱為包含類別和被包含類別之間的「擁有」(has-a)關係。Python 不需要明確的型別宣告,使得組合的使用變得特別容易。你可以透過在類別的 __init__ 方法中例項化其他物件,或者將它們作為引數傳遞來實作組合。

使用引擎組合物件的範例

以下是一個使用組合的範例,其中 Car 類別包含了一個 Engine 類別的例項:

class Engine:
    def start(self):
        print("引擎啟動")

class Car:
    def __init__(self):
        self.engine = Engine()  # 組合:Car 擁有 Engine

    def start(self):
        self.engine.start()
        print("汽車啟動")

if __name__ == "__main__":
    my_car = Car()
    my_car.start()

執行上述程式碼將輸出:

引擎啟動
汽車啟動

程式碼解析

  1. 定義 Engine 類別及其 start 方法。
  2. 定義 Car 類別,並在其 __init__ 方法中例項化 Engine,實作組合。
  3. Carstart 方法中呼叫 Enginestart 方法。

內容解密:

  • 在這個範例中,Car 類別透過包含一個 Engine 物件來實作組合。這使得 Car 可以輕易地更換引擎型別,而無需修改 Car 類別本身。
  • self.engine = Engine() 這一行程式碼是組合的關鍵,它建立了 CarEngine 之間的「擁有」關係。
  • 這種設計提高了程式碼的靈活性和可維護性。

導向介面而非實作的程式設計原則

在軟體設計中,過度關注實作細節可能導致程式碼緊密耦合,難以修改。「導向介面而非實作」的原則提供瞭解決方案。

介面的定義與好處

介面定義了一種契約,規定了類別必須實作的方法集合。遵循這一原則,可以使程式碼與特定的實作類別解耦,更容易替換或擴充套件實作,而不影響系統的其他部分。

採用導向介面的程式設計,可以帶來以下好處:

  • 靈活性:可以輕易地在不同的實作之間切換,而無需修改使用它們的程式碼。
  • 可維護性:減少了程式碼對特定實作的依賴,使得更新或替換元件變得更容易。
  • 可測試性:介面使得撰寫單元測試變得更加簡單,因為可以在測試過程中輕易地模擬介面。

介面的技術實踐

在 Python 中,可以透過兩種主要技術來實作介面:抽象基礎類別(ABCs)和協定。

抽象基礎類別(ABCs)

ABCs 由 abc 模組提供,允許定義必須由任何非抽象子類別實作的抽象方法。

from abc import ABC, abstractmethod

class MyInterface(ABC):
    @abstractmethod
    def do_something(self, param: str):
        pass

class MyClass(MyInterface):
    def do_something(self, param: str):
        print(f"執行某些操作,使用引數:'{param}'")

if __name__ == "__main__":
    MyClass().do_something("某個引數")

程式碼解析

  1. 匯入 ABC 類別和 abstractmethod 裝飾器函式。
  2. 定義介面類別 MyInterface,並宣告抽象方法 do_something
  3. 定義具體類別 MyClass,實作介面方法。

內容解密:

  • 在這個範例中,MyInterface 定義了一個介面,規定了 do_something 方法必須被實作。
  • MyClass 實作了 MyInterface,提供了 do_something 方法的具體實作。
  • 這種設計使得程式碼更加靈活,易於擴充套件和測試。

Python中的介面設計:ABCs與Protocols的比較

在Python中,介面設計是軟體開發中的重要概念。介面定義了一種契約或規範,規定了類別應該實作哪些方法。在Python中,有兩種主要的方式來定義介面:抽象基礎類別(Abstract Base Classes, ABCs)和協定(Protocols)。

抽象基礎類別(ABCs)

抽象基礎類別是一種透過繼承來定義介面的方式。使用abc模組,可以定義一個抽象基礎類別,並在其中宣告抽象方法。任何繼承自該抽象基礎類別的具體類別都必須實作這些抽象方法。

使用ABCs的優點

  • 明確的介面定義:透過繼承抽象基礎類別,子類別必須實作特定的方法。
  • 執行時檢查:Python會在執行時檢查子類別是否實作了所有抽象方法。

使用ABCs的缺點

  • 限制性:子類別必須明確地繼承自抽象基礎類別。
  • 不夠靈活:如果一個類別已經繼承自另一個類別,則無法再繼承自抽象基礎類別。

協定(Protocols)

協定是Python 3.8中引入的一種新的介面定義方式,透過typing.Protocol來定義。協定採用結構化鴨子型別(structural duck typing),只要一個物件具有特定的屬性和方法,就可以被視為符合某個協定。

使用Protocols的優點

  • 更靈活:不需要明確地繼承自協定類別,只要實作了特定的方法即可。
  • 編譯時檢查:透過靜態型別檢查工具(如mypy),可以在編譯時檢查型別錯誤。

使用Protocols的缺點

  • 需要靜態型別檢查工具的支援:Python本身是動態型別語言,需要額外的工具來進行靜態型別檢查。

範例:不同型別的日誌記錄器

下面是一個使用ABCs和Protocols來定義日誌記錄器介面的範例。

使用ABCs的日誌記錄器

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message: str):
        pass

class ConsoleLogger(Logger):
    def log(self, message: str):
        print(f"Console: {message}")

class FileLogger(Logger):
    def log(self, message: str):
        with open("log.txt", "a") as f:
            f.write(f"File: {message}\n")

def log_message(logger: Logger, message: str):
    logger.log(message)

if __name__ == "__main__":
    log_message(ConsoleLogger(), "A console log.")
    log_message(FileLogger(), "A file log.")

使用Protocols的日誌記錄器

from typing import Protocol

class Logger(Protocol):
    def log(self, message: str):
        ...

class ConsoleLogger:
    def log(self, message: str):
        print(f"Console: {message}")

class FileLogger:
    def log(self, message: str):
        with open("log.txt", "a") as f:
            f.write(f"File: {message}\n")

def log_message(logger: Logger, message: str):
    logger.log(message)

if __name__ == "__main__":
    log_message(ConsoleLogger(), "A console log.")
    log_message(FileLogger(), "A file log.")

程式碼解密:

這兩個範例都定義了一個日誌記錄器介面,並提供了兩種不同的實作:ConsoleLoggerFileLogger。使用ABCs的版本需要明確地繼承自Logger抽象基礎類別,而使用Protocols的版本則不需要。

log_message函式中,無論是使用ABCs還是Protocols,都可以傳入任何實作了log方法的物件。這種靈活性使得程式碼更容易擴充套件和維護。

圖表示例

  graph LR;
    A[Logger 介面] --> B[ConsoleLogger];
    A --> C[FileLogger];
    B --> D[log_message 函式];
    C --> D;

圖表翻譯: 此圖示展示了 Logger 介面與其兩種實作 ConsoleLoggerFileLogger 之間的關係,以及它們如何被 log_message 函式使用。無論是 ConsoleLogger 還是 FileLogger,都可以被傳入 log_message 函式中,因為它們都實作了 Logger 介面中定義的 log 方法。

遵循鬆散耦合原則

隨著軟體複雜度的增加,其元件之間的關係可能會變得混亂,導致系統難以理解、維護和擴充套件。鬆散耦合原則旨在緩解這一問題。

什麼是鬆散耦合?

鬆散耦合是指最小化程式不同部分之間的依賴關係。在一個鬆散耦合的系統中,元件是獨立的,並透過明確定義的介面進行互動,從而使得對某一部分的修改不會影響其他部分。

鬆散耦合的好處

鬆散耦合提供了多項優勢:

  • 可維護性:由於依賴關係較少,因此更容易更新或替換個別元件。
  • 可擴充套件性:鬆散耦合的系統可以更容易地新增新功能或元件。
  • 可測試性:獨立的元件更容易在隔離狀態下進行測試,從而提高軟體的整體品質。

鬆散耦合的技術

實作鬆散耦合的兩種主要技術是依賴注入和觀察者模式。依賴注入允許元件從外部來源接收其依賴關係,而不是自行建立,從而使其更容易替換或模擬這些依賴關係。觀察者模式則允許物件釋出其狀態的變化,以便其他物件可以相應地做出反應,而無需緊密繫結。

依賴注入範例 - 訊息服務

在Python中,可以透過依賴注入實作鬆散耦合。讓我們看一個簡單的範例,涉及MessageService類別:

class MessageService:
    def __init__(self, sender):
        self.sender = sender

    def send_message(self, message: str):
        self.sender.send(message)

class EmailSender:
    def send(self, message: str):
        print(f"傳送電子郵件:{message}")

class SMSSender:
    def send(self, message: str):
        print(f"傳送簡訊:{message}")

if __name__ == "__main__":
    email_service = MessageService(EmailSender())
    email_service.send_message("透過電子郵件問候")
    sms_service = MessageService(SMSSender())
    sms_service.send_message("透過簡訊問候")

程式碼解析:

此範例中,MessageService類別透過依賴注入與EmailSenderSMSSender實作鬆散耦合。這使得您可以輕鬆地在不同的傳送機制之間切換,而無需修改MessageService類別。

  1. MessageService類別:透過建構函式接收一個sender物件,並使用其send方法傳送訊息。
  2. EmailSenderSMSSender類別:分別實作了傳送電子郵件和簡訊的功能。
  3. 測試:建立了兩個MessageService例項,分別使用EmailSenderSMSSender,並發送了訊息。

輸出結果:

傳送電子郵件:透過電子郵件問候
傳送簡訊:透過簡訊問候

結語

本章節介紹了鬆散耦合原則及其實作方法,包括依賴注入技術。透過範例演示瞭如何在Python中應用這些原則,以提高軟體的可維護性、可擴充套件性和可測試性。

SOLID 原則

在軟體工程領域,原則和最佳實踐是強健、可維護和高效程式碼函式庫的基礎。在前一章中,我們介紹了每位開發者都應遵循的基本原則。

本章節將繼續探討設計原則,重點介紹SOLID原則。SOLID是由Robert C. Martin提出的一套五項設計原則,旨在使軟體更易於理解、更具靈活性和可維護性。

SOLID 原則的主要內容

  • 單一職責原則(SRP)
  • 開放-封閉原則(OCP)
  • 里氏替換原則(LSP)
  • 介面隔離原則(ISP)
  • 依賴倒置原則(DIP)

透過本章節的學習,您將瞭解這五項額外的設計原則,並學習如何在Python中應用它們。

單一職責原則(SRP)

SRP是軟體設計中的一個基本概念。它主張,在定義一個類別以提供功能時,該類別應該只有一個存在的理由,並且應該只負責功能的一個方面。簡而言之,它促進了每個類別應該有一個工作或責任,並且該工作應該被封裝在該類別中。

SOLID 設計原則:開發更優質的軟體系統

在軟體開發的世界中,設計模式與原則扮演著至關重要的角色。其中,SOLID 設計原則是一套被廣泛接受且實用的指導方針,幫助開發者開發出更具維護性、擴充套件性和可讀性的程式碼。本文將探討 SOLID 原則中的單一職責原則(SRP)與開放封閉原則(OCP),並透過例項展示如何將這些原則應用於實際的軟體開發中。

單一職責原則(Single Responsibility Principle, SRP)

SRP 強調一個類別(Class)應該僅有一個引起它變化的原因。換言之,一個類別應該只負責一件事情。這種設計方式能夠提升程式碼的可維護性和可理解性。當每個類別都有明確且單一的目的時,管理和擴充套件程式碼將變得更加容易。

SRP 的實踐範例

假設我們有一個名為 Report 的類別,它負責生成報告並將報告儲存到檔案中。最初的實作可能如下:

class Report:
    def __init__(self, content):
        self.content = content

    def generate(self):
        print(f"Report content: {self.content}")

    def save_to_file(self, filename):
        with open(filename, 'w') as file:
            file.write(self.content)

這個 Report 類別同時負責生成報告和儲存報告到檔案,違反了 SRP。為了遵循 SRP,我們可以將其重構為兩個類別:ReportReportSaver

class Report:
    def __init__(self, content: str):
        self.content: str = content

    def generate(self):
        print(f"Report content: {self.content}")

class ReportSaver:
    def __init__(self, report: Report):
        self.report: Report = report

    def save_to_file(self, filename: str):
        with open(filename, 'w') as file:
            file.write(self.report.content)

if __name__ == "__main__":
    report_content = "This is the content."
    report = Report(report_content)
    report.generate()
    report_saver = ReportSaver(report)
    report_saver.save_to_file("report.txt")

內容解密:

  1. Report 類別:僅負責生成報告內容。

    • __init__ 方法初始化報告內容。
    • generate 方法輸出報告內容。
  2. ReportSaver 類別:負責將報告儲存到檔案。

    • __init__ 方法接收一個 Report 物件。
    • save_to_file 方法將報告內容寫入指定的檔案。

透過這種重構,我們達到了單一職責的目標,使得程式碼更加清晰和易於維護。

開放封閉原則(Open-Closed Principle, OCP)

OCP 指出軟體實體(如類別和模組)應該對擴充套件開放,但對修改封閉。這意味著當需要新增功能時,我們應該透過擴充套件現有的程式碼來實作,而不是修改原有的程式碼。這種做法能夠減少引入新錯誤的風險,並使軟體系統更加靈活和可維護。

OCP 的實踐範例

考慮一個計算不同形狀面積的例子。最初,我們可能有一個 Rectangle 類別和一個 calculate_area 函式:

class Rectangle:
    def __init__(self, width: float, height: float):
        self.width: float = width
        self.height: float = height

def calculate_area(shape) -> float:
    if isinstance(shape, Rectangle):
        return shape.width * shape.height

若要新增對圓形(Circle)的支援,我們需要修改 calculate_area 函式,這違反了 OCP。為了遵循 OCP,我們可以引入一個 Shape 協定(Protocol),定義一個計算面積的方法:

import math
from typing import Protocol

class Shape(Protocol):
    def area(self) -> float:
        ...

class Rectangle:
    def __init__(self, width: float, height: float):
        self.width: float = width
        self.height: float = height

    def area(self) -> float:
        return self.width * self.height

class Circle:
    def __init__(self, radius: float):
        self.radius: float = radius

    def area(self) -> float:
        return math.pi * (self.radius ** 2)

def calculate_area(shape: Shape) -> float:
    return shape.area()

內容解密:

  1. Shape 協定:定義了一個 area 方法,用於計算形狀的面積。
  2. RectangleCircle 類別:分別實作了 Shape 協定,提供了各自的面積計算方法。
  3. calculate_area 函式:接受任何實作了 Shape 協定的物件,並呼叫其 area 方法計算面積。

透過這種設計,當我們需要新增新的形狀時,只需建立新的類別並實作 Shape 協定,而無需修改現有的 calculate_area 函式,從而遵循了 OCP。