返回文章列表

Python依賴注入與反模式最佳實踐

本文深入探討Python依賴注入的實踐與單元測試,同時解析常見的Python反模式,涵蓋程式碼風格違規、正確性反模式等,提供改進建議和最佳實踐,以提升程式碼品質、可讀性和可維護性。

軟體工程 Python

Python 的依賴注入模式能有效提升程式碼的彈性、可測試性和可維護性,本文的範例以簡訊和 Email 傳送服務示範如何透過介面和裝飾器實作依賴注入,並利用 Stub 類別模擬實際服務進行單元測試,確保程式碼的正確性。同時,程式碼品質的提升仰賴對反模式的理解與避免。不規範的程式碼風格,例如縮排、行長、命名和註解等,都會降低程式碼可讀性,增加維護成本。此外,使用 type() 函式進行型別比較、可變預設引數以及直接存取受保護的類別成員等正確性反模式,都可能導致非預期的行為或錯誤。

依賴注入模式的實踐與單元測試

依賴注入(Dependency Injection, DI)是一種軟體設計模式,旨在提升程式碼的彈性、可測試性和可維護性。本文將透過實際範例,深入探討依賴注入模式的實作方式,並展示如何撰寫單元測試來驗證其正確性。

實作依賴注入模式

首先,我們定義一個 NotificationSender 介面,該介面宣告了 send 方法,用於傳送通知訊息。接著,我們實作兩個具體的傳送器類別:EmailSenderSMSSender,分別用於傳送電子郵件和簡訊。

from abc import ABC, abstractmethod

class NotificationSender(ABC):
    @abstractmethod
    def send(self, message: str):
        pass

class EmailSender(NotificationSender):
    def send(self, message: str):
        print(f"Sending Email: {message}")

class SMSSender(NotificationSender):
    def send(self, message: str):
        print(f"Sending SMS: {message}")

接下來,我們建立一個 NotificationService 類別,該類別依賴於 NotificationSender 介面。為了實作依賴注入,我們定義了一個裝飾器 inject_sender,用於將具體的傳送器類別注入到 NotificationService 中。

def inject_sender(sender_cls):
    def decorator(cls):
        cls.sender = sender_cls()
        return cls
    return decorator

@inject_sender(EmailSender)
class NotificationService:
    def notify(self, message):
        self.sender.send(message)

透過使用 @inject_sender(EmailSender) 裝飾 NotificationService 類別,我們將 EmailSender 類別注入其中,使其能夠傳送電子郵件通知。若要改為傳送簡訊通知,只需將裝飾器改為 @inject_sender(SMSSender) 即可。

if __name__ == "__main__":
    service = NotificationService()
    service.notify("Hello, this is a test notification!")

執行上述程式碼,將輸出 Sending Email: Hello, this is a test notification!。若將裝飾器改為 @inject_sender(SMSSender),則輸出將變為 Sending SMS: Hello, this is a test notification!

撰寫單元測試

為了驗證 NotificationService 的正確性,我們需要撰寫單元測試。由於 NotificationService 依賴於 NotificationSender 介面,我們可以使用 stub 類別來模擬其行為。

import unittest

class EmailSenderStub:
    def __init__(self):
        self.messages_sent = []

    def send(self, message: str):
        self.messages_sent.append(message)

class SMSSenderStub:
    def __init__(self):
        self.messages_sent = []

    def send(self, message: str):
        self.messages_sent.append(message)

class TestNotificationService(unittest.TestCase):
    def test_notify_with_email(self):
        email_stub = EmailSenderStub()
        service = NotificationService()
        service.sender = email_stub
        service.notify("Test Email Message")
        self.assertIn("Test Email Message", email_stub.messages_sent)

    def test_notify_with_sms(self):
        sms_stub = SMSSenderStub()
        
        @inject_sender(SMSSenderStub)
        class CustomNotificationService:
            def notify(self, message):
                self.sender.send(message)

        service = CustomNotificationService()
        service.sender = sms_stub
        service.notify("Test SMS Message")
        self.assertIn("Test SMS Message", sms_stub.messages_sent)

if __name__ == "__main__":
    unittest.main()

在上述測試程式碼中,我們定義了兩個 stub 類別:EmailSenderStubSMSSenderStub,分別用於模擬 EmailSenderSMSSender 的行為。透過將這些 stub 物件注入到 NotificationService 中,我們可以驗證其 notify 方法是否正確地發送了通知訊息。

執行單元測試後,我們可以確認 NotificationService 的行為是否符合預期。

流程圖說明

圖表翻譯:

此圖示展示了依賴注入模式的實作流程。首先,系統會根據需求選擇傳送方式(電子郵件或簡訊)。接著,根據選擇的傳送方式,建立相應的傳送器物件(EmailSenderSMSSender)。然後,將該傳送器物件注入到 NotificationService 中。最後,透過 NotificationService 傳送通知訊息。這個流程清晰地說明瞭依賴注入模式如何提升程式碼的彈性和可測試性。

Python 反模式解析:提升程式碼品質的關鍵實踐

身為軟體開發者,我們經常會遇到一些常見的程式設計模式,這些模式雖然看似無害,但實際上卻可能導致程式碼效率降低、可讀性下降以及維護困難。在本章中,我們將深入探討 Python 中的反模式,幫助開發者寫出更乾淨、更高效的程式碼。

技術需求

本章的技術需求與第一章相同,請參閱相關章節以瞭解詳細要求。

程式碼風格違規:常見問題與解決方案

Python 的官方風格(PEP8)為我們提供了程式碼可讀性和一致性的建議。遵循這些建議有助於開發者之間的協作以及專案的長期維護。以下是一些常見的程式碼風格違規案例及其改進方法。

縮排規範

正確的縮排是 Python 程式碼可讀性的基礎。我們應該使用四個空格作為一個縮排層級,並避免混合使用 Tab 和空格。

最大行長度與空白行

PEP8 建議將程式碼行的最大長度限制在 79 個字元以內,以提高可讀性。此外,空白行的使用也有明確的規定:頂層函式和類別定義之間應該用兩個空白行隔開,而類別中的方法定義之間應該用一個空白行隔開。

# 正確的格式範例
class MyClass:
    def method1(self):
        pass

    def method2(self):
        pass

def top_level_function():
    pass

匯入模組的正確順序

匯入模組的順序和組織方式對於程式碼的可讀性至關重要。正確的做法是將匯入陳述式分成三類別:標準函式庫匯入、相關的第三方函式庫匯入,以及本地應用程式或函式庫的匯入。每類別匯入之間應該用一個空白行隔開。

# 正確的匯入順序
import os
import sys
import numpy as np
from mymodule import myfunction

命名慣例

使用描述性的名稱對於變數、函式、類別和模組至關重要。不同的命名慣例如下:

  • 函式和變數(包括類別屬性和方法):使用小寫字母和底線(lower_case_with_underscores)
  • 類別名稱:使用首字母大寫的駝峰命名法(CapWords)
  • 常數:使用全大寫字母和底線(ALL_CAPS_WITH_UNDERSCORES)
# 正確的命名範例
def calculate_sum(a, b):
    return a + b

class MyClass:
    pass

MAX_VALUE = 100

程式碼中的註解

註解應該是完整的句子,第一個單字大寫,並且清晰簡潔。對於區塊註解和行內註解,有特定的建議:

  • 區塊註解通常適用於其後面的程式碼,並且與該程式碼保持相同的縮排層級。每行區塊註解以 # 和一個空格開始。
  • 行內註解應該謹慎使用。行內註解與陳述式位於同一行,以 # 和一個空格與陳述式分隔。
# 正確的註解範例
# 這是一個區塊註解。
# 它跨越多行。
def foo():
    pass  # 這是一個行內註解。

正確性反模式:常見錯誤與改進

某些程式設計模式可能導致錯誤或非預期的行為。以下是一些常見的正確性反模式及其改進方法。

使用 type() 函式比較型別

在某些情況下,我們需要根據物件的型別進行比較。雖然使用 type() 函式看似簡單直接,但它並不能正確處理子類別的情況。更靈活的方法是使用 isinstance() 函式。

# 正確的型別比較範例
from collections import UserList

class CustomListA(UserList):
    pass

class CustomListB(UserList):
    pass

# 正確的做法
obj = CustomListA()
if isinstance(obj, UserList):
    print("obj 是 UserList 的子類別")

可變預設引數

使用可變物件作為函式的預設引數可能導致非預期的行為。建議避免這種做法。

存取類別的受保護成員

從類別外部直接存取受保護的成員(以下劃線開頭的屬性或方法)是不良的實踐。應該透過類別提供的公開介面來存取這些成員。

Plantuml 程式碼審查流程

圖表翻譯:

此圖示展示了程式碼審查的基本流程。首先,審查過程從「開始程式碼審查」階段開始,接著檢查程式碼是否符合既定的風格規範。如果程式碼風格不符合規範,則需要進行修正;若符合,則進一步檢查是否存在正確性反模式。如果發現正確性反模式,則需要進行修正。最終,無論是修正程式碼風格還是正確性反模式,流程都會進入「完成審查」階段。

結語

遵循 Python 的官方風格(PEP8)並避免常見的正確性反模式,可以顯著提高程式碼的可讀性、可維護性和正確性。開發者應該養成良好的程式設計習慣,利用工具檢查程式碼中的問題,並持續改程式式碼品質。透過這些實踐,我們可以寫出更乾淨、更高效的 Python 程式碼。

Python反模式詳解:程式碼正確性與可維護性最佳實踐

在Python開發過程中,遵循最佳實踐和避免常見的反模式(Anti-Patterns)對於確保程式碼的正確性和可維護性至關重要。本文將深入探討Python中常見的反模式,並提供改進建議和最佳實踐。

不正確的型別檢查

在Python中,檢查物件型別時應避免使用type()函式,而應使用isinstance()函式。後者能夠正確處理子類別的情況,提供更大的靈活性。

# 不推薦的寫法
def compare(obj):
    if type(obj) in (CustomListA, CustomListB):
        print("It's a custom list!")
    else:
        print("It's a something else!")

# 推薦的寫法
def better_compare(obj):
    if isinstance(obj, UserList):
        print("It's a custom list!")
    else:
        print("It's a something else!")

內容解密:

此範例展示了兩種不同的型別檢查方法。compare()函式使用type()檢查物件型別,而better_compare()函式則使用isinstance()。後者能夠正確識別子類別例項,因此更加靈活和可靠。

圖表翻譯:

此圖示展示了使用type()isinstance()進行型別檢查的差異。type()檢查可能遺漏子類別,而isinstance()則能夠正確識別子類別例項,展現了其在型別檢查中的靈活性與可靠性。

可變預設引數的陷阱

在定義函式時,應避免使用可變物件(如列表或字典)作為預設引數值。正確的做法是將預設值設為None,並在函式內部根據需要初始化可變物件。

# 不推薦的寫法
def manipulate(mylist=[]):
    mylist.append("test")
    return mylist

# 推薦的寫法
def better_manipulate(mylist=None):
    if mylist is None:
        mylist = []
    mylist.append("test")
    return mylist

內容解密:

此範例展示了使用可變預設引數可能導致的問題。manipulate()函式由於使用了可變的預設引數[],導致多次呼叫時結果累積。better_manipulate()函式透過將預設值設為None並在內部初始化列表,避免了這一問題。

圖表翻譯:

此序列圖展示了manipulate()better_manipulate()函式在多次呼叫時的行為差異。manipulate()由於可變預設引數導致結果累積,而better_manipulate()則每次都傳回正確的結果,展現了正確處理可變預設引數的重要性。

存取受保護的類別成員

應避免直接存取類別的受保護成員(以下劃線 _ 開頭的屬性)。正確的做法是透過類別提供的公開介面(如方法或屬性)來存取這些成員。

# 不推薦的寫法
class Book:
    def __init__(self, title, author):
        self._title = title
        self._author = author

# 推薦的寫法
class BetterBook:
    def __init__(self, title, author):
        self._title = title
        self._author = author

    def presentation_line(self):
        return f"{self._title} by {self._author}"

內容解密:

此範例展示了直接存取受保護成員與透過公開介面存取的差異。Book類別的例項直接存取了受保護成員,而BetterBook類別則透過presentation_line()方法提供了安全的存取方式。

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Python依賴注入與反模式最佳實踐

package "軟體測試架構" {
    package "測試層級" {
        component [單元測試
Unit Test] as unit
        component [整合測試
Integration Test] as integration
        component [端對端測試
E2E Test] as e2e
    }

    package "測試類型" {
        component [功能測試] as functional
        component [效能測試] as performance
        component [安全測試] as security
    }

    package "工具框架" {
        component [pytest] as pytest
        component [unittest] as unittest
        component [Selenium] as selenium
        component [JMeter] as jmeter
    }
}

unit --> pytest : 撰寫測試
unit --> integration : 組合模組
integration --> e2e : 完整流程
functional --> selenium : UI 自動化
performance --> jmeter : 負載測試

note right of unit
  測試金字塔基礎
  快速回饋
  高覆蓋率
end note

@enduml

圖表翻譯:

此類別圖展示了BookBetterBook類別的設計差異。BetterBook類別透過提供presentation_line()方法封裝了受保護成員的存取,展現了更好的封裝性和可維護性。

綜合最佳實踐建議

  1. 正確的型別檢查:使用isinstance()而非type()進行型別檢查,以正確處理子類別。
  2. 避免可變預設引數:將預設值設為None,並在函式內部根據需要初始化可變物件。
  3. 封裝受保護成員:透過類別提供的公開介面(如方法或屬性)存取受保護成員,避免直接存取。
  4. 使用萬用字元匯入的謹慎:避免使用from module import *,以防止名稱空間汙染。
  5. EAFP原則:採用「請求原諒比請求許可更容易」(EAFP)的程式設計風格,即先嘗試執行程式碼,捕捉並處理異常,而不是事先進行大量的檢查。
  6. 避免過度繼承和緊耦合:優先使用組合而非繼承,減少類別之間的耦合度,提高程式碼的靈活性和可維護性。
  7. 避免使用全域變數:減少使用全域變數,透過函式引數和傳回值傳遞資料,提高程式碼的可讀性和可測試性。

遵循這些最佳實踐和避免常見的反模式,可以顯著提升Python程式碼的正確性、可讀性和可維護性,為開發高品質的Python應用奠定堅實的基礎。