正在学习
Claude 上下文文件
app_legacy.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import sqlite3
from typing import List, Optional
app = FastAPI(title="Tasks Legacy Monolith", version="0.1.0")
DB_PATH = "tasks.db"
class TaskIn(BaseModel):
title: str
description: Optional[str] = ""
completed: bool = False
class Task(TaskIn):
id: int
def _ensure_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute(
"CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, description TEXT, completed INTEGER)"
)
conn.commit()
conn.close()
_ensure_db()
@app.get("/health")
def health():
return {"status": "ok", "service": "legacy"}
@app.get("/tasks", response_model=List[Task])
def list_tasks():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("SELECT id, title, description, completed FROM tasks ORDER BY id")
rows = cur.fetchall()
conn.close()
return [
{"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
for r in rows
]
@app.post("/tasks", response_model=Task, status_code=201)
def create_task(payload: TaskIn):
if not payload.title or not payload.title.strip():
raise HTTPException(status_code=400, detail="Title required")
if len(payload.title) > 120:
raise HTTPException(status_code=400, detail="Title too long")
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute(
"INSERT INTO tasks (title, description, completed) VALUES (?, ?, ?)",
(payload.title.strip(), payload.description or "", int(payload.completed)),
)
conn.commit()
task_id = cur.lastrowid
cur.execute("SELECT id, title, description, completed FROM tasks WHERE id = ?", (task_id,))
row = cur.fetchone()
conn.close()
return {"id": row[0], "title": row[1], "description": row[2], "completed": bool(row[3])}
@app.put("/tasks/{task_id}", response_model=Task)
def put_task(task_id: int, payload: TaskIn):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
if not cur.fetchone():
conn.close()
raise HTTPException(status_code=404, detail="not found")
cur.execute(
"UPDATE tasks SET title=?, description=?, completed=? WHERE id=?",
(payload.title.strip(), payload.description or "", int(payload.completed), task_id),
)
conn.commit()
cur.execute("SELECT id, title, description, completed FROM tasks WHERE id = ?", (task_id,))
row = cur.fetchone()
conn.close()
return {"id": row[0], "title": row[1], "description": row[2], "completed": bool(row[3])}
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
conn.commit()
conn.close()
运行它:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install fastapi uvicorn
uvicorn app_legacy:app --reload
现在你已经有了一个基线版本。接下来,重构为分层结构。完全按照所示创建以下文件和文件夹。
app/
init.py
app.py
database.py
models.py
repositories.py
services.py
routes.py
tests/
test_tasks.py
requirements.txt
requirements.txt
fastapi==0.114.0
uvicorn==0.30.5
app/database.py
import sqlite3
from typing import Iterator
DB_PATH = "tasks.db"
def init_db() -> None:
with sqlite3.connect(DB_PATH) as conn:
cur = conn.cursor()
cur.execute(
"CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, description TEXT, completed INTEGER)"
)
conn.commit()
def get_conn() -> Iterator[sqlite3.Connection]:
return sqlite3.connect(DB_PATH)
app/models.py
from pydantic import BaseModel, Field
from typing import Optional
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=120)
description: Optional[str] = ""
completed: bool = False
class Task(TaskCreate):
id: int
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
app/repositories.py
import sqlite3
from typing import List, Optional, Dict, Any
class TaskRepository:
def __init__(self, conn: sqlite3.Connection):
self.conn = conn
def list(self) -> List[Dict[str, Any]]:
cur = self.conn.cursor()
cur.execute("SELECT id, title, description, completed FROM tasks ORDER BY id")
return [
{"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
for r in cur.fetchall()
]
def get(self, task_id: int) -> Optional[Dict[str, Any]]:
cur = self.conn.cursor()
cur.execute("SELECT id, title, description, completed FROM tasks WHERE id=?", (task_id,))
r = cur.fetchone()
if not r:
return None
return {"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
def create(self, title: str, description: str, completed: bool) -> Dict[str, Any]:
cur = self.conn.cursor()
cur.execute(
"INSERT INTO tasks (title, description, completed) VALUES (?, ?, ?)",
(title, description, int(completed)),
)
self.conn.commit()
return self.get(cur.lastrowid)
def put(self, task_id: int, title: str, description: str, completed: bool) -> Optional[Dict[str, Any]]:
cur = self.conn.cursor()
cur.execute("UPDATE tasks SET title=?, description=?, completed=? WHERE id=?",
(title, description, int(completed), task_id))
self.conn.commit()
return self.get(task_id)
def delete(self, task_id: int) -> None:
cur = self.conn.cursor()
cur.execute("DELETE FROM tasks WHERE id=?", (task_id,))
self.conn.commit()
app/services.py
from typing import Dict, Any, Optional
from .repositories import TaskRepository
from .models import TaskCreate, TaskUpdate
class TaskService:
def __init__(self, repo: TaskRepository):
self.repo = repo
def list_tasks(self):
return self.repo.list()
def create_task(self, payload: TaskCreate):
title = payload.title.strip()
desc = (payload.description or "").strip()
return self.repo.create(title, desc, payload.completed)
def get_task(self, task_id: int) -> Optional[Dict[str, Any]]:
return self.repo.get(task_id)
def put_task(self, task_id: int, payload: TaskCreate):
title = payload.title.strip()
desc = (payload.description or "").strip()
return self.repo.put(task_id, title, desc, payload.completed)
def patch_task(self, task_id: int, payload: TaskUpdate):
current = self.repo.get(task_id)
if not current:
return None
title = current["title"] if payload.title is None else payload.title.strip()
desc = current["description"] if payload.description is None else (payload.description or "").strip()
completed = current["completed"] if payload.completed is None else bool(payload.completed)
return self.repo.put(task_id, title, desc, completed)
def delete_task(self, task_id: int):
self.repo.delete(task_id)
app/routes.py
from fastapi import APIRouter, HTTPException
from typing import List
from .models import Task, TaskCreate, TaskUpdate
from .services import TaskService
router = APIRouter()
def mount_routes(service: TaskService) -> APIRouter:
@router.get("/health")
def health():
return {"status": "ok", "service": "refactored"}
@router.get("/tasks", response_model=List[Task])
def list_tasks():
return service.list_tasks()
@router.post("/tasks", response_model=Task, status_code=201)
def create_task(payload: TaskCreate):
return service.create_task(payload)
@router.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int):
task = service.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="not found")
return task
@router.put("/tasks/{task_id}", response_model=Task)
def put_task(task_id: int, payload: TaskCreate):
if not service.get_task(task_id):
raise HTTPException(status_code=404, detail="not found")
return service.put_task(task_id, payload)
@router.patch("/tasks/{task_id}", response_model=Task)
def patch_task(task_id: int, payload: TaskUpdate):
updated = service.patch_task(task_id, payload)
if not updated:
raise HTTPException(status_code=404, detail="not found")
return updated
@router.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
if not service.get_task(task_id):
raise HTTPException(status_code=404, detail="not found")
service.delete_task(task_id)
return router
app/app.py
from fastapi import FastAPI
from .database import init_db, get_conn
from .repositories import TaskRepository
from .services import TaskService
from .routes import mount_routes
def create_app() -> FastAPI:
init_db()
app = FastAPI(title="Tasks Refactored", version="1.0.0")
# 简单的组合根
conn = get_conn()
repo = TaskRepository(conn)
service = TaskService(repo)
app.include_router(mount_routes(service))
return app
app = create_app()
tests/test_tasks.py
from fastapi.testclient import TestClient
from app.app import app
client = TestClient(app)
def test_health():
r = client.get("/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_create_list_get_put_patch_delete():
# 创建
r = client.post("/tasks", json={"title": "Refactor", "description": "split layers", "completed": False})
assert r.status_code == 201
tid = r.json()["id"]
# 列表
r = client.get("/tasks")
assert r.status_code == 200
assert any(t["id"] == tid for t in r.json())
# 获取
r = client.get(f"/tasks/{tid}")
assert r.status_code == 200
assert r.json()["title"] == "Refactor"
# 更新
r = client.put(f"/tasks/{tid}", json={"title": "Refactor All", "description": "", "completed": True})
assert r.status_code == 200
assert r.json()["completed"] is True
# 部分更新
r = client.patch(f"/tasks/{tid}", json={"description": "done"})
assert r.status_code == 200
assert r.json()["description"] == "done"
# 删除
r = client.delete(f"/tasks/{tid}")
assert r.status_code == 204
# 删除后返回 404
r = client.get(f"/tasks/{tid}")
assert r.status_code == 404
运行重构后的服务和测试:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt pytest
uvicorn app.app:app --reload
# 在另一个终端中:
pytest -q
你在隔离关注点的同时保留了原有行为。路由层简洁,服务层表达业务规则和聚合逻辑,仓储层封装 SQL。这使得测试更快,也让未来的变更更加安全。
对比表:重构前 vs.
之后
| 方面 | 遗留单体 | 重构后结构 | 优势 |
|---|---|---|---|
| 数据库访问 | 路由中内联 sqlite 3 调用 | TaskRepository 隐藏 SQL | 更易于更换存储并进行测试 |
| 业务规则 | 分散在各个端点中 | TaskService 集中处理逻辑 | 验证一致且可复用 |
| 路由 | 臃肿的端点 | routes.py 中的精简处理器 | HTTP 层更易读 |
| 初始化 | 全局状态和辅助函数 | create_app( ) 组合 | 启动行为确定 |
| 可测试性 | 难以隔离逻辑 | pytest 命中稳定边界 | 增量变更更安全 |
你将一个可用但脆弱的单体应用,转变为一个易于理解和扩展的、规模较小的分层应用。这次重构改变了结构,而非行为,并引入了接缝——一个仓储和一个服务——在这些位置 Claude Code 可以帮助你自信地进行迭代。从这里开始,你可以添加缓存、将 SQLite 切换为 Postgres,或者接入后台任务,而无需改动路由代码。在下一节中,你将把同样的模式应用于跨多个模块同步编辑,让 Claude 在一次引导式的操作中同时更新路由、服务和测试,同时保持接口契约不变。
第十一章 – Claude 工作流中的安全、风险与治理
练习题
在旧版 FastAPI 应用程序中,使用哪个装饰器来定义健康检查端点?
A. @app.post("/health")
B. @app.put("/health")
C. @app.get("/health")
D. @app.delete("/health")
遗留应用程序中的哪个类定义了创建新任务的结构,包括其标题、描述和完成状态?
A. Task
B. BaseModel
C. TaskIn
D. TaskUpdate
旧版 FastAPI 应用程序中的端点返回的有效 HTTP 状态码有哪些?
A. 200 OK
B. 201 Created
C. 204 No Content
D. 404 Not Found
E. 500 Internal Server Error
该遗留应用程序使用 SQLite 作为其数据库,数据库文件名为 'tasks.db'。
在遗留应用程序中,负责确保数据库表存在的函数被命名为 ___。
解释遗留应用程序中 Task 模型的用途。
使用 uvicorn 运行旧版 FastAPI 应用程序的命令是什么?
A. uvicorn app_legacy:app --port 8000
B. uvicorn app_legacy:app --reload
C. uvicorn app:app_legacy --reload
D. uvicorn run app_legacy:app
遗留应用程序的 create_task 端点验证任务标题不能为空且不超过 120 个字符。
在重构后的分层结构中,用于初始化数据库的函数被命名为 ___。
描述 TaskRepository 类在重构后的分层结构中的作用。
在重构后的分层结构中,哪个类负责处理与任务相关的业务逻辑?
A. TaskRepository
B. TaskService
C. TaskRoute
D. TaskModel
根据提供的知识点,以下哪些是 FastAPI 应用程序中错误处理的最佳实践?
A. 使用 print 语句进行调试
B. 使用 FastAPI 的 HTTPException 处理错误
C. 忽略错误,让应用程序崩溃
D. 使用 try-except 块但不引发 HTTPException
E. 在 HTTPException 中提供有意义的错误消息
在重构后的分层结构中,TaskRepository 类应直接与数据库交互,以执行对任务的 CRUD 操作。
在 FastAPI 中,要验证任务标题的长度,应在 TaskCreate 模型中使用 ___ 属性。
解释在重构后的分层结构中隔离数据访问的重要性。
登录后解锁笔记、知识点解析、AI 问答
立即登录