后端结构、语言与客户端
后端不是一个“写接口的语言 + 一个数据库”。它是一组需要长期维护的边界:请求怎样进入,业务规则放在哪里,数据访问怎样隔离,哪些模块可以依赖哪些模块,客户端与服务器各自负责什么。
语言和框架决定表达方式,结构决定变更成本。项目早期全写在一个文件里也能上线,但当需求变化、多人协作或故障排查出现时,边界是否清楚就会变成主要矛盾。
1. 语言选择看约束
跳转到“1. 语言选择看约束”不同语言在语法上的差异,通常不是最难的。真正要比较的是运行时模型、并发方式、类型系统、生态、部署和团队经验。
| 语言 | 常见优势 | 需要关注 |
|---|---|---|
| Python | 生态丰富、表达直接、数据与脚本工具成熟 | 全局解释器锁/并发模型、类型提示需工具执行、性能热点 |
| JavaScript/TypeScript | 前后端共享语言、事件循环、生态大 | 单线程 CPU 阻塞、运行时类型校验、依赖复杂度 |
| Go | 编译快、部署简单、并发原语直观 | 泛型与生态取舍、错误处理风格、框架选择 |
| Java/Kotlin | 强类型、成熟 JVM 生态、工具链完整 | 启动与内存、框架复杂度、抽象层过多 |
| C# | 强类型、跨平台运行时、企业生态 | 平台和团队经验、部署体积 |
| Rust | 内存与并发安全、性能强 | 学习曲线、开发速度、生态成熟度因领域而异 |
“性能”不能只看基准测试。数据库、网络、序列化和磁盘通常比语言执行速度更影响端到端延迟。CPU 密集、内存敏感或高并发场景才更需要细看运行时。
1.1 类型系统与运行时
跳转到“1.1 类型系统与运行时”静态类型能在编译期发现字段和调用错误,但不能证明数据在运行时有效。动态类型更灵活,边界处需要 schema 校验和测试。强类型语言也可能有运行时空值、反序列化和数据库类型不匹配。
1.2 内存与并发
跳转到“1.2 内存与并发”垃圾回收语言减少手动释放负担,但可能出现暂停和内存峰值;手动内存管理提供控制,也增加错误风险。线程、协程、异步任务和消息传递各有适用场景。无论语言提供什么原语,共享可变状态都需要明确同步。
1.3 选型问题
跳转到“1.3 选型问题”- 团队是否能维护,而不是是否能写出第一版。
- 目标平台与部署方式是否合适。
- 关键库、数据库驱动和观测工具是否成熟。
- 类型与 schema 是否能在边界执行。
- 性能瓶颈是否真的在语言层。
- 招聘、升级、安全补丁和长期支持成本。
2. Web 框架的位置
跳转到“2. Web 框架的位置”框架通常处理路由、请求解析、中间件、响应、依赖注入和错误处理。它能让第一个接口很快跑起来,但不会自动带来好结构。
一个请求通常经过:
HTTP 服务器 → 框架中间件 → 路由 → 控制器/处理器 → 应用服务 → 领域规则 → 仓储/外部适配器 → 数据库/API框架选择应看:
- 请求模型是同步、异步还是二者混合。
- 中间件、认证、校验和 schema 支持。
- 测试工具是否方便替换依赖。
- 可观测性、部署和进程管理方式。
- 版本升级和安全维护。
不要因为框架提供依赖注入容器,就把所有对象都注册进去;也不要因为框架简单,就把业务逻辑写进路由处理函数。
3. 分层与依赖方向
跳转到“3. 分层与依赖方向”分层的目标是隔离变化,而不是制造文件数量。
3.1 常见四层
跳转到“3.1 常见四层”| 层 | 负责 | 不应负责 |
|---|---|---|
| 接口层/Controller | 解析输入、调用用例、选择响应 | 复杂业务规则、直接拼接 SQL |
| 应用层/Service | 组织用例、权限、事务边界 | HTML/HTTP 细节、底层驱动细节 |
| 领域层/Domain | 业务实体、值对象、规则与不变量 | 数据库连接、框架对象 |
| 基础设施/Repository/Adapter | 持久化、消息、外部 API | 决定业务是否允许 |
这是一张常用图,不是唯一答案。小项目可以合并应用层和领域层,但职责不应反过来。
3.2 DTO 与领域模型
跳转到“3.2 DTO 与领域模型”DTO(数据传输对象)用于接口或层之间的数据结构,不等于数据库实体,也不一定等于领域对象。
HTTP JSON ↕ DTO / schema 校验应用输入 ↕ 转换领域对象 ↕ 映射数据库行把同一对象在所有层直接传递,短期省代码,长期会让外部字段、数据库字段和业务规则互相污染。字段改名可能破坏 API,数据库迁移会影响业务对象。边界转换看似繁琐,却能把变化限制在局部。
3.3 依赖倒置
跳转到“3.3 依赖倒置”业务层不应该依赖某个数据库驱动的具体类,而应依赖它需要的能力接口:
OrderService → OrderRepository(接口) ↑ PostgresOrderRepository(实现)这样测试时可以替换内存仓储,更换数据库时业务逻辑不必跟着改。接口应由使用方定义,而不是把底层实现的所有方法都暴露出来。
3.4 贫血与充血
跳转到“3.4 贫血与充血”贫血模型把数据和行为分离,业务规则散落在服务层;充血模型把规则放进实体。两者都能工作。关键不是标签,而是规则是否有单一位置、是否可以在没有数据库时测试、是否容易产生非法状态。
4. 项目结构按变化组织
跳转到“4. 项目结构按变化组织”常见但容易失控的结构是:
controllers/services/repositories/utils/当项目只有几个功能时没问题;功能变多后,一次修改需要跨五个目录,边界仍不清楚。按业务功能或限界上下文组织通常更耐用:
src/ orders/ http/ application/ domain/ infrastructure/ payments/ ... shared/ db/ auth/ observability/每个功能内部可以再分层。跨功能依赖应通过明确接口或事件,而不是直接访问对方内部表。
4.1 模块边界
跳转到“4.1 模块边界”一个模块应该:
- 暴露少量稳定的公共接口。
- 隐藏内部数据结构和实现。
- 拥有自己的测试和迁移。
- 不随意导入另一个模块的内部文件。
- 有清晰的所有权与变更节奏。
单仓库(monorepo)和多仓库都只是代码组织方式。单仓库便于共享和统一构建,但需要依赖规则与权限;多仓库隔离强,但版本协调和跨仓修改更麻烦。
5. 架构风格
跳转到“5. 架构风格”5.1 单体
跳转到“5.1 单体”一个部署单元包含多个业务模块。它的优势是事务简单、调用直接、部署和调试成本低。问题不是“单体落后”,而是模块边界不清、所有功能共享一个数据库且无法独立扩展。
一个结构良好的模块化单体,往往比过早拆分的微服务更可靠。
5.2 微服务
跳转到“5.2 微服务”服务按业务边界拆分、独立部署,适合团队规模大、模块需要独立扩容或发布节奏不同的场景。代价包括:
- 网络调用替代本地函数,增加延迟和部分失败。
- 数据跨服务后,事务和一致性更难。
- 需要服务发现、网关、追踪、配置和部署平台。
- 版本兼容、联调和故障定位成本上升。
拆分的依据应是业务边界和团队所有权,不是“服务数量看起来更先进”。
5.3 事件驱动
跳转到“5.3 事件驱动”服务通过事件解耦,适合异步流程和跨模块通知。但要处理事件顺序、重复、丢失、schema 演进和最终一致。事件是已经发生的事实,命令是希望对方执行的动作,两者语义不同。
5.4 Serverless
跳转到“5.4 Serverless”平台负责服务器和扩缩容,开发者关注函数或托管服务。优势是按需伸缩、运维少;代价是冷启动、执行时长、连接管理、调试和供应商绑定。长连接、重度后台任务和可预测高负载不一定适合。
6. 客户端形态与跨平台
跳转到“6. 客户端形态与跨平台”后端并不是唯一客户端。理解客户端约束有助于定义 API:
| 客户端 | 特点 | API 设计影响 |
|---|---|---|
| Web | 发布快、跨平台、受浏览器沙箱限制 | CORS、缓存、Cookie、资源体积 |
| iOS/Android 原生 | 体验与系统能力好,发布需审核 | 版本碎片、离线、推送、权限 |
| 跨平台 App | 共享代码,可能牺牲部分原生能力 | 平台差异、插件质量和性能 |
| 桌面应用 | 可访问文件、系统能力和本地资源 | 更新、签名、权限、代理 |
| CLI/自动化 | 无 UI、可脚本化、需稳定退出码 | 机器可读输出、非交互、幂等 |
跨平台不是“写一次到处完全一样”。文件路径、权限、通知、后台限制、键盘和输入法都会不同。共享业务逻辑和类型可以,但平台边界仍需专门处理。
6.1 客户端与服务端的职责
跳转到“6.1 客户端与服务端的职责”- 客户端负责交互、即时反馈和部分缓存,不能作为权限边界。
- 服务端负责最终校验、业务规则、权限和持久化。
- 客户端版本可能长期存在,API 要能兼容旧版本。
- 离线操作需要冲突解决策略,而不只是本地队列。
7. 领域专用语言与配置
跳转到“7. 领域专用语言与配置”DSL(领域专用语言)是为特定问题设计的表达方式。SQL、HTML、CSS、正则、Terraform HCL、Kubernetes YAML 和构建配置都可以看作 DSL。
DSL 的价值在于把某类规则变成可检查、可复用的结构。代价是学习成本、工具链和版本演进。团队自造 DSL 前应先确认现有格式是否足够,以及谁来维护解析器、错误提示和迁移。
7.1 配置与代码的边界
跳转到“7.1 配置与代码的边界”- 配置适合随环境变化、需要运维修改的值。
- 代码适合复杂逻辑、类型约束和必须测试的行为。
- 不要把整个业务规则塞进 YAML;也不要把所有环境差异硬编码进代码。
配置也需要 schema 校验、默认值、版本和变更审计。一个拼写错误可能在启动时才暴露,甚至静默使用错误默认值。
8. 结构怎样验证
跳转到“8. 结构怎样验证”结构好坏不能只看目录图。用这些问题检查:
- 修改一个业务规则,需要改几处?
- 能否在没有数据库的情况下测试核心规则?
- 外部 API 字段变化会影响多少内部代码?
- 新同学能否在半小时内找到某个请求的入口?
- 模块之间是否存在循环依赖?
- 数据迁移和接口版本是否有明确路径?
- 一个模块失败时,是否会把所有模块拖垮?
检查清单
跳转到“检查清单”- 语言选择是否基于团队、生态、部署和真实瓶颈。
- 业务规则是否集中,是否避免散落在控制器和 SQL 中。
- 依赖方向是否从外层指向内层,而不是互相穿透。
- DTO、领域对象和数据库模型是否有必要的边界。
- 项目是否按业务能力组织,模块是否暴露清晰接口。
- 单体、微服务或事件驱动是否有明确理由,而不是跟风。
- 客户端版本、权限和离线行为是否纳入 API 设计。
- DSL 与配置是否有 schema、默认值、版本和校验。
- 关键模块是否能在没有完整基础设施时测试。