軟體設計原則是確保程式碼品質的關鍵。本文從組合、介面、鬆散耦合到 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()
執行上述程式碼將輸出:
引擎啟動
汽車啟動
程式碼解析
- 定義
Engine
類別及其start
方法。 - 定義
Car
類別,並在其__init__
方法中例項化Engine
,實作組合。 - 在
Car
的start
方法中呼叫Engine
的start
方法。
內容解密:
- 在這個範例中,
Car
類別透過包含一個Engine
物件來實作組合。這使得Car
可以輕易地更換引擎型別,而無需修改Car
類別本身。 self.engine = Engine()
這一行程式碼是組合的關鍵,它建立了Car
和Engine
之間的「擁有」關係。- 這種設計提高了程式碼的靈活性和可維護性。
導向介面而非實作的程式設計原則
在軟體設計中,過度關注實作細節可能導致程式碼緊密耦合,難以修改。「導向介面而非實作」的原則提供瞭解決方案。
介面的定義與好處
介面定義了一種契約,規定了類別必須實作的方法集合。遵循這一原則,可以使程式碼與特定的實作類別解耦,更容易替換或擴充套件實作,而不影響系統的其他部分。
採用導向介面的程式設計,可以帶來以下好處:
- 靈活性:可以輕易地在不同的實作之間切換,而無需修改使用它們的程式碼。
- 可維護性:減少了程式碼對特定實作的依賴,使得更新或替換元件變得更容易。
- 可測試性:介面使得撰寫單元測試變得更加簡單,因為可以在測試過程中輕易地模擬介面。
介面的技術實踐
在 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("某個引數")
程式碼解析
- 匯入
ABC
類別和abstractmethod
裝飾器函式。 - 定義介面類別
MyInterface
,並宣告抽象方法do_something
。 - 定義具體類別
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.")
程式碼解密:
這兩個範例都定義了一個日誌記錄器介面,並提供了兩種不同的實作:ConsoleLogger
和FileLogger
。使用ABCs的版本需要明確地繼承自Logger
抽象基礎類別,而使用Protocols的版本則不需要。
在log_message
函式中,無論是使用ABCs還是Protocols,都可以傳入任何實作了log
方法的物件。這種靈活性使得程式碼更容易擴充套件和維護。
圖表示例
graph LR; A[Logger 介面] --> B[ConsoleLogger]; A --> C[FileLogger]; B --> D[log_message 函式]; C --> D;
圖表翻譯: 此圖示展示了 Logger
介面與其兩種實作 ConsoleLogger
和 FileLogger
之間的關係,以及它們如何被 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
類別透過依賴注入與EmailSender
和SMSSender
實作鬆散耦合。這使得您可以輕鬆地在不同的傳送機制之間切換,而無需修改MessageService
類別。
MessageService
類別:透過建構函式接收一個sender
物件,並使用其send
方法傳送訊息。EmailSender
和SMSSender
類別:分別實作了傳送電子郵件和簡訊的功能。- 測試:建立了兩個
MessageService
例項,分別使用EmailSender
和SMSSender
,並發送了訊息。
輸出結果:
傳送電子郵件:透過電子郵件問候
傳送簡訊:透過簡訊問候
結語
本章節介紹了鬆散耦合原則及其實作方法,包括依賴注入技術。透過範例演示瞭如何在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,我們可以將其重構為兩個類別:Report
和 ReportSaver
。
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")
內容解密:
Report
類別:僅負責生成報告內容。__init__
方法初始化報告內容。generate
方法輸出報告內容。
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()
內容解密:
Shape
協定:定義了一個area
方法,用於計算形狀的面積。Rectangle
和Circle
類別:分別實作了Shape
協定,提供了各自的面積計算方法。calculate_area
函式:接受任何實作了Shape
協定的物件,並呼叫其area
方法計算面積。
透過這種設計,當我們需要新增新的形狀時,只需建立新的類別並實作 Shape
協定,而無需修改現有的 calculate_area
函式,從而遵循了 OCP。