第6章 编写模块化应用

第6章 编写模块化应用

学习目标

  • 能把一个应用拆成解析、存储、生成、编排四个可独立测试的包,并画出它们之间的依赖方向
  • 能用 frozen dataclass 与 Protocol 让基础设施依赖领域,而非领域反向依赖基础设施
  • 自测:换掉一个存储实现后,哪些测试需要改、哪些不需要?为什么?

为什么一个能跑的脚本还不算应用

先预测:一个示例在作者机器上跑通,是否足以证明它可维护?答案是不能。一个能跑的例子只说明在那一刻、那台机器上输入对了;它没有说清解释器与依赖、输入边界、失败路径、可观察结果和重建步骤。本章把这些写成可检查的契约,再放进可重复的流程。

把职责切开是第一步。解析、存储、生成和编排各自能单独测试,改一个不会牵连另一个;先画清输入源、解析器、数据库、输出和主程序之间的依赖方向,再在隔离环境里跑起来。基础设施只能依赖接口,不能反过来把数据库细节漏进业务模型。

本章用原书的 Atomisator 案例——一个聚合订阅源的小应用——演示这套切法。原书成书于早期工具生态,命令会按当前 Python 与 PyPA 文档迁移,但“为什么这样切”的结构原样保留。

原书骨架与现代迁移

官方章节围绕Atomisator案例、整体架构与工作环境、测试运行器与包结构、parser、db与feed API、应用分发与依赖展开。原书出版于Python 2.5与早期敏捷工具生态,本章保留它解释“为什么”的结构;命令和安全默认按当前Python、标准库与PyPA维护文档迁移,不把EasyInstall、distutils安装命令、旧CI或旧项目平台直接当成新项目默认。

承接前两项,把局部语法或工具放回应用数据流。阅读时沿输入、协议、状态、输出和证据追踪,避免只背API名称。

原书把数据契约固化为,下面两种视角对比同一段数据契约:

from dataclasses import dataclass
 
@dataclass(frozen=True)
class Entry:
    source: str
    title: str
    url: str

机制一:责任与数据流

原书用Atomisator聚合订阅源,展示应用不是一个大脚本,而是解析、存储、生成和编排等可独立测试的包。 先画输入源、解析器、数据库、输出feed和主程序的依赖方向,再配置隔离环境;这里依赖方向遵循,基础设施只能依赖接口,不反向污染领域模型。 先写出公共行为和失败类型,再选择语法或工具。这样即使实现从原书工具迁到当前生态,调用者仍能依据相同契约判断结果。

提醒我们:抽象不能消除成本,只会改变成本出现的位置。包装、生成器、构建系统、CI或缓存都必须说明资源、顺序和异常传播。

基础设施只能依赖接口,下面两种视角对比同一段依赖注入逻辑:

from typing import Protocol
 
class EntryStore(Protocol):
    def save(self, entries: list[Entry]) -> int: ...
 
def ingest(parser, store: EntryStore, payload: bytes) -> int:
    return store.save(parser.parse(payload))
编写模块化应用
Atomisator案例、架构、测试、parser/db/feed、分发AtomisatorAtomisator案例架构整体架构与工作环境测试测试运行器与包结构分发应用分发与依赖
Atomisator案例

应用不是大脚本,而是解析、存储、生成和编排等可独立测试的包。先画依赖方向再配置隔离环境。

机制二:失败与边界

测试入口和包结构在写业务前建立反馈环;这种反馈环靠建立:单元测试隔离解析与存储,集成测试再连接临时数据库和真实格式样本。 解析器输出稳定记录,数据库负责事务,feed只渲染查询结果;跨包API传领域值与显式错误,不泄漏ORM会话或全局连接。 边界实验至少包含正常、空输入、上限附近、依赖失败和重复执行。涉及网络、并发或外部制品时,再加入超时、取消、部分完成与摘要校验。

历史工具不等于无价值。正确迁移是先提取声明式配置、隔离、持续反馈和可回滚发布等不变量,再用维护中的接口重写;错误做法是机械替换命令却保留隐式环境和不可追踪副作用。

测试入口在写业务前建立反馈环,下面两种视角对比同一段测试编排:

python -m unittest tests.unit
python -m unittest tests.integration
python -m atomisator --check-config

机制三:证据闭环

本节负责收束:应用由多个包组成时要区分库依赖和部署配置,锁定完整运行环境并验证入口点;分发成功还要证明迁移、启动和关闭。 保存解释器、依赖锁定、输入、命令、退出状态、关键输出和制品摘要,才能让另一台干净机器重放同一结论。

实战验收清单

  1. 在隔离环境运行三段示例,记录解释器实现、版本和依赖来源。
  2. 为核心行为增加正常、空输入、失败和重复执行测试,先看到失败再修改实现。
  3. 清理缓存和临时文件后重跑,证明结果不依赖工作区残留。
  4. 对历史命令写出当前替代路径,并说明保留的架构不变量与不再采用的安全默认。

迁移决策题

设想团队正在维护一个已经运行多年的Python服务:它仍依赖本章对应的历史工具,但业务不能停机。先不要直接重写。第一步列出Atomisator案例承担的真实输入和输出,再用整体架构与工作环境识别构建或运行时依赖;第二步把测试运行器与包结构放进隔离实验,证明当前行为与失败类型;第三步用parser、db与feed API设计兼容层,让旧入口和新入口在同一组契约测试下运行;最后以应用分发与依赖保存制品摘要、性能或行为差异与回滚条件。只有新路径在正常、边界和故障输入上都达到既定条件,才逐步切换流量或调用者。这样迁移的是可验证契约,而不是把一个旧命令盲目替换为一个新命令。

常见误区

误区 1

现象 → 领域模型里直接 import ORM 类,换存储引擎要改模型 原因 → 领域模型反向依赖基础设施,违反依赖倒置 修法 → 领域值用 dataclass/Protocol 定义,存储实现依赖领域接口而非反之

误区 2

现象 → 单元测试连真实数据库,CI 慢且不稳定 原因 → 没有隔离单元层与集成层,测试共享同一环境 修法 → 单元测试用内存桩,集成测试连临时数据库,两层分开运行

误区 3

现象 → 跨包传递 ORM 会话对象,改一处接口全链路报错 原因 → 泄漏基础设施细节到领域层,包之间强耦合 修法 → 跨包 API 只传领域值与显式错误,不传会话、连接或全局状态

误区 4

现象 → 部署后启动失败,本地却正常 原因 → 依赖未锁定,部署环境装了不同版本的包 修法 →pyproject 声明依赖、lock 锁定版本,在干净机器验证启动与关闭

本章回顾

本章逐项覆盖Atomisator案例、整体架构与工作环境、测试运行器与包结构、parser、db与feed API、应用分发与依赖。方法是用可读接口表达责任,用边界测试证明失败语义,再以可重建环境和制品证据完成闭环。

小结

  • 应用不是大脚本:解析、存储、生成、编排各自独立可测的包
  • 领域值用 frozen dataclass,跨包传递不泄漏基础设施
  • Protocol 定义接口,基础设施依赖领域而非反向
  • 测试分层:单元隔离、集成连真实,两层分开运行
  • 分发要锁依赖、验入口,干净机器能重放启动与关闭

练习与验收

练习

问题 1: 为什么 Entry 要用 frozen=True

问题 2: EntryStoreProtocol 而非具体类有什么好处?

问题 3: 为一个“聚合 RSS 订阅源”的应用设计包结构与分层测试入口:要求解析、存储、feed 三个包各自独立可测,集成测试连临时数据库,并提供启动配置检查入口。(独立实现)

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

领域值

用不可变数据类装着的业务数据,解析、存储、生成各包都认它,但它不带数据库连接或会话,换存储不用改业务。

依赖倒置

业务定义接口,数据库等基础设施去实现它;业务代码不反过来 import 数据库类,换底层不影响业务。

测试分层

单元测试用假数据隔离跑业务逻辑,集成测试连临时数据库跑真实样本,两层分开跑、各有各的退出码,失败能定位到具体层。

证据闭环

把解释器版本、依赖锁、输入、命令、退出状态和产物摘要都存下来,换一台干净机器照着做能复现同一结果。

资料与写作方式声明

本章以Tarek Ziade《Expert Python Programming》合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…