依賴注入是一種設計模式,旨在降低程式碼耦合度,提升應用程式的可維護性和可測試性。本文探討如何結合引導指令碼和手動依賴注入,建構更具彈性的應用程式架構。引導指令碼負責初始化核心物件和注入依賴,而手動注入則確保依賴關係明確且易於管理。文章將進一步說明如何利用 Python 的特性(如閉包、偏函式和類別)實作手動依賴注入,並以訊息匯流排的設計為例,展示如何將其應用於實際專案中。最後,文章還會探討如何在單元測試和整合測試中,透過自定義訊息匯流排和模擬物件,有效地驗證程式碼的正確性,確保應用程式的穩定性和可靠性。
依賴注入與引導指令碼:解耦應用程式的關鍵
在軟體開發中,管理依賴關係是一項重要的任務。適當的依賴注入(Dependency Injection, DI)能夠提高程式碼的可測試性、可維護性和擴充套件性。本文將探討如何使用引導指令碼(Bootstrap Script)和手動依賴注入來最佳化應用程式的架構。
顯式依賴 vs. 隱式依賴
首先,我們需要了解顯式依賴和隱式依賴之間的區別。顯式依賴是指在函式或類別的簽名中明確宣告的依賴,而隱式依賴則是透過其他方式(如全域性變數或模組匯入)引入的依賴。
# 隱式依賴
from email import send_mail
def send_out_of_stock_notification(event: events.OutOfStock):
send_mail('[email protected]', f'Out of stock for {event.sku}')
# 顯式依賴
def send_out_of_stock_notification(event: events.OutOfStock, send_mail: Callable):
send_mail('[email protected]', f'Out of stock for {event.sku}')
內容解密:
- 顯式依賴的優點:使測試更容易編寫和管理,因為可以輕易地替換依賴。
- 符合依賴倒置原則:依賴於抽象而不是具體實作,提高了程式碼的靈活性。
手動依賴注入
手動依賴注入是一種不使用框架,直接在程式碼中管理依賴關係的方法。它可以透過閉包(Closures)、偏函式(Partial Functions)或類別來實作。
使用閉包或偏函式
def allocate(cmd: commands.Allocate, uow: unit_of_work.AbstractUnitOfWork):
# 原始的allocate函式
line = OrderLine(cmd.orderid, cmd.sku, cmd.qty)
with uow:
# 處理邏輯
pass
# 使用閉包
def bootstrap():
uow = unit_of_work.SqlAlchemyUnitOfWork()
allocate_composed = lambda cmd: allocate(cmd, uow)
return allocate_composed
# 使用偏函式
import functools
allocate_composed = functools.partial(allocate, uow=uow)
內容解密:
- 閉包和偏函式的作用:將依賴注入到函式中,使其變成一個可直接呼叫的函式。
- 區別:閉包使用晚繫結(Late Binding),可能導致可變依賴的問題。
使用類別
class AllocateHandler:
def __init__(self, uow: unit_of_work.AbstractUnitOfWork):
self.uow = uow
def __call__(self, cmd: commands.Allocate):
line = OrderLine(cmd.orderid, cmd.sku, cmd.qty)
with self.uow:
# 處理邏輯
pass
# 初始化
uow = unit_of_work.SqlAlchemyUnitOfWork()
allocate = AllocateHandler(uow)
內容解密:
- 類別的作用:透過
__init__宣告依賴,並透過__call__使例項可呼叫。 - 適用場景:適合於需要明確依賴關係和複雜初始化邏輯的場景。
引導指令碼
引導指令碼負責初始化應用程式的核心物件,如訊息匯流排(Message Bus),並注入必要的依賴。
def bootstrap(
start_orm: bool = True,
uow: unit_of_work.AbstractUnitOfWork = unit_of_work.SqlAlchemyUnitOfWork(),
send_mail: Callable = email.send,
publish: Callable = redis_eventpublisher.publish,
) -> messagebus.MessageBus:
if start_orm:
orm.start_mappers()
dependencies = {'uow': uow, 'send_mail': send_mail, 'publish': publish}
injected_event_handlers = {
event_type: [
inject_dependencies(handler, dependencies)
for handler in event_handlers
]
for event_type, event_handlers in handlers.EVENT_HANDLERS.items()
}
injected_command_handlers = {
command_type: inject_dependencies(handler, dependencies)
for command_type, handler in handlers.COMMAND_HANDLERS.items()
}
return messagebus.MessageBus(
uow=uow,
event_handlers=injected_event_handlers,
command_handlers=injected_command_handlers,
)
內容解密:
- 引導指令碼的功能:宣告預設依賴、執行初始化工作、注入依賴到處理器。
- 靈活性:允許透過引數覆寫預設依賴,以適應不同環境和測試需求。
依賴注入與訊息匯流排的實作
在現代軟體開發中,依賴注入(Dependency Injection, DI)是一種重要的設計模式,用於提高程式碼的模組化、可測試性和可維護性。本文將探討如何在 Python 專案中實作依賴注入,以及如何將其應用於訊息匯流排(Message Bus)的設計中。
透過檢查函式簽名進行依賴注入
首先,我們介紹如何透過檢查函式簽名來實作依賴注入。這種方法涉及使用 Python 的 inspect 模組來檢查處理函式(handler function)的引數,並將相應的依賴注入到這些函式中。
程式碼範例:依賴注入實作
def inject_dependencies(handler, dependencies):
params = inspect.signature(handler).parameters
deps = {
name: dependency
for name, dependency in dependencies.items()
if name in params
}
return lambda message: handler(message, **deps)
內容解密:
inspect.signature(handler).parameters:檢查handler函式的引數簽名,取得其引數名稱和相關資訊。deps字典生成式:根據dependencies字典和handler函式的引數簽名,生成一個新的字典deps,其中包含需要注入的依賴。lambda message: handler(message, **deps):傳回一個新的匿名函式,該函式呼叫原始的handler函式,並將deps中的依賴以關鍵字引數的形式傳遞給handler。
手動依賴注入
除了使用 inspect 模組進行自動化的依賴注入外,也可以手動建立部分函式(partial function)來實作依賴注入。這種方法雖然更為繁瑣,但在某些情況下可能更為直觀和易於理解。
程式碼範例:手動建立部分函式
injected_event_handlers = {
events.Allocated: [
lambda e: handlers.publish_allocated_event(e, publish),
lambda e: handlers.add_allocation_to_read_model(e, uow),
],
# 其他事件處理函式...
}
injected_command_handlers = {
commands.Allocate: lambda c: handlers.allocate(c, uow),
commands.CreateBatch: lambda c: handlers.add_batch(c, uow),
# 其他命令處理函式...
}
內容解密:
injected_event_handlers和injected_command_handlers字典:手動建立事件和命令的處理函式,並將依賴(如uow和publish)直接注入到這些處理函式中。lambda函式:用於建立部分函式,將特定的依賴注入到處理函式中。
將訊息匯流排設計為可組態的類別
為了使訊息匯流排更加靈活和可組態,我們將其設計為一個類別,並將已經注入依賴的處理函式傳遞給它。
程式碼範例:訊息匯流排類別
class MessageBus:
def __init__(self, uow, event_handlers, command_handlers):
self.uow = uow
self.event_handlers = event_handlers
self.command_handlers = command_handlers
def handle(self, message):
self.queue = [message]
while self.queue:
message = self.queue.pop(0)
if isinstance(message, events.Event):
self.handle_event(message)
elif isinstance(message, commands.Command):
self.handle_command(message)
else:
raise Exception(f'{message} was not an Event or Command')
內容解密:
__init__方法:初始化訊息匯流排例項,接收工作單元(uow)、事件處理函式和命令處理函式作為引數。handle方法:處理傳入的訊息(事件或命令),並根據訊息型別呼叫相應的處理函式。
在應用程式入口點使用載入程式
在應用程式的入口點(如 Flask 路由),我們呼叫載入程式(bootstrap.bootstrap())來取得一個已組態好的訊息匯流排例項。
程式碼範例:Flask 路由呼叫載入程式
bus = bootstrap.bootstrap()
@app.route("/add_batch", methods=['POST'])
def add_batch():
cmd = commands.CreateBatch(
request.json['ref'], request.json['sku'], request.json['qty'], eta,
)
bus.handle(cmd)
return 'OK', 201
內容解密:
bootstrap.bootstrap():呼叫載入程式,取得一個已組態好的訊息匯流排例項。bus.handle(cmd):使用訊息匯流排例項處理傳入的命令。
測試中的自定義訊息匯流排實作
在測試過程中,我們可以利用 bootstrap.bootstrap() 函式並覆寫預設值來取得自定義的訊息匯流排。以下是一個整合測試的範例:
覆寫 Bootstrap 預設值(tests/integration/test_views.py)
@pytest.fixture
def sqlite_bus(sqlite_session_factory):
bus = bootstrap.bootstrap(
start_orm=True,
uow=unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory),
send_mail=lambda *args: None,
publish=lambda *args: None,
)
yield bus
clear_mappers()
def test_allocations_view(sqlite_bus):
sqlite_bus.handle(commands.CreateBatch('sku1batch', 'sku1', 50, None))
sqlite_bus.handle(commands.CreateBatch('sku2batch', 'sku2', 50, date.today()))
# ...
assert views.allocations('order1', sqlite_bus.uow) == [
{'sku': 'sku1', 'batchref': 'sku1batch'},
{'sku': 'sku2', 'batchref': 'sku2batch'},
]
內容解密:
sqlite_busfixture 的建立:使用bootstrap.bootstrap()建立一個自定義的訊息匯流排,其中start_orm=True表示啟動 ORM,uow使用SqlAlchemyUnitOfWork並傳入sqlite_session_factory以建立資料函式庫連線。send_mail和publish的處理:將send_mail和publish設定為lambda *args: None,表示在測試中不實際傳送郵件或發布訊息,以避免副作用。test_allocations_view測試邏輯:透過sqlite_bus.handle()處理命令,建立批次並驗證分配結果。
在單元測試中使用 FakeUnitOfWork
在單元測試中,我們可以使用 FakeUnitOfWork 來簡化測試:
def bootstrap_test_app():
return bootstrap.bootstrap(
start_orm=False,
uow=FakeUnitOfWork(),
send_mail=lambda *args: None,
publish=lambda *args: None,
)
內容解密:
start_orm=False:由於使用FakeUnitOfWork,因此不需要啟動 ORM。FakeUnitOfWork的使用:簡化單元測試中的工作單元實作,避免實際的資料函式庫操作。