ticket_api · 呼叫順序解說
TicketService 先查自己的 cache,沒有才問 FakeRepo
呼叫端只跟 TicketService 說話。TicketService 一律先檢查自己的 cache;只有 cache 沒有這張票時,才呼叫 FakeRepo.find。FakeRepo 從不主動呼叫 TicketService,只回傳結果。
三種情況一句話
- 首次讀取:cache 未命中 → 呼叫
FakeRepo.find(reads加 1)→ 把結果複製進 cache → 回200。 - 再次讀取同一張票:cache 命中 → 直接回
200,FakeRepo完全沒被呼叫。 - 找不到票:
FakeRepo.find回None→get_ticket拋出NotFound→handle_get接住並回(404, {"error": "not found"})。
所有判斷都在 get_ticket 這八行
呼叫端的入口是 handle_get,它把工作交給 get_ticket,再把結果或例外轉成「狀態碼+內容」的 tuple。FakeRepo 是測試用的假資料來源:find 每被呼叫一次,計數器 reads 就加 1,並用 dict.get 查表,查不到時回 None。
9 def find(self, ticket_id): # FakeRepo
10 self.reads += 1
11 return self.rows.get(ticket_id)
⋮
18 def get_ticket(self, ticket_id): # TicketService
19 if ticket_id in self.cache:
20 return self.cache[ticket_id]
21 row = self.repo.find(ticket_id)
22 if row is None:
23 raise NotFound(ticket_id)
24 self.cache[ticket_id] = dict(row)
25 return self.cache[ticket_id]
26
27 def handle_get(self, ticket_id):
28 try:
29 return 200, self.get_ticket(ticket_id)
30 except NotFound:
31 return 404, {"error": "not found"}- 實線:呼叫
- 虛線:回傳
- 方框:該參與者內部的動作
- 「呼叫端」在測試中是
test_service.py;圖中的票號沿用測試資料
首次讀取:TicketService → FakeRepo,一來一回
第一次要 "T1" 時,cache 是空的(建構時設為 {},第 16 行),所以 TicketService 呼叫 FakeRepo.find("T1")。FakeRepo 把 reads 加 1 並回傳那一列資料;TicketService 用 dict(row) 複製一份放進 cache,再回傳 cache 裡的那份。
再次讀取:停在 TicketService,不碰 FakeRepo
第二次要 "T1" 時,第 19 行的 if ticket_id in self.cache 成立,第 20 行直接回傳,第 21 行的 self.repo.find 根本不會執行。因此 FakeRepo.reads 停在 1。這正是測試 test_repeat_hit 要確認的事。
repo.reads == 1。
首次與再次讀取對照
| 面向 | 首次讀取 | 再次讀取 |
|---|---|---|
| cache 檢查(第 19 行) | 未命中 | 命中 |
是否呼叫 FakeRepo.find | 是,呼叫 1 次 | 否 |
reads 之後的值 | 1 | 仍為 1 |
| cache 變化 | 寫入 cache["T1"] = dict(row) | 不變 |
| 回傳 | (200, cache["T1"]) | (200, cache["T1"]),與首次是同一個 dict 物件 |
程式推論,已小型實測兩次回傳的是 cache 裡同一個 dict(第 20、25 行都回 self.cache[ticket_id]),不是各自的複本;而它與 FakeRepo.rows 裡的原始資料是不同物件(第 24 行做了 dict(row))。所以若呼叫端改動拿到的 dict,下一次讀取會看到改動後的內容。我以一段臨時 Python 指令確認:兩次回傳 is 比較為 True,與 rows["T1"] 比較為 False。現有測試沒有涵蓋這一點。
找不到票:例外在 TicketService 內部被轉成 404
找不到票時,FakeRepo 仍然被呼叫,只是 rows.get 回 None。get_ticket 看到 None 就拋出 NotFound,不寫入 cache;外層 handle_get 的 except NotFound 接住它,回傳 (404, {"error": "not found"})。例外不會傳到呼叫端,呼叫端收到的是一般的 tuple。
(404, {"error": "not found"})。
程式推論,已小型實測因為找不到時不寫入 cache,對同一個不存在的票號重複查詢,每次都會再呼叫一次 FakeRepo.find。臨時指令中連查兩次 "X",reads 依序加 1,cache 裡也沒有 "X"。現有測試沒有涵蓋這一點。
程式推論,未測試handle_get 只接 NotFound。若 find 拋出其他例外,它會直接傳到呼叫端,不會變成 404 或其他狀態碼。
證據與限制
- 讀過的來源:README.md、service.py、test_service.py,共三個檔案。
- 測試實際執行:2026-10-05 在
ticket_api目錄以 Python 3.14.7 執行python3 -m unittest -v,test_repeat_hit與test_missing兩項皆 ok(Ran 2 tests,OK)。 - 來源衝突:60 秒過期。README 寫「Cached values expire after 60 seconds」,但
service.py的 cache 只是普通 dict(第 16、24 行),沒有任何時間或過期邏輯。依程式,只要是同一個TicketService實例,再次讀取不論隔多久都會命中 cache。兩者哪個代表預期行為,現有材料無法判斷;測試也沒有測過期。 - 「process-local」的範圍:README 說 cache 屬於行程;依程式,cache 是每個
TicketService實例各自的self.cache,兩個實例不共用。這是從第 16 行推論,未另行測試。 - 範圍:
FakeRepo是記憶體中的假資料來源;材料中沒有真實資料庫或 HTTP 層,200/404只是handle_get回傳 tuple 的第一個元素。
自我檢查(可略過)
先查 handle_get("T9") 得到 404,之後有人把 "T9" 加進 FakeRepo.rows,再查一次會得到什麼?
會得到 200 和那張票。找不到時沒有寫入 cache,所以第二次仍會呼叫 FakeRepo.find,這次查得到。這是依第 22–24 行的推論,現有測試沒有涵蓋。