房屋租賃搜尋引擎:Clean Architecture 的 Python 實踐
在「Rent-o-Matic」專案中,我們將建構一個簡潔的房屋租賃搜尋引擎。每個房間物件包含以下屬性:唯一識別碼、面積(平方公尺)、租金(歐元/天)、經緯度。本篇文章將重點探討如何運用 Clean Architecture 的原則,以 Python 逐步實作這個系統。
專案建置與測試
首先,複製專案儲存函式庫並切換到 second-edition
分支。完整的解決方案位於 second-edition-top
分支,其中包含了我將提到的所有程式碼標籤。我強烈建議您跟著一起編寫程式碼,並僅在需要時參考我的標籤來找出錯誤。
git clone https://github.com/pycabook/rentomatic
cd rentomatic
git checkout --track origin/second-edition
建立虛擬環境並安裝必要的套件:
pip install -r requirements/dev.txt
現在,您可以執行測試套件:
pytest -svv
您應該會看到類別似以下的輸出:
=========================== test session starts =========================== platform linux -- Python XXXX, pytest-XXXX, py-XXXX, pluggy-XXXX -- cabook/venv3/bin/python3 cachedir: .cache rootdir: cabook/code/calc, inifile: pytest.ini plugins: cov-XXXX collected 0 items ========================== no tests ran in 0.02s ==========================
之後,您可能需要檢視程式碼覆寫率檢查的輸出,可以使用以下指令啟動它:
pytest -svv --cov=rentomatic --cov-report=term-missing
在本章中,我不會明確說明何時執行測試套件,因為我認為它是標準工作流程的一部分。每次編寫測試時,都應該執行測試套件並檢查是否出現錯誤。我提供的解決方案程式碼應該能讓測試套件透過。當然,您也可以在複製我的解決方案之前嘗試自行實作程式碼。
您可能會注意到,我將專案組態為使用 black
,並設定了非正統的 75 字元行長。我選擇這個數字是為了在書中以視覺上舒適的方式呈現程式碼,避免程式碼換行導致難以閱讀。
領域模型
讓我們從定義簡單的 Room
模型開始。Clean Architecture 中的模型通常比較輕量,至少比常見 Web 框架中的模型更輕量。
遵循 TDD 方法,我首先編寫測試程式碼。這個測試確保模型可以使用正確的值初始化:
# tests/domain/test_room.py
import uuid
from rentomatic.domain.room import Room
def test_room_model_init():
code = uuid.uuid4()
room = Room(
code,
size=200,
price=10,
longitude=-0.09998975,
latitude=51.75436293,
)
assert room.code == code
assert room.size == 200
assert room.price == 10
assert room.longitude == -0.09998975
assert room.latitude == 51.75436293
記得在您建立的 tests/
的每個子目錄中建立一個空的 __init__.py
檔案。
內容解密:
這段程式碼定義了一個測試案例 test_room_model_init
,用於驗證 Room
模型的初始化。它首先使用 uuid.uuid4()
產生一個唯一的識別碼,然後使用這個識別碼和其他屬性(大小、價格、經緯度)建立一個 Room
物件。最後,它使用斷言 (assert
) 檢查 Room
物件的屬性是否與初始化時提供的值相符。這個測試案例確保了 Room
模型的建構函式正確地設定了物件的屬性。
graph LR A[測試案例: test_room_model_init] --> B{建立 Room 物件}; B --> C[斷言: 檢查屬性];
上圖展示了測試案例的流程。首先建立 Room
物件,然後斷言檢查其屬性。這個簡單的流程確保了 Room
模型的正確性。
在後續的文章中,我將會探討 Clean Architecture 的其他層級,並逐步完善這個房屋租賃搜尋引擎。敬請期待!
import uuid
import dataclasses
@dataclasses.dataclass
class Room:
code: uuid.UUID
size: int
price: int
longitude: float
latitude: float
@classmethod
def from_dict(cls, d):
return cls(**d)
def to_dict(self):
return dataclasses.asdict(self)
import json
class RoomJsonEncoder(json.JSONEncoder):
def default(self, o):
try:
to_serialize = {
"code": str(o.code),
"size": o.size,
"price": o.price,
"latitude": o.latitude,
"longitude": o.longitude,
}
return to_serialize
except AttributeError:
return super().default(o)
import pytest
import uuid
from unittest import mock
from rentomatic.domain.room import Room
from rentomatic.use_cases.room_list import room_list_use_case
@pytest.fixture
def domain_rooms():
room_1 = Room(
code=uuid.uuid4(),
size=215,
price=39,
longitude=-0.09998975,
latitude=51.75436293,
)
room_2 = Room(
code=uuid.uuid4(),
size=405,
price=66,
longitude=0.18228006,
latitude=51.74640997,
)
room_3 = Room(
code=uuid.uuid4(),
size=56,
price=60,
longitude=0.27891577,
latitude=51.45994069,
)
room_4 = Room(
code=uuid.uuid4(),
size=93,
price=48,
longitude=0.33894476,
latitude=51.39916678,
)
return [room_1, room_2, room_3, room_4]
def test_room_list_without_parameters(domain_rooms):
repo = mock.Mock()
repo.list.return_value = domain_rooms
result = room_list_use_case(repo)
repo.list.assert_called_with()
assert result == domain_rooms
Room 類別的設計與應用
在 rentomatic/domain/room.py
中,我們定義了 Room
類別,用於表示房間的資訊。這個類別使用了 dataclasses
,簡潔地定義了房間的屬性:code
、size
、price
、longitude
和 latitude
。
內容解密:
dataclasses
提供了一種簡潔的方式來建立資料類別,自動生成了一些常用的方法,例如 __init__
、__repr__
和 __eq__
。
從字典建立 Room 物件
我們還定義了 from_dict
方法,方便從字典資料建立 Room
物件。這在處理外部資料輸入時非常有用,例如從 API 接收 JSON 資料。
內容解密:
from_dict
方法使用了 classmethod
裝飾器,允許我們直接使用類別名稱呼叫該方法。**d
將字典 d
的鍵值對作為引數傳遞給 Room
的建構函式。
將 Room 物件轉換為字典
to_dict
方法則將 Room
物件轉換回字典格式,方便資料序列化,例如轉換為 JSON 格式。
內容解密:
dataclasses.asdict(self)
將 Room
物件轉換為字典。
Room 物件的比較
dataclasses
自動生成的 __eq__
方法允許我們直接比較兩個 Room
物件是否相等。
內容解密:
__eq__
方法比較兩個 Room
物件的所有屬性是否相等。
Room 物件的 JSON 序列化
在 rentomatic/serializers/room.py
中,我們定義了 RoomJsonEncoder
類別,用於將 Room
物件序列化為 JSON 格式。
內容解密:
RoomJsonEncoder
繼承自 json.JSONEncoder
,覆寫了 default
方法,以便處理 Room
物件的序列化。在 default
方法中,我們將 Room
物件的屬性轉換為可序列化的格式,例如將 UUID
物件轉換為字串。
Room 列表的 Use Case 測試
在 tests/use_cases/test_room_list.py
中,我們測試了 room_list_use_case
函式,該函式從儲存函式庫中取得所有房間的列表。
內容解密:
我們使用 mock.Mock()
模擬了儲存函式庫,並定義了 list
方法的傳回值。然後,我們呼叫 room_list_use_case
函式,並斷言其傳回值與預期值相等。
Room 類別的結構
classDiagram class Room { +UUID code +int size +int price +float longitude +float latitude +from_dict(d: dict) Room +to_dict() dict }
這個圖表展示了 Room
類別的結構,包括其屬性和方法。
這篇文章討論瞭如何設計和實作 Room
類別,以及如何將其序列化為 JSON 格式。我們還測試了 room_list_use_case
函式,該函式從儲存函式庫中取得所有房間的列表。
呼叫儲存函式庫的 list 方法是一個 use case 應該執行的輸出查詢動作,根據單元測試規則,我們不應該測試輸出查詢。然而,我們應該測試系統如何執行輸出查詢,也就是執行查詢所使用的引數。將 use case 的實作放在檔案 rentomatic/use_cases/room_list.py
中:
# rentomatic/use_cases/room_list.py
def room_list_use_case(repo):
return repo.list()
這個解決方案看起來可能過於簡單,讓我們來討論一下。首先,這個 use case 只是儲存函式庫特定函式的包裝器,它不包含任何錯誤檢查,這是我們尚未考慮的事情。在下一篇文章中,我們將討論請求和回應,屆時 use case 將會變得稍微複雜一些。
您可能會注意到的下一件事是我使用了一個簡單的函式。在這個技術的第一版中,我使用了一個類別來表示 use case,感謝幾位讀者的提醒,我開始質疑我的選擇,所以我想簡要地討論一下您可以選擇的方案。
use case 代表業務邏輯,一個流程,這意味著您可以在程式語言中最簡單的實作是一個函式:一些接收輸入引數並傳回輸出資料的程式碼。然而,類別是另一個選項,因為本質上它是一個變數和函式的集合。因此,就像許多其他情況一樣,問題是您應該使用函式還是類別,我的答案是取決於您正在實作的演算法的複雜程度。
您的業務邏輯可能很複雜,需要與幾個外部系統連線,每個系統都有特定的初始化方式,而在這個簡單的案例中,我只是傳入了儲存函式庫。因此,原則上,如果您需要為演算法提供更多結構,我認為使用類別來表示 use case 並沒有錯,但要注意不要在更簡單的解決方案(函式)可以完成相同工作時使用它們,這是我在之前版本的程式碼中犯的錯誤。記住,程式碼必須維護,所以程式碼越簡單越好。
儲存系統
在開發 use case 期間,我們假設它會接收一個包含資料並公開 list 函式的物件。這個物件通常被稱為「儲存函式庫」,是 use case 的資訊來源。它與 Git 儲存函式庫無關,所以要注意不要混淆這兩個術語。
儲存函式庫位於乾淨架構的第四層,即外部系統。內部元素透過介面存取這一層中的元素,在 Python 中,這僅僅意味著公開一組給定的方法(在這種情況下只有 list)。值得注意的是,乾淨架構中儲存函式庫提供的抽象級別高於框架中的 ORM 或 SQLAlchemy 之類別的工具提供的抽象級別。儲存函式庫僅提供應用程式所需的端點,其介面是根據應用程式實作的特定業務問題量身定製的。
為了用具體技術闡明這一點,SQLAlchemy 是一個抽象存取 SQL 資料函式庫的絕佳工具,因此儲存函式庫的內部實作可以使用它來存取 PostgreSQL 資料函式庫,例如。但是該層的外部 API 並不是 SQLAlchemy 提供的 API。API 是一組簡化的函式,use case 呼叫這些函式來取得資料,內部實作可以使用各種解決方案來實作相同的目標,從原始 SQL 查詢到透過 RabbitMQ 網路的複雜遠端呼叫系統。
儲存函式庫的一個非常重要的特性是它可以傳回網域模型,這與框架 ORM 通常的做法是一致的。第三層中的元素可以存取內部層中定義的所有元素,這意味著網域模型和 use case 可以直接從儲存函式庫中呼叫和使用。
為了這個簡單的例子,我們不會佈署和使用真正的資料函式庫系統。考慮到我們所說的,我們可以自由地使用最適合我們需求的系統來實作儲存函式庫,在這種情況下,我想保持一切簡單。因此,我們將建立一個非常簡單的記憶體儲存系統,其中載入了一些預定義資料。
首先要做的是編寫一些測試來記錄儲存函式庫的公共 API。包含測試的檔案是 tests/repository/test_memrepo.py
。
# tests/repository/test_memrepo.py
import pytest
from rentomatic.domain.room import Room
from rentomatic.repository.memrepo import MemRepo
@pytest.fixture
def room_dicts():
return [
{
"code": "f853578c-fc0f-4e65-81b8-566c5dffa35a",
"size": 215,
"price": 39,
"longitude": -0.09998975,
"latitude": 51.75436293,
},
{
"code": "fe2c3195-aeff-487a-a08f-e0bdc0ec6e9a",
"size": 405,
"price": 66,
"longitude": 0.18228006,
"latitude": 51.74640997,
},
{
"code": "913694c6-435a-4366-ba0d-da5334a611b2",
"size": 56,
"price": 60,
"longitude": 0.27891577,
"latitude": 51.45994069,
},
{
"code": "eed76e77-55c1-41ce-985d-ca49bf6c0585",
"size": 93,
"price": 48,
"longitude": 0.33894476,
"latitude": 51.39916678,
},
]
def test_repository_list_without_parameters(room_dicts):
repo = MemRepo(room_dicts)
rooms = [Room.from_dict(i) for i in room_dicts]
assert repo.list() == rooms
在這種情況下,我們需要一個測試來檢查 list 方法的行為。透過測試的實作位於檔案 rentomatic/repository/memrepo.py
中:
# rentomatic/repository/memrepo.py
from rentomatic.domain.room import Room
class MemRepo:
def __init__(self, data):
self.data = data
def list(self):
return [Room.from_dict(i) for i in self.data]
您可以很容易地想像這個類別是真實資料函式庫或任何其他儲存型別的包裝器。雖然程式碼可能會變得更複雜,但其基本結構將保持不變,只有一個公共方法 list。我將在後面的文章中探討資料函式庫儲存函式庫。
命令列介面
到目前為止,我們建立了網域模型、序列化器、use case 和儲存函式庫,但我們仍然缺少一個將所有東西粘合在一起的系統。這個系統必須從使用者那裡取得呼叫引數,使用儲存函式庫初始化 use case,執行從儲存函式庫中取得網域模型的 use case,並將它們傳回給使用者。
現在讓我們看看我們剛剛建立的架構如何與像 CLI 這樣的外部系統互動。乾淨架構的優勢在於外部系統是可插拔的,這意味著我們可以推遲決定我們要使用的系統的細節。在這種情況下,我們想為使用者提供一個介面來查詢系統並取得儲存系統中包含的房間列表,最簡單的選擇是命令列工具。
稍後我們將建立一個 REST 端點,並透過 Web 伺服器公開它,到時候就會清楚為什麼我們建立的架構如此強大。
目前,在包含 setup.cfg
的目錄中建立一個檔案 cli.py
。這是一個簡單的 Python 指令碼,不需要任何特定選項即可執行,因為它只是查詢儲存函式庫中包含的所有網域模型。檔案的內容如下:
# cli.py
#!/usr/bin/env python
from rentomatic.repository.memrepo import MemRepo
from rentomatic.use_cases.room_list import room_list_use_case
repo = MemRepo([])
result = room_list_use_case(repo)
print(result)
您可以使用 python cli.py
執行這個檔案,或者,如果您願意,可以執行 chmod +x cli.py
(使其可執行),然後直接使用 ./cli.py
執行它。預期結果是一個空列表:
$ ./cli.py []
這是正確的,因為檔案 cli.py
中的 MemRepo
類別是用一個空列表初始化的。我們使用的簡單記憶體儲存沒有永續性,所以每次我們建立它時,我們都必須在其中載入一些資料。這樣做是為了保持儲存層的簡單性,但請記住,如果儲存是一個適當的資料函式庫,這部分程式碼將連線到它,但不需要…
程式碼說明:
上述 cli.py
程式碼示範瞭如何使用 MemRepo
和 room_list_use_case
。它首先初始化一個空的 MemRepo
,然後呼叫 room_list_use_case
來取得房間列表。由於儲存函式庫是空的,因此結果也是一個空列表。這段程式碼的主要目的是展示 use case 和儲存函式庫如何一起工作。
流程圖
graph LR A[CLI] --> B(room_list_use_case) B --> C{MemRepo} C --> D[房間列表] D --> A
上圖展示了 CLI、room_list_use_case
和 MemRepo
之間的互動流程。CLI 呼叫 room_list_use_case
,room_list_use_case
再呼叫 MemRepo
的 list
方法取得房間列表,最後將結果傳回給 CLI。
房源列表功能的 HTTP 端點實作
在這個部分,玄貓將會詳細說明如何為房源列表功能建立一個 HTTP 端點。這個端點將會是一個由 Web 伺服器公開的 URL,它會執行特定的邏輯並以標準格式傳回結果。
玄貓會遵循 RESTful API 的設計原則,因此這個端點將會傳回 JSON 格式的資料。然而,RESTful API 並非整潔架構的一部分,這表示您可以根據自己的喜好選擇建模 URL 和傳回資料的格式。
為了公開 HTTP 端點,我們需要一個用 Python 編寫的 Web 伺服器,在這裡,玄貓選擇了 Flask。Flask 是一個輕量級的 Web 伺服器,具有模組化結構,只提供使用者需要的部分。特別的是,我們不會使用任何資料函式庫/ORM,因為我們已經實作了自己的儲存函式庫層。
Flask 設定
首先,讓我們更新需求檔案。requirements/prod.txt
檔案應該包含 Flask,因為這個套件包含一個可以執行本地 Web 伺服器的指令碼,我們可以用它來公開端點:
# requirements/prod.txt Flask
requirements/test.txt
檔案將包含 pytest 擴充套件,以便與 Flask 一起使用:
# requirements/test.txt -r prod.txt pytest tox coverage pytest-cov pytest-flask
更新這些檔案後,記得再次執行 pip install -r requirements/dev.txt
,以便在您的虛擬環境中安裝新的套件。
Flask 應用程式的設定並不複雜,但涉及許多概念,由於這不是 Flask 的教學,玄貓將快速完成這些步驟。不過,玄貓會為每個概念提供 Flask 檔案的連結。
Flask 應用程式可以使用普通的 Python 物件進行設定,因此玄貓建立了 application/config.py
檔案,其中包含以下程式碼:
# application/config.py
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
"""基礎設定"""
pass
class ProductionConfig(Config):
"""生產環境設定"""
pass
class DevelopmentConfig(Config):
"""開發環境設定"""
pass
class TestingConfig(Config):
"""測試環境設定"""
TESTING = True
現在,我們需要一個函式來初始化 Flask 應用程式,設定它,並註冊藍圖。application/app.py
檔案包含以下程式碼,它是一個應用程式工廠:
# application/app.py
from flask import Flask
from application.rest import room
def create_app(config_name):
app = Flask(__name__)
config_module = f"application.config.{config_name.capitalize()}Config"
app.config.from_object(config_module)
app.register_blueprint(room.blueprint)
return app
測試和建立 HTTP 端點
在建立 Web 伺服器的正確設定之前,我們想要建立將要公開的端點。端點最終是在使用者向特定 URL 傳送請求時執行的函式,因此我們仍然可以使用 TDD,因為最終目標是擁有產生特定結果的程式碼。
測試端點時遇到的問題是,當我們點選測試 URL 時,需要 Web 伺服器啟動並執行。Web 伺服器本身是一個外部系統,因此我們不會測試它,但提供端點的程式碼是應用程式的一部分。它實際上是一個閘道器,是一個允許 HTTP 框架存取使用案例的介面。
pytest-flask
擴充套件允許我們執行 Flask,模擬 HTTP 請求,並測試 HTTP 回應。這個擴充套件隱藏了很多自動化,因此乍一看可能會覺得有點「神奇」。當您安裝它時,一些 fixture,例如 client
會自動可用,因此您不需要匯入它們。此外,它會嘗試存取另一個您必須定義的名為 app
的 fixture。
graph LR A[Client] --> B{HTTP Endpoint} B --> C[Use Case] C --> D[Repository] D --> E[Database]
內容解密: 上圖展示了客戶端、HTTP 端點、使用案例、儲存函式庫和資料函式庫之間的互動流程。客戶端傳送請求到 HTTP 端點,端點觸發使用案例,使用案例與儲存函式庫互動取得資料,儲存函式庫最終從資料函式庫讀取資料。
這個架構確保了各個層級的職責分離,提高了程式碼的可測試性和可維護性。例如,我們可以模擬儲存函式庫的行為來測試使用案例,而無需實際連線到資料函式庫。
在後續的文章中,玄貓將會更深入地探討如何使用 Flask 建立更複雜的 Web 應用程式,並整合真實的外部系統,例如資料函式庫。
Flask 框架下開發 RESTful API 端點的測試策略
在建構 Web 應用程式時,確保 API 端點的正確性和穩定性至關重要。本文將探討如何在 Flask 框架下,運用測試驅動開發(TDD)的理念,結合 pytest-flask 以及 mock 技術,建構一個強健的測試環境,以驗證 API 端點的功能。
全域性 Fixture 的設定與應用
為了提高測試程式碼的可重用性,我們可以利用 pytest 的 conftest.py 檔案定義全域性 fixture。以下是一個定義 Flask 應用程式的全域性 fixture 示例:
# tests/conftest.py
import pytest
from application.app import create_app
@pytest.fixture
def app():
app = create_app("testing")
return app
這個 app
fixture 使用 create_app("testing")
工廠函式建立一個 Flask 應用程式例項,並使用測試組態,設定 TESTING
標誌為 True
。如此一來,所有測試案例都能夠使用這個 fixture,避免重複程式碼。
模擬與測試 API 端點
以下是一個測試 API 端點的示例,其中模擬了 use case 的行為:
# tests/rest/test_room.py
import json
from unittest import mock
from rentomatic.domain.room import Room
room_dict = {
"code": "3251a5bd-86be-428d-8ae9-6e51a8048c33",
"size": 200,
"price": 10,
"longitude": -0.09998975,
"latitude": 51.75436293,
}
rooms = [Room.from_dict(room_dict)]
@mock.patch("application.rest.room.room_list_use_case")
def test_get(mock_use_case, client):
mock_use_case.return_value = rooms
http_response = client.get("/rooms")
assert json.loads(http_response.data.decode("UTF-8")) == [room_dict]
mock_use_case.assert_called()
assert http_response.status_code == 200
assert http_response.mimetype == "application/json"
這個測試案例使用 mock.patch
裝飾器模擬了 room_list_use_case
的行為,並設定其傳回值為預先定義的 rooms
列表。接著,使用 client.get("/rooms")
模擬 HTTP GET 請求,並驗證 API 端點的傳回結果、狀態碼以及 MIME 型別。
Flask 端點的建構與實作
以下是一個 Flask 端點的實作示例:
# application/rest/room.py
import json
from flask import Blueprint, Response
from rentomatic.repository.memrepo import MemRepo
from rentomatic.use_cases.room_list import room_list_use_case
from rentomatic.serializers.room import RoomJsonEncoder
blueprint = Blueprint("room", __name__)
rooms = [...] # 預先定義的房間資料
@blueprint.route("/rooms", methods=["GET"])
def room_list():
repo = MemRepo(rooms)
result = room_list_use_case(repo)
return Response(
json.dumps(result, cls=RoomJsonEncoder),
mimetype="application/json",
status=200,
)
這個端點使用 MemRepo
作為儲存函式庫,並呼叫 room_list_use_case
取得房間列表。最後,使用 RoomJsonEncoder
將結果序列化為 JSON 格式,並傳回 HTTP 回應。
WSGI 介面與 Flask 應用程式佈署
為了佈署 Flask 應用程式,我們需要定義一個 WSGI 介面檔案:
# wsgi.py
import os
from application.app import create_app
app = create_app(os.environ["FLASK_CONFIG"])
這個檔案建立了一個 Flask 應用程式例項,並根據環境變數 FLASK_CONFIG
選擇不同的組態。
graph LR A[Client] --> B{API Endpoint} B --> C[Use Case] C --> D[Repository]
內容解密: 上圖展示了 API 端點、Use Case 和 Repository 之間的互動關係。Client 傳送請求到 API Endpoint,Endpoint 呼叫 Use Case 進行業務邏輯處理,Use Case 再透過 Repository 存取資料。
透過以上步驟,我們可以建構一個經過良好測試的 Flask API 端點,並確保其功能的正確性和穩定性。這種測試驅動的開發方法,能夠有效提升程式碼品質,降低錯誤風險,並促程式式碼的可維護性。
from rentomatic.responses import (
ResponseSuccess,
ResponseFailure,
ResponseTypes,
)
class RoomListRequest:
def __init__(self, filters=None):
self.filters = filters
@classmethod
def from_dict(cls, adict):
invalid_req = cls.invalid_request(adict)
if invalid_req:
return invalid_req
return cls(filters=adict.get('filters', None))
@staticmethod
def invalid_request(adict):
if "filters" in adict and not isinstance(adict['filters'], dict):
return ResponseFailure(
ResponseTypes.PARAMETERS_ERROR, "filters must be a dict"
)
return None
def __bool__(self):
return True
def room_list_use_case(repo, request):
if not request:
return ResponseFailure(
ResponseTypes.SYSTEM_ERROR, "request object must be provided"
)
if request.filters is None:
rooms = repo.list()
return ResponseSuccess(rooms)
try:
rooms = repo.list(filters=request.filters)
except Exception as exc: # pylint: disable=broad-except
return ResponseFailure(
ResponseTypes.SYSTEM_ERROR,
f"filtering failed: {exc}"
)
return ResponseSuccess(rooms)
# 內容解密:
# 這段程式碼定義了 `RoomListRequest` 類別和 `room_list_use_case` 函式,用於處理房間列表的請求和過濾。
# `RoomListRequest` 類別:
# - `__init__` 方法初始化 `filters` 屬性,用於儲存過濾條件。
# - `from_dict` 類別方法從字典建立 `RoomListRequest` 物件,並檢查 `filters` 引數是否為字典型別。如果不是,則傳回 `ResponseFailure` 物件,表示引數錯誤。
# - `invalid_request` 靜態方法檢查請求是否有效,若無效則傳回 `ResponseFailure` 物件。
# - `__bool__` 方法確保物件始終被視為 True。
# `room_list_use_case` 函式:
# - 接收儲存函式庫物件 `repo` 和 `RoomListRequest` 物件 `request` 作為引數。
# - 檢查 `request` 是否為空,若為空則傳回 `ResponseFailure` 物件。
# - 如果 `request.filters` 為 `None`,則呼叫 `repo.list()` 取得所有房間,並傳回 `ResponseSuccess` 物件。
# - 如果 `request.filters` 不為 `None`,則呼叫 `repo.list(filters=request.filters)` 取得符合過濾條件的房間。
# - 如果過濾過程中發生錯誤,則傳回 `ResponseFailure` 物件,包含錯誤訊息。
# - 如果過濾成功,則傳回 `ResponseSuccess` 物件,包含過濾後的房間列表。
# 針對此實作的替代方案討論:
# 1. 可以使用 Pydantic 這樣的資料驗證函式庫來驗證請求資料,這可以簡化程式碼並提供更強大的驗證功能。
# 2. 可以使用更具體的異常型別來處理過濾錯誤,而不是使用廣泛的 `Exception` 型別。這可以提供更精確的錯誤訊息,並允許更細粒度的錯誤處理。
# 3. 可以將過濾邏輯移至儲存函式庫層,這可以簡化 use case 的邏輯,並使儲存函式庫更具彈性。
# 使用場景:
# 這個 use case 適用於需要根據特定條件過濾房間列表的場景,例如,使用者可能想要查詢價格低於特定值的房間,或者大小在特定範圍內的房間。
# 潛在陷阱與最佳化空間:
# 1. 使用廣泛的 `Exception` 型別可能會捕捉預期之外的異常,因此應儘可能使用更具體的異常型別。
# 2. 過濾邏輯可能會很複雜,因此應仔細設計和測試,以確保其正確性和效率。
# 3. 應考慮增加日誌記錄,以便在發生錯誤時更容易除錯。