返回文章列表

Python物件導向SOLID設計原則與裝飾器應用

本文探討了在物件導向軟體開發中 SOLID 設計原則的應用,特別是依賴注入模式如何提升程式碼的彈性和可測試性,並以 Python 程式碼示範如何使用 Pinject 函式庫簡化依賴注入的過程。此外,文章還詳細介紹了 Python

軟體設計 Python

在物件導向程式設計中,SOLID 原則提供了一套指導方針,用於構建更具彈性、可維護和可擴充套件的軟體系統。本文著重探討了依賴注入模式如何解決程式碼耦合的問題,並以 EventStreamer 類別為例,示範如何利用依賴注入和 DataTargetClient 介面提升程式碼的彈性和可測試性。同時,也介紹瞭如何使用 Pinject 函式庫來管理更複雜的依賴關係。此外,本文還深入淺出地講解了 Python 裝飾器的概念和用法,包含函式裝飾器和類別裝飾器,並以實際案例展示如何應用裝飾器來簡化程式碼、提升程式碼的可讀性,例如使用裝飾器實作重試機制、單例模式以及事件物件的序列化。最後,文章還介紹了更進階的引數化裝飾器,讓讀者能夠更靈活地運用裝飾器。

SOLID 設計原則在物件導向軟體開發中的應用

在物件導向軟體開發中,SOLID 設計原則是一套重要的指導方針,幫助開發者建立出更具彈性、可維護性和可擴充套件性的軟體系統。這些原則分別代表了單一職責原則(Single Responsibility Principle)、開放封閉原則(Open/Closed Principle)、里氏替換原則(Liskov Substitution Principle)、介面隔離原則(Interface Segregation Principle)和依賴倒置原則(Dependency Inversion Principle)。

依賴注入(Dependency Injection)

依賴注入是一種設計模式,它允許開發者在不修改類別內部實作的情況下,更改類別的依賴關係。這種模式透過將依賴關係的建立與類別的例項化分離,提供了更大的彈性和可測試性。

案例分析:EventStreamer 類別

假設我們有一個 EventStreamer 類別,負責將事件串流到某個目標系統。最初的實作可能是直接在類別內部建立一個 Syslog 物件作為目標系統:

class EventStreamer:
    def __init__(self):
        self._target = Syslog()

    def stream(self, events: list[Event]) -> None:
        for event in events:
            self._target.send(event.serialise())

這種實作方式存在幾個問題:

  1. 缺乏彈性EventStreamer 類別被繫結到 Syslog 目標系統,無法輕易地更換為其他目標系統。
  2. 測試困難:由於 Syslog 物件是在類別內部建立的,因此在測試 EventStreamer 類別時,很難模擬或替換 Syslog 物件。

解決方案:使用依賴注入

為了提高 EventStreamer 類別的彈性和可測試性,我們可以使用依賴注入模式,將目標系統的建立與 EventStreamer 類別的例項化分離:

class EventStreamer:
    def __init__(self, target: DataTargetClient):
        self._target = target

    def stream(self, events: list[Event]) -> None:
        for event in events:
            self._target.send(event.serialise())

在這個實作中,EventStreamer 類別不再直接依賴於 Syslog 類別,而是依賴於 DataTargetClient 介面。這使得我們可以輕易地更換目標系統,只需提供一個實作 DataTargetClient 介面的物件即可。

使用 Pinject 進行依賴注入

當物件之間的依賴關係變得複雜時,我們可以使用像 Pinject 這樣的函式庫來簡化依賴注入的過程。Pinject 允許我們以宣告式的方式定義物件之間的依賴關係,並自動建立物件例項。

class _EventStreamerBindingSpec(pinject.BindingSpec):
    def provide_target(self):
        return Syslog()

object_graph = pinject.new_object_graph(binding_specs=[_EventStreamerBindingSpec()])
event_streamer = object_graph.provide(EventStreamer)

內容解密:

  1. DataTargetClient 介面的定義DataTargetClient 是一個抽象基礎類別,定義了 send() 方法的介面。這使得任何實作該介面的類別都可以被用作 EventStreamer 的目標系統。
  2. EventStreamer 類別的彈性:透過使用依賴注入,EventStreamer 類別可以輕易地與不同的目標系統整合,只需提供一個實作 DataTargetClient 介面的物件即可。
  3. 測試便利性:使用依賴注入後,我們可以輕易地為 EventStreamer 類別編寫單元測試,透過提供模擬或測試替身(test double)來取代實際的目標系統。
  4. pinject 的使用pinject 提供了一種宣告式的方式來定義物件之間的依賴關係,使得建立複雜的物件圖譜變得更加簡單。

軟體設計的挑戰與SOLID原則的實踐

軟體開發是一項極具挑戰性的任務。程式碼的邏輯複雜,其執行時的行為難以預測,加上需求和環境的不斷變化,以及多種可能出錯的情況,使得軟體開發變得更加困難。此外,構建軟體有多種不同的方法、技術、正規化或工具可供選擇,這些方法可以協同工作,以特定方式解決特定問題。然而,並非所有這些方法都能在時間的推移中證明是正確的,需求的變化或演變也可能使原有的設計變得不合時宜。等到發現設計不正確時,往往為時已晚,因為設計已經僵化、缺乏彈性,難以更改或重構為正確的解決方案。

這意味著,如果我們一開始就設計錯誤,將來會付出巨大的代價。那麼,如何才能實作一個好的設計,讓它最終帶來回報?答案是,我們無法確定。我們面對的是未來,而未來是不確定的——沒有辦法確定我們的設計是否正確,我們的軟體是否能在未來幾年中保持靈活和適應性。正因為如此,我們必須堅持原則。

SOLID原則的重要性

SOLID原則在這裡發揮了重要作用。它們不是萬能規則(畢竟,在軟體工程中沒有銀彈),但它們提供了良好的指導方針,這些方針已經在過去的專案中被證明是有效的,可以使我們的軟體更有可能成功。我們的目標不是從一開始就把所有需求都弄正確,而是實作一個可擴充套件、足夠靈活的設計,以便根據需要進行調整。

在本章中,我們探討了SOLID原則,以理解乾淨的設計。在接下來的章節中,我們將繼續探索語言的細節,並在某些情況下了解如何將這些工具和功能與這些原則結合使用。

使用裝飾器改善程式碼

在下一章《使用裝飾器改善我們的程式碼》中,我們將探討如何利用裝飾器來改善我們的程式碼。與本章更多地關注軟體工程的抽象概念不同,《使用裝飾器改善我們的程式碼》將更多地關注Python本身,但我們將使用剛剛學到的原則。

裝飾器的目標

本章的主要目標包括:

  • 瞭解Python中裝飾器的工作原理
  • 學習如何實作適用於函式和類別的裝飾器
  • 有效地實作裝飾器,避免常見的實作錯誤
  • 分析如何使用裝飾器避免程式碼重複(DRY原則)
  • 研究裝飾器如何促進關注點分離
  • 分析優秀裝飾器的範例
  • 評估裝飾器何時是正確的選擇

Python中的裝飾器是什麼?

裝飾器在很久以前就被引入Python(PEP-318),作為一種簡化函式和方法定義的方式,當它們需要在原始定義後進行修改時。

首先,我們需要了解,在Python中,函式就像其他大多數物件一樣,是正規物件。這意味著你可以將它們指定給變數,透過引數傳遞,或者甚至對它們應用其他函式。常見的做法是編寫一個小函式,然後對其進行一些轉換,生成該函式的新修改版本(類別似於數學中的函式組合)。

引入裝飾器的最初動機之一是,像classmethodstaticmethod這樣的函式用於轉換方法的原始定義,它們需要額外的行來修改原始定義。

一般來說,每當我們需要對函式應用轉換時,我們都必須使用修飾函式呼叫它,然後將其重新指定給函式原始定義的名字。

def original(...):
    ...

original = modifier(original)

請注意我們如何更改函式並將其重新指定給相同的名稱。這種做法令人困惑,容易出錯(想象一下有人忘記重新指定函式,或者重新指定但不是在函式定義之後立即執行,而是在很遠的地方執行),並且很麻煩。因此,語言中增加了一些語法支援。

裝飾器的實作與優勢

上述範例可以重寫如下:

@modifier
def original(...):
    ...

內容解密:

  1. 裝飾器的語法:在Python中,使用@符號來表示裝飾器。上面的範例展示瞭如何使用@modifier來裝飾original函式,等同於在定義original後再執行original = modifier(original)
  2. 函式作為物件:Python中的函式是第一級物件,可以被指定、傳遞和修改,這使得裝飾器的實作成為可能。
  3. 避免重複和提高可維護性:透過使用裝飾器,我們可以避免重複的程式碼,並提高程式碼的可維護性。例如,使用裝飾器來記錄函式的執行時間或處理異常,可以避免在每個函式中重複相同的程式碼。
  4. 關注點分離:裝飾器幫助實作關注點分離,即將主要邏輯與輔助邏輯(如日誌記錄、身份驗證等)分開,使程式碼更加清晰和易於管理。

總之,裝飾器是Python中一種強大的工具,能夠幫助我們編寫更乾淨、更可維護的程式碼。透過有效地使用裝飾器,我們可以提高程式碼的可讀性和可擴充套件性,並減少重複勞動。

Python裝飾器:函式與類別的強化工具

在Python中,裝飾器(decorator)是一種強大的工具,能夠在不修改原始函式或類別的情況下,擴充套件其功能。裝飾器本質上是一個函式,它接受另一個函式或類別作為引數,並傳回一個新的函式或類別。

函式裝飾器

函式裝飾器主要用於對函式進行增強,例如引數驗證、重試機制、快取結果等。下面是一個簡單的重試機制裝飾器範例:

# decorator_function_1.py
from functools import wraps
import logging

class ControlledException(Exception):
    """程式領域中的一般例外。"""
    pass

def retry(operation):
    @wraps(operation)
    def wrapped(*args, **kwargs):
        last_raised = None
        RETRIES_LIMIT = 3
        for _ in range(RETRIES_LIMIT):
            try:
                return operation(*args, **kwargs)
            except ControlledException as e:
                logging.info("重試 %s", operation.__qualname__)
                last_raised = e
        raise last_raised
    return wrapped

@retry
def run_operation(task):
    """執行特定任務,模擬某些失敗情況。"""
    return task.run()

內容解密:

  1. @wraps(operation):這個裝飾器用於保留原始函式的後設資料(如名稱、檔案字串等),使得除錯更加方便。
  2. RETRIES_LIMIT = 3:定義重試次數的限制。
  3. for _ in range(RETRIES_LIMIT):使用_變數表示迴圈中的變數不會被使用,這是Python中的慣用表示法。
  4. try-except區塊:捕捉ControlledException例外,如果發生,則進行重試;如果重試次數達到上限,則重新丟擲最後一次的例外。

類別裝飾器

類別裝飾器與函式裝飾器類別似,但它作用於類別。類別裝飾器可用於強制多個類別遵循某個介面或標準,或對類別進行特定的轉換。

# decorator_class_1.py
def singleton(cls):
    instances = {}
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class Logger:
    def __init__(self, name):
        self.name = name

    def log(self, message):
        print(f"{self.name}: {message}")

內容解密:

  1. instances = {}:用於儲存已建立的例項。
  2. wrapper函式:檢查該類別是否已在instances中存在例項,如果不存在,則建立一個新的例項並儲存;否則,直接傳回已有的例項。
  3. @singleton:將Logger類別轉換為單例模式,確保無論建立多少次Logger,都只會有一個例項。

探討Python裝飾器:原理、實作與進階應用

在Python程式設計中,裝飾器(Decorator)是一種強大的工具,能夠在不修改原始函式或類別的情況下,動態地為其新增新的功能或行為。本文將探討裝飾器的基本原理、實作方法以及進階應用,幫助讀者更全面地理解和掌握這一重要技術。

裝飾器的基本概念與實作

裝飾器本質上是一個可呼叫的物件(通常是函式或類別),它接受另一個函式或類別作為引數,並傳回一個新的函式或類別。在Python中,我們使用@符號來應用裝飾器。

簡單的函式裝飾器範例

首先,讓我們來看看一個簡單的裝飾器範例,該裝飾器用於記錄函式的執行時間:

import time
from functools import wraps

def timing_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"函式 {func.__name__} 執行時間:{end_time - start_time} 秒")
        return result
    return wrapper

@timing_decorator
def example_function():
    time.sleep(1)
    print("範例函式執行完成")

example_function()

內容解密:

  1. timing_decorator定義:這是一個裝飾器函式,接受一個函式func作為引數。
  2. @wraps(func)的作用:保留原始函式func的元資料(如名稱、檔案字串等),使得被裝飾後的函式仍能保持原始函式的資訊。
  3. wrapper函式:計算函式執行的時間,並列印出來,最後傳回原始函式的執行結果。
  4. @timing_decorator應用:將timing_decorator應用於example_function,使得該函式在執行時能夠自動記錄其執行時間。

類別裝飾器與序列化邏輯

接下來,我們將探討如何使用類別裝飾器來實作事件物件的序列化邏輯。序列化是指將物件轉換為可儲存或傳輸的格式(如字典或JSON字串)的過程。

事件序列化裝飾器實作

from dataclasses import dataclass
import datetime

def hide_field(field) -> str:
    return "**redacted**"

def format_time(field_timestamp: datetime.datetime) -> str:
    return field_timestamp.strftime("%Y-%m-%d %H:%M")

def show_original(event_field):
    return event_field

class EventSerializer:
    def __init__(self, serialization_fields: dict) -> None:
        self.serialization_fields = serialization_fields

    def serialize(self, event) -> dict:
        return {
            field: transformation(getattr(event, field))
            for field, transformation in self.serialization_fields.items()
        }

class Serialization:
    def __init__(self, **transformations):
        self.serializer = EventSerializer(transformations)

    def __call__(self, event_class):
        def serialize_method(event_instance):
            return self.serializer.serialize(event_instance)
        event_class.serialize = serialize_method
        return event_class

@Serialization(
    username=str.lower,
    password=hide_field,
    ip=show_original,
    timestamp=format_time,
)
@dataclass
class LoginEvent:
    username: str
    password: str
    ip: str
    timestamp: datetime.datetime

# 使用範例
login_event = LoginEvent("User123", "password123", "192.168.1.1", datetime.datetime.now())
print(login_event.serialize())

內容解密:

  1. EventSerializer類別:負責根據提供的序列化規則,將事件物件轉換為字典格式。
  2. Serialization類別裝飾器:透過接受序列化規則作為引數,動態地為事件類別新增serialize方法。
  3. @Serialization應用:為LoginEvent類別新增序列化邏輯,定義了各欄位的處理方式,如隱藏密碼、格式化時間戳等。
  4. serialize方法:透過裝飾器動態新增到LoginEvent類別,使其能夠將自身例項序列化為字典。

裝飾器的進階應用:引數化裝飾器

為了使裝飾器更加靈活,我們可以實作引數化裝飾器,即允許在應用裝飾器時傳遞引數。

巢狀函式實作引數化裝飾器

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)
        return wrapper
    return decorator_repeat

@repeat(3)
def greet(name):
    print(f"你好,{name}!")

greet("小明")

內容解密:

  1. repeat函式:一個引數化裝飾器工廠,接受重複執行的次數num_times作為引數。
  2. decorator_repeat內部裝飾器:實際的裝飾器,接受函式func作為引數,並傳回wrapper函式。
  3. wrapper函式:重複呼叫原始函式func指定的次數。
  4. @repeat(3)應用:使得greet函式在被呼叫時重複執行三次。