中间那个东西是什么来着?
——Otto West,《一条叫旺达的鱼》
预览
本章会展开介绍 Service 层——也就是那个中间的东西。漏水的屋顶可能会花掉很多钱。软件中的“泄漏”没那么明显,但可能会消耗大量时间和精力。你该如何组织自己的应用程序,使各层之间不会泄漏?尤其是,中间的 Service 层中应该放什么,又不应该放什么?
定义一个 Service
Service 层是网站的心脏,是它存在的理由。它接收来自多个来源的请求,访问作为网站 DNA 的数据,并返回响应。
常见的 service 模式包括以下几类操作的组合:
创建 / 检索 / 修改,部分或完整修改 / 删除
一个东西 / 多个东西
在 RESTful 路由层,名词是资源。在本书中,我们的资源最初会包括神秘生物,也就是想象中的生物,以及人,也就是神秘生物探索者。
稍后,还可以定义类似下面这些相关资源:
地点
事件,例如探险、目击记录
布局
下面是当前的文件和目录布局:
main.py
web
├── __init__.py
├── creature.py
├── explorer.py
service
├── __init__.py
├── creature.py
├── explorer.py
data
├── __init__.py
├── creature.py
├── explorer.py
model
├── __init__.py
├── creature.py
├── explorer.py
fake
├── __init__.py
├── creature.py
├── explorer.py
└── test
在本章中,你会折腾 service 目录中的文件。
保护
分层的一个好处是,你不必担心所有事情。Service 层只关心进入数据和从数据出来的内容。正如你将在第 11 章中看到的,更高的一层,在本书中是 Web 层,可以处理认证和授权这些麻烦事。创建、修改和删除函数不应该完全开放,甚至 get 函数最终也可能需要一些限制。
函数
让我们从 creature.py 开始。此时,explorer.py 的需求几乎一样,我们可以借用几乎所有内容。写一个同时处理二者的单个 service 文件非常诱人,但几乎不可避免的是,在某个时候我们会需要以不同方式处理它们。
同样在这个阶段,service 文件几乎就是一个透传层。这属于那种一开始多加一点结构,之后会获得回报的情况。就像你在第 8 章中为 web/creature.py 和 web/explorer.py 所做的那样,你会为二者都定义 service 模块,并暂时把它们连接到对应的假数据模块,见示例 9-1 和 9-2。
示例 9-1 一个初始的 service/creature.py 文件
from models.creature import Creature
import fake.creature as data
def get_all() -> list[Creature]:
return data.get_all()
def get_one(name: str) -> Creature | None:
return data.get(id)
def create(creature: Creature) -> Creature:
return data.create(creature)
def replace(id, creature: Creature) -> Creature:
return data.replace(id, creature)
def modify(id, creature: Creature) -> Creature:
return data.modify(id, creature)
def delete(id, creature: Creature) -> bool:
return data.delete(id)
示例 9-2 一个初始的 service/explorer.py 文件
from models.explorer import Explorer
import fake.explorer as data
def get_all() -> list[Explorer]:
return data.get_all()
def get_one(name: str) -> Explorer | None:
return data.get(name)
def create(explorer: Explorer) -> Explorer:
return data.create(explorer)
def replace(id, explorer: Explorer) -> Explorer:
return data.replace(id, explorer)
def modify(id, explorer: Explorer) -> Explorer:
return data.modify(id, explorer)
def delete(id, explorer: Explorer) -> bool:
return data.delete(id)
提示
get_one() 函数返回值的语法,也就是 Creature | None,至少需要 Python 3.9。对于更早版本,你需要使用 Optional:
from typing import Optional
...
def get_one(name: str) -> Optional[Creature]:
...
测试!
现在代码库已经开始稍微充实起来,是引入自动化测试的好时机。上一章中的 Web 测试全部都是手动测试。所以,让我们创建一些目录:
test
顶层目录,与 web、service、data 和 model 并列。
unit
执行单个函数,但不跨越层边界。
web
Web 层单元测试。
service
Service 层单元测试。
data
Data 层单元测试。
full
也被称为端到端测试或契约测试,这些测试会一次性跨越所有层。它们针对 Web 层中的 API 端点。
这些目录带有 test_ 前缀或 _test 后缀,以供 pytest 使用。你会从示例 9-4 开始看到这一点,该示例会运行示例 9-3 中的测试。
在测试之前,需要做出几个 API 设计选择。如果 get_one() 函数没有找到匹配的 Creature 或 Explorer,应该返回什么?你可以像示例 9-2 那样返回 None。或者,你也可以抛出异常。Python 的内置异常类型中,没有哪一个直接处理缺失值:
TypeError 可能最接近,因为 None 和 Creature 是不同类型。
ValueError 更适用于给定类型下的错误值,不过我想你也可以说,把一个缺失的字符串 id 传给 get_one(id) 符合这种情况。
如果你真的想要,也可以定义自己的 MissingError。
无论你选择哪种方法,其影响都会一路冒泡到最顶层。
现在,我们先选择 None 方案,而不是异常。毕竟,none 就是这个意思。示例 9-3 是一个测试。
示例 9-3 Service 测试 test/unit/service/test_creature.py
from model.creature import Creature
from service import creature as code
sample = Creature(name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman",
)
def test_create():
resp = code.create(sample)
assert resp == sample
def test_get_exists():
resp = code.get_one("yeti")
assert resp == sample
def test_get_missing():
resp = code.get_one("boxturtle")
assert data is None
在示例 9-4 中运行测试。
示例 9-4 运行 service 测试
$ pytest -v test/unit/service/test_creature.py
test_creature.py::test_create PASSED [ 16%]
test_creature.py::test_get_exists PASSED [ 50%]
test_creature.py::test_get_missing PASSED [ 66%]
======================== 3 passed in 0.06s =========================
注意
在第 10 章中,当找不到某个生物时,get_one() 将不再返回 None,因此示例 9-4 中的 test_get_missing() 测试会失败。但到时候会修复它。
其他 Service 层内容
现在我们处在栈的中间——也就是那个真正定义网站目的的部分。而到目前为止,我们只是用它把 Web 请求转发给下一章中的 Data 层。
到目前为止,本书一直以迭代方式开发网站,为未来工作构建一个最小基础。随着你对自己拥有什么、能做什么,以及用户可能想要什么了解得越来越多,你可以开始分支探索并进行实验。有些想法可能只对更大型网站有价值,但下面是一些技术性的网站辅助想法:
日志
指标
监控
追踪
本节会逐一讨论这些内容。我们会在“故障排除”中重新访问这些选项,看看它们能否帮助诊断问题。
日志
FastAPI 会记录每一次对端点的 API 调用,包括时间戳、方法和 URL,但不会记录通过请求体或请求头传递的任何数据。
指标、监控、可观测性
如果你运行一个网站,你大概会想知道它表现如何。对于一个 API 网站,你可能想知道哪些端点正在被访问、有多少人在访问,等等。关于这些因素的统计被称为指标,而对这些指标的收集被称为监控或可观测性。
如今流行的指标工具包括用于收集指标的 Prometheus,以及用于展示指标的 Grafana。
追踪
你的网站表现得有多好?常见情况是,整体指标不错,但这里或那里有些令人失望的结果。或者,整个网站可能都是一团糟。无论哪种情况,拥有一个能够测量一次 API 调用端到端耗时的工具都很有用——而且不仅是总耗时,还包括每个中间步骤的耗时。如果某些东西很慢,你就可以找到链条中的薄弱环节。这就是追踪。
一个新的开源项目把 Jaeger 等早期追踪产品吸收进来,并以 OpenTelemetry 的品牌重新组织。它有一个 Python API,并且至少有一种与 FastAPI 的集成。
要用 Python 安装和配置 OpenTelemetry,请按照 OpenTelemetry Python 文档中的说明操作。
其他
这些生产问题会在第 13 章中讨论。除此之外,我们的领域,也就是神秘生物以及与其相关的任何东西,又该如何呢?除了探索者和生物的基本详情,你还可能想处理什么?你可能会提出一些新想法,而这些想法需要对模型和其他层进行修改。下面是一些你可以尝试的想法:
- 探索者与他们寻找的生物之间的链接
- 目击数据
- 探险
- 照片和视频
- Sasquatch 马克杯和 T 恤,见图 9-1
图 9-1 来自赞助商的一句话
这些类别中的每一种通常都需要定义一个或多个新模型,以及新的模块和函数。其中一些会在第 IV 部分中添加,该部分是一个应用程序画廊,会在第 III 部分构建的基础之上继续添加应用。
回顾
在本章中,你复制了一些来自 Web 层的函数,并移动了它们所使用的假数据。目标是启动新的 Service 层。到目前为止,这还是一个模板化的过程,但此后它会演进并分化。下一章会构建最后的 Data 层,从而得到一个真正运行的实时网站。