news 2026/10/27 7:34:55

SIMPL五条硬约束:用可度量简单控制软件架构复杂度

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SIMPL五条硬约束:用可度量简单控制软件架构复杂度

一个项目最先崩掉的往往不是功能,而是接手的人脑子里的那张图。我带的几个项目都撞过同一堵墙:最初只有三个文件,半年后变成四十个包、六层调用、一堆没人敢删的兼容分支。改一行日志要翻五个仓库,新人上手第一周全在问“这个到底走哪条链路”。后来我们内部把一套被反复验证过的收敛手法固化成约定,取名叫SIMPL——名字不是缩写炫技,而是把“简单”当成一条可以被度量、被评审、被强制执行的工程约束。它说的是:用五条硬性约束管住项目复杂度,让一个中等规模的系统在迭代十几次之后,仍然能被一个人完整读懂。这篇文章面向已经写过几个项目、也开始被维护成本反噬的开发者,也面向刚入行、想一开始就把地基打正的新人。我会把 SIMPL 的五条约束拆开讲透,再给出一套可直接照抄的目录结构、代码骨架和评审清单。

1. SIMPL 到底是什么:先把概念钉死

1.1 五个字母对应的五条硬约束

网上叫“极简”的方法论太多了,多数停在口号层面——“保持简单”“避免过度设计”。这类话在评审会上毫无杀伤力,因为没人能证明自己违反了它。SIMPL 的做法是把“简单”拆成五条可以当场判定对错的约束,每条对应一个字母。

字母约束判定方式违反后的典型症状
SSimple Surface(接口面收敛)单个模块对外公开的入口不超过 7 个调用方不知道该用哪个函数
IIsolated Boundary(边界隔离)跨层调用必须经过显式接口,禁止越级改底层要连带改上层 UI
MMinimal Dependency(依赖最小化)新增第三方依赖需说明“不用它会多写多少行”装了两百个包,构建要八分钟
PPredictable Flow(数据流可预测)同一个状态只有一处写入点出现“改了这个值,那个页面自己变了”
LLayered Depth(分层深度受限)从入口到最底层的调用深度不超过 4 层断点要打八个,栈帧看不完

这套东西的价值不在“新”,而在“可执行”。我见过太多团队把架构图画得漂漂亮亮,落地时全靠自觉。SIMPL 的每一条都能在代码评审时被指出来:这里公开了 11 个方法,超了;这条调用链穿了 5 层,砍掉一层;这个依赖只为了格式化一个日期,自己写 15 行不行?

注意:SIMPL 是约束,不是银弹。它的前提是你的项目还有救——如果代码已经烂到没人愿意碰,先别谈约束,先做止血。

1.2 为什么“简”比“全”难得多

写代码的时候,“多写一点”几乎总是更安全的选项。多留一个兼容分支,多包一层抽象,多开一个配置项——每个决定单独看都合理,甚至有“防御性编程”的光环。问题在于这些东西不会自己消失。三个月后,那个“万一以后要支持多租户”的抽象层还空着,但每个新人都得花两小时搞懂它为什么存在。

复杂度是有复利的。我统计过一个模拟项目 X 的演化:第一版 1200 行,接口 6 个;半年后 9800 行,接口 47 个。代码量涨了 8 倍,但真正处理业务的分支只涨了 2 倍出头,剩下的全是抽象层、适配层、兼容层。这中间的差额就是“为了将来可能的需求”付出的利息。

SIMPL 的第一原则是:任何抽象都必须由一个已经发生的、具体的重复来支撑,而不是由一个想象中的未来。这条听起来保守,实际执行起来非常反直觉。因为删掉一个“可能有用”的抽象层,心理成本远高于留着它。我自己的做法是设一条硬线:同一段逻辑出现第三次,才允许抽取;只出现两次,宁可复制。

有人会说复制粘贴是技术债。是,但它是一种看得见、改得动的债;而过早抽象是一种看不见、改不动的债。前者的利息是未来某次改动要改两处,后者的利息是每次改动都要先理解一层没人说得清的东西。孰轻孰重,做过两年维护的人心里都有数。

1.3 什么场景该用,什么场景别硬套

SIMPL 明显不适合两类项目。第一类是探索型原型:需求本身还没定,这时候任何结构都是浪费,直接一个文件写到底,能跑就行,跑通了再谈重构。第二类是强合规或强集成场景:比如必须对接多个外部系统的场景,接口数量由对方定,这时候“接口不超过 7 个”这条约束就是自欺欺人。

它最适合的场景我总结成一句话:有明确业务边界、需要长期维护、团队规模在 1 到 8 人之间的系统。这个区间很关键。一个人做,约束靠自律就够;超过八个人,沟通成本会压过结构收益,你需要的可能是更重的流程而不是更轻的结构。

判断标准可以量化一下。拿你手上的项目算三个数:

  • 新人从拉代码到跑通第一个改动,需要几小时?超过 4 小时,结构有问题。
  • 改一个字段,平均要动几个文件?超过 5 个,边界没隔开。
  • 删掉一个功能,你敢不敢直接删?不敢,说明依赖关系已经糊成一团。

这三个数不用工具,拿秒表和一双手就能测出来。测完你就知道自己该不该往下读了。

2. 整体设计思路:把复杂度关进笼子

2.1 先算复杂度预算,再动手写代码

大部分人做设计是从“要实现什么”出发的,SIMPL 建议反过来:先算“允许多复杂”,再往里填功能。这套算法我用了两年,粗糙但有效。

复杂度预算的核心指标是有效交互面:把系统里所有可以被外部触发的东西数出来,包括 HTTP 接口、消息队列的消费点、定时任务、CLI 命令、对外暴露的函数。这个数字乘以平均调用深度,就是你系统的心智负担近似值。

我通常这样估:

  • 单模块公开入口数上限:7(这个数字来自人短期记忆的容量经验值 7±2,超过之后调用方基本靠翻文档)
  • 调用深度上限:4(入口层 → 编排层 → 领域层 → 基础设施层,再多一层就说明职责没切干净)
  • 单文件行数软上限:400(超过之后滚动条就成了主要导航工具)
  • 单函数行数上限:50(超过之后基本没法在屏幕上完整看完)

拿一个模拟项目 X 举例,它的业务只有三件事:接收上报、聚合统计、生成报表。按上面的约束,接口面算下来是 3 个业务入口 × 平均 2 个变体 = 6 个,刚好在 7 以内。这就意味着它不该有独立的鉴权服务、独立的配置中心、独立的消息总线。不是这些技术不好,是这个体量的项目用不上,装上就是纯负担。

提示:复杂度预算要在项目启动时算一次,写进 README 的第一屏。它最大的作用是给后来的人一个“反驳的靶子”——当有人提议加一层缓存中间件时,你可以指着那个数字问:加完之后入口数变成多少?

2.2 分层与依赖方向:只允许单向流动

SIMPL 的 I(边界隔离)和 L(分层深度)落地成一条规则:依赖只能单向下行,且最多四层。

我把四层定义得很直白,不玩概念:

  1. 入口层:HTTP 路由、CLI 入口、定时任务触发点。只做参数校验和转发,不写业务。
  2. 编排层:把多个领域操作串成一个用例。负责事务边界和错误转换。
  3. 领域层:纯业务规则,不依赖任何框架,输入输出都是普通数据结构。
  4. 基础设施层:数据库、缓存、外部调用、文件读写。只被领域层通过接口调用。

关键在方向上:入口层可以调编排层,编排层可以调领域层,领域层通过接口调基础设施层。反过来一律禁止。领域层不许 import 框架的 ORM,入口层不许直接连数据库。

为什么卡这么死?因为依赖方向决定了改动的影响半径。如果领域层直接 import 了 ORM,那你想换存储方案时,业务代码里全是 ORM 的查询构造器,等于重写。反过来,领域层只认一个Repository接口,换存储时只需要实现一个新类,业务代码一行不动。

这套隔离在项目小的时候看起来像“为了一个可能永远不会发生的迁移写样板代码”。我的建议是:接口只写一次,但写法要极简。不要去定义IRepositoryFactoryProvider这种层级的接口,一个模块一个接口类,三五个方法,够了。

2.3 技术选型:够用即止的三问法

每次要引入新东西,我让提需求的人先回答三个问题。回答不上来,就不加。

第一问:不用它,我要多写多少行?如果答案是“不到 200 行”,自己写。为 200 行代码引入一个长期维护的外部依赖,性价比是负的——你不仅要管它,还要管它的版本升级、它的安全通告、它和别的库的冲突。

第二问:它会不会成为我唯一的退出路径?有些库一旦用上就拔不掉,比如深度侵入数据模型的 ORM、绑定特定框架的认证方案。这类依赖要格外谨慎,因为退出成本可能是重写。

第三问:它的心智负担有多大?一个库如果需要在项目里写 80 行配置才能跑起来,那它的实际成本是 80 行配置 + 未来每个新人都要读这 80 行。相比之下,一个零配置的库哪怕功能少一半,长期看也划算。

我做过一个对比,两个模拟项目做同一件事——解析一批结构化文本并入库:

方案依赖数量首次可用耗时半年后修改成本
引入一个全功能解析框架7 个传递依赖2 天(含学习)改字段要读框架文档
手写 180 行解析 + 标准库01 天直接改自己代码

第二个方案的首次耗时不比第一个短太多,但半年后修改成本差了三倍以上。这个结论我在至少四个项目里反复验证过。

3. 核心环节落地:从零搭一个 SIMPL 结构

3.1 目录划分:让结构自己说话

目录结构是 SIMPL 最容易被抄走也最容易被抄错的部分。常见错误是照着“标准分层”建了十几个空目录,结果每个目录里只有一两个文件。我的原则是:目录数量跟实际模块数量走,不预留空位。

一个中等规模的模拟项目 X(三到五个业务模块),我一般这样布:

project/ app/ entry/ # 入口层:路由、CLI、定时任务 http.py jobs.py flow/ # 编排层:用例串接 report_flow.py ingest_flow.py domain/ # 领域层:纯业务规则 models.py rules.py ports.py # 领域层对外的接口定义 infra/ # 基础设施层:具体实现 db.py storage.py config.py tests/ unit/ integration/ pyproject.toml README.md

四个目录对应四层,一一对齐,不搞花活。ports.py是这套结构的关键——它定义了领域层需要基础设施提供什么能力,但只有接口,没有实现。基础设施层的类去实现这些接口,在入口处组装。

注意:不要建utils/这种目录。它是一切的垃圾桶入口,一旦存在,两周内就会塞满不知道该放哪的东西,而且这些东西之间毫无关系。真需要公用函数,放到使用它的那一层里,或者单独开一个语义明确的模块。

3.2 配置与环境:一个文件,三种来源

配置管理的复杂度经常被低估。我见过一个项目有config.py、settings.py、env.py、constants.py四个配置相关的文件,谁也说不清哪个生效。

SIMPL 的做法是:只有一个配置入口,值来自三个优先级明确的来源。

优先级从高到低:环境变量 > 本地配置文件 > 代码内置默认值。这样本地开发不用配环境变量,部署时环境变量覆盖一切,代码里的默认值保证“不配也能跑起来”。

具体实现上,我用一个不可变的配置对象,启动时一次性读齐,之后全程只读。这解决了一个非常隐蔽的问题:配置在运行中被修改。有些人为了方便,会把配置对象到处传,某个中间件顺手改了一个值,后面所有逻辑都受影响,排查时根本想不到。

# infra/config.py from dataclasses import dataclass import os import json @dataclass(frozen=True) class Config: db_url: str batch_size: int report_dir: str @classmethod def load(cls, path: str = "config.local.json") -> "Config": file_values = {} if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: file_values = json.load(f) def pick(key, default, cast=str): raw = os.environ.get(key.upper(), file_values.get(key, default)) return cast(raw) return cls( db_url=pick("db_url", "sqlite:///local.db"), batch_size=pick("batch_size", 200, int), report_dir=pick("report_dir", "./out"), )

frozen=True这一行是整个设计里最便宜也最有效的保险。它让运行时被悄悄改配置这种事从一开始就报错,而不是等到线上出问题才发现。

参数怎么定?batch_size我按单次处理耗时来反推:目标单批不超过 2 秒,实测每条记录 5 毫秒,那 200 是安全值,800 就有超时风险。这种“先定目标再倒推数值”的习惯,比拍脑袋写个 1000 靠谱得多。

3.3 数据流:单一写入点

P(可预测的数据流)是最容易在评审时被忽略、出问题时最难排查的一条。它的判定标准只有一句:任何一个状态,全项目只能有一个地方写它。

做到这一点,靠的是把“状态”和“变更”分开。状态本身是只读的数据结构,变更通过一个明确的函数完成,这个函数在项目里只出现一次。

举个具体例子。模拟项目 X 里有一个“任务状态”,取值是待处理、处理中、完成、失败。糟糕的写法是在入口层读数据库、判断、写回,在定时任务里也读数据库、判断、写回。两个地方各写一遍,规则稍有出入就出 bug——比如入口层允许“失败”重试,定时任务却不允许,两边打架。

正确的做法是把状态变更收进领域层的一个函数:

# domain/rules.py from dataclasses import replace from domain.models import Task, TaskStatus ALLOWED = { TaskStatus.PENDING: {TaskStatus.RUNNING}, TaskStatus.RUNNING: {TaskStatus.DONE, TaskStatus.FAILED}, TaskStatus.FAILED: {TaskStatus.RUNNING}, TaskStatus.DONE: set(), } def transit(task: Task, target: TaskStatus) -> Task: if target not in ALLOWED[task.status]: raise ValueError(f"非法状态迁移: {task.status} -> {target}") return replace(task, status=target)

这个小函数只有十几行,但它是整个系统的“规则唯一真相”。入口层要用,调它;定时任务要用,也调它。想加一条新规则,改这一个地方,不用满项目搜status =。

提示:replace用的是不可变数据类,返回新对象而不是就地修改。这一点看起来多写了几个字符,但它让“谁改了状态”这个问题彻底消失——状态只能被新对象替代,不可能被暗中修改。

3.4 构建与部署:一条命令跑完

构建流程复杂度是 SIMPL 里最容易被放过的一块,因为它不影响代码质量,只影响人的耐心。但当“部署一次要执行六个脚本、顺序还不能错”的时候,就没人愿意频繁部署了,而没人愿意频繁部署的团队,发布节奏一定慢。

我的标准是:从干净环境到服务可用,一条命令。

./run.sh prepare # 装依赖、建目录、初始化数据库 ./run.sh test # 跑测试 ./run.sh start # 启动服务

run.sh内部不超过 60 行,每一步都打印在做什么。我坚持不引入复杂的构建工具链,理由很实际:这套东西是给值班的人用的,半夜出问题时,他要能在三十秒内看懂这条命令在干什么。

依赖锁定必须做。不是“写上版本号”那种锁定,而是真正的锁文件——所有传递依赖的版本都被固定下来。这解决的是“上个月还能构建,这个月突然失败”的经典问题:某个下游库发了个小版本,行为变了。

4. 代码与配置细节:可以直接抄的部分

4.1 一个最小可运行骨架

把前面几节的内容串起来,一个最小可运行的服务大概长这样。它没有任何框架,纯标准库,但四层结构齐全。

# app/domain/models.py from dataclasses import dataclass from enum import Enum class TaskStatus(str, Enum): PENDING = "pending" RUNNING = "running" DONE = "done" FAILED = "failed" @dataclass(frozen=True) class Task: id: str status: TaskStatus payload: str
# app/domain/ports.py from typing import Protocol from domain.models import Task class TaskRepo(Protocol): def save(self, task: Task) -> None: ... def get(self, task_id: str) -> Task | None: ...
# app/infra/db.py from domain.models import Task from infra.config import Config import sqlite3 class SqliteTaskRepo: def __init__(self, cfg: Config): self._cfg = cfg def save(self, task: Task) -> None: with sqlite3.connect(self._cfg.db_url) as conn: conn.execute( "INSERT OR REPLACE INTO tasks(id, status, payload) VALUES (?, ?, ?)", (task.id, task.status.value, task.payload), ) def get(self, task_id: str) -> Task | None: with sqlite3.connect(self._cfg.db_url) as conn: row = conn.execute( "SELECT id, status, payload FROM tasks WHERE id = ?", (task_id,) ).fetchone() if row is None: return None return Task(id=row[0], status=TaskStatus(row[1]), payload=row[2])
# app/entry/http.py from http.server import BaseHTTPRequestHandler, HTTPServer import json from domain.rules import transit from domain.models import TaskStatus class Handler(BaseHTTPRequestHandler): repo = None # 由组装处注入 def do_POST(self): length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length) or b"{}") task = self.repo.get(body["id"]) if task is None: self._reply(404, {"error": "not found"}) return try: updated = transit(task, TaskStatus(body["to"])) except ValueError as e: self._reply(400, {"error": str(e)}) return self.repo.save(updated) self._reply(200, {"id": updated.id, "status": updated.status.value}) def _reply(self, code: int, data: dict): raw = json.dumps(data).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw)

这段代码不到 120 行,但四层结构、依赖方向、单一写入点、不可变配置全部体现出来了。Handler.repo是注入的,入口层不知道具体是 SQLite 还是别的什么,它只认save和get两个方法——这就是接口面收敛。

注意:Protocol是结构化类型,不需要显式继承。这意味着基础设施层的类不必 import 领域层的接口名,避免了反向依赖的风险。这一点在跨包的项目里特别重要。

4.2 依赖注入:手写,别上容器

依赖注入容器是个典型的过度设计高发区。我见过一个不到两万行的服务,用了三个注解、两个配置类来管理依赖,最后没人说得清某个对象的生命周期到底是单例还是每次新建。

SIMPL 的做法是:在进程启动的地方,一个函数里组装所有对象,手工传参。

# app/main.py from infra.config import Config from infra.db import SqliteTaskRepo from entry.http import Handler def build(): cfg = Config.load() repo = SqliteTaskRepo(cfg) Handler.repo = repo return cfg if __name__ == "__main__": cfg = build() Handler.repo.init(cfg) HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

整个组装逻辑不到十行,一眼看完,出问题直接在这一处断点。依赖关系从“框架帮我管”变成“我明确知道谁依赖谁”。项目规模在几十个类以内时,手写组装的成本几乎为零;只有到几百个类的规模,容器才有意义——而那时候你大概率该拆服务了,而不是该上容器。

4.3 日志与可观测性:三个必打字段

日志这块最常见的浪费是“什么都打”。日志量上去了,排查时反而找不到关键信息。SIMPL 的做法是规定三个必打字段,其他按需。

必打的三项是:请求标识、当前层、耗时。请求标识让一次调用链的所有日志能串起来;当前层让你一眼看出慢在哪一层;耗时帮你做性能归因。

import logging import time from contextlib import contextmanager @contextmanager def span(name: str, trace_id: str): start = time.monotonic() try: yield finally: cost = (time.monotonic() - start) * 1000 logging.info("trace=%s layer=%s cost=%.1fms", trace_id, name, cost)

用法很简单,在每个层的入口包一下:

with span("flow.ingest", trace_id): with span("domain.rule", trace_id): ...

这样一次请求打出来的日志是几行带层级的记录,慢在哪一层一目了然。我实测过一个批次处理慢的问题,靠这个五行的上下文管理器,十分钟就定位到是基础设施层单次连接建立开销太大,改成复用连接后耗时降了六成。要是没有分层耗时,大概率会先去优化业务逻辑,白忙半天。

4.4 测试策略:三层,比例别搞反

测试的复杂度同样会失控。我见过测试代码比业务代码多三倍的项目,改一个字段要修二十个测试,最后大家都开始跳过测试。

SIMPL 的测试策略是按层分配,比例大概 7:2:1。

单元测试占七成,只测领域层。因为领域层不依赖任何框架,测试写起来极快,一个测试几毫秒。这部分是真正能防回归的,业务规则变了,测试立刻红。

集成测试占两成,测编排层串起来的用例,用真实的数据库(内存版)。这部分测的是“各层拼起来还能不能跑”,重点在事务边界和错误传播。

端到端测试占一成,只覆盖两三条最关键的主路径。这部分慢、脆、维护成本高,所以数量必须少。

比例的判断依据很直接:测试的价值随它的运行速度下降而下降。一个跑 3 秒的测试,开发时不会愿意频繁跑;一个跑 3 毫秒的测试,可以挂在保存即运行的钩子上。所以把大部分测试压在最快的单元层,是最划算的投入。

提示:不要把测试覆盖率当目标。我见过为了冲覆盖率写的测试,全是“调用一下不报错就算过”,一点用处没有。真正该测的是分支和边界:非法状态迁移有没有被拦住、空输入会不会炸、并发写同一行会怎样。

5. 常见问题与排查实录

5.1 过度抽象的五个危险信号

抽象失控是 SIMPL 要解决的头号问题,但它往往是在“良好实践”的名义下发生的。我总结了五个信号,出现任意两个就该停下来审一遍。

第一个信号:只有一个实现者的接口。如果你定义了Storage接口,但全项目只有LocalStorage一个实现,那这个接口大概率是多余的。除非你有明确的、近期就要做的第二个实现,否则直接删掉接口,领域层直接依赖具体类,等真的需要第二个实现时再抽。

第二个信号:参数透传超过两层。一个函数收了a、b、c,自己不用,原样传给下一层。这说明调用链的层级设计有问题,或者这个函数根本不该存在。

第三个信号:配置文件里的开关从未被关闭过。项目里有个enable_new_flow的配置,上线三年一直是true。那就是纯粹的历史包袱,删掉。

第四个信号:同名的类出现在三个包里。比如UserService、UserManager、UserHandler,职责区别说不清。这通常是多人协作没对齐边界的结果。

第五个信号:单元测试需要 mock 五个以上的对象。这说明被测单元依赖太多,边界没切干净。正常的领域层单元测试,零 mock 才是常态。

5.2 依赖膨胀的排查方法

依赖膨胀是慢性病,等发现时构建已经很慢了。我一般三个月做一次依赖审计,方法很土但有效:把依赖列表拉出来,逐个问“删掉它要改几行”。

删掉改动量小于 50 行的,直接删;50 到 200 行之间,评估要不要自己实现;200 行以上的,留着,但记录下来。

这套方法的依据是:依赖成本 = 引入成本 + 长期维护成本。引入时省下的 50 行代码,在长期里会被版本升级、安全通告、冲突排查的成本反超。实测下来,一个中等项目能从 60 个直接依赖砍到 25 个左右,构建时间从五分钟降到一分半。

砍完要做回归,重点测三类地方:序列化边界(日期、数字、空值)、异常处理路径、并发相关逻辑。这三类是最容易被“看起来差不多”的替代品埋坑的地方。

5.3 问题速查表

实际操作里反复出现的问题,我整理成一张表,出问题时先按这张表过一遍,能省不少时间。

症状最可能原因排查动作处理方式
改一个字段要动很多文件边界没隔开,数据模型被透传搜字段名,看出现分布在编排层加转换,阻断透传
某接口偶发超时基础设施层连接未复用看分层耗时日志引入连接池,设上限
状态出现意料外的值存在多处写入点搜赋值语句收敛到唯一函数
单元测试要 mock 一堆对象领域层依赖了外部资源看领域层 import抽接口,反转依赖
上线后行为与本地不一致配置来源不明确打印最终生效配置启动时打一行配置快照
构建偶发失败依赖未真正锁定检查锁文件固定传递依赖版本
日志里找不到关联缺请求标识看日志格式入口生成标识,逐层透传

这张表里,我个人踩得最深的是“状态出现意料外的值”。当时排查了两天,最后发现是一个后台任务在做数据修复时直接写了数据库,绕过了所有规则。从那以后我在项目里加了一条硬规矩:任何写数据库的地方,如果是修复类脚本,必须在代码里注释说明绕过了哪条规则、为什么。这条规矩后来救过好几次。

5.4 一个真实踩坑的复盘

说个具体的。模拟项目 X 上线三个月后,出现一个奇怪现象:报表里的数据偶发对不上,重跑一次又对了。没有报错,没有超时,就是数据差那么几条。

排查路径是这样的。先看分层耗时日志,正常。看数据库记录数,正常。看报表生成逻辑,发现它是“先查总数,再分页查明细,最后汇总”。问题在这儿:总数和明细是两次独立查询,中间如果有新数据写入,两次结果就不一致。

根因是分页查询没有在同一个一致性视图下完成。修复方式很简单:改成一次性查出所有需要的行再内存分页,或者在同一个事务里完成两次查询。改动不到十行。

但这件事真正的教训不在技术上,而在设计上:报表这类“需要一致快照”的操作,在编排层就该声明成事务,而不是让领域层自己去猜。我当时把事务边界画在了基础设施层,结果编排层的两次独立调用根本没被包住。后来调整成编排层开事务、基础设施层接受事务句柄,这类问题就再没出现过。

6. 团队协作与长期维护

6.1 评审清单:把约束变成可执行的动作

约束要落地,必须变成评审时能一条条对着看的东西。我用的清单只有七条,多了没人认真看。

检查项通过标准常见驳回理由
新增公开接口模块入口总数仍不超过 7已超,需要合并或下沉
新增第三方依赖说明不用它的替代成本替代成本低于 200 行
跨层调用依赖方向为下行,深度不超过 4入口层直接连了数据库
状态写入全项目唯一写入点在任务里又写了一遍
新增配置项说明何时会取不同值永远是同一个值
新增抽象层有至少三处重复作为依据只有一处调用
新增目录有明确职责边界又是 utils

这七条我贴在评审模板里,每一条对应一个勾选框。刚开始推行时有人觉得繁琐,两周后就习惯了——因为它挡掉的问题,本来会在三个月后以 bug 的形式回来。

6.2 文档的最小集

文档的复杂度比代码更容易失控,因为它没有编译器帮你检查。SIMPL 的文档策略是“三份,一份都不能多”。

第一份是 README 的第一屏,必须回答四个问题:这是什么、怎么跑起来、目录怎么读、出问题找谁。这四行答不上来的项目,新人上手成本至少翻倍。

第二份是架构决策记录,只记“为什么这么选”,不记“怎么用”。比如“为什么领域层不直接用 ORM”,写清当时的备选方案和被否掉的理由。这种东西半年后价值极高,因为它防止新来的人把已经论证过的问题重新论证一遍。

第三份是排障手册,就是前面那张速查表的项目定制版。每次线上出问题,处理完往里加一行。一年下来它就是最有价值的文档。

除此之外的文档,比如详细的接口文档、设计说明书,我倾向于让它跟着代码走——接口文档从类型注解生成,设计说明写进代码注释的顶部。手写文档和代码分离的那天起,就开始腐化了。

6.3 演进路径:从约束到习惯

SIMPL 落地有个顺序问题,一步到位基本会失败。我建议的路径是分三步,每步间隔一到两个月。

第一步先做减法:删空目录、删没实现者的接口、删永远为真的开关、删没用上的依赖。这一步不改任何逻辑,风险最低,收益最直接——通常能砍掉一到两成的代码量。

第二步做边界:把领域层从框架里剥出来,让它不依赖任何外部库。这一步工作量最大,也最容易出 bug,建议配合测试一起做,每剥一个模块跑一次全量测试。

第三步做约束:把评审清单挂上去,把复杂度预算写进 README,把排障表建起来。这一步是让前面两步的成果不被后来的改动侵蚀。

三步走完大概需要三到六个月。我带的项目走到第二步时,新人的上手时间从一周降到了两天;走到第三步后,线上事故的定位时间从平均四小时降到四十分钟以内。这些数字不是靠什么高深技术实现的,只是把“简单”当成了需要维护的东西而已。

最后一个我自己反复用的判断方法:当你犹豫某个东西该不该加的时候,先想它被删掉时的场景。如果想象不出什么情况下会删它,那它大概率不该加——因为这意味着它会一直留着,而所有“一直留着”的东西,最终都会变成别人读代码时的障碍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 7:34:53

极简后端骨架SIMPL:唯一入口、隐式布局与最小依赖

做基础架构这行十来年,我经手过不少“极简框架”,大多数最后都变成了“极简到只有作者自己会用”。所以第一次看到 SIMPL 这个名字的时候,我是带着偏见的——又是个把 Simple 少写一个 e 就想上热搜的项目。真正让我改变看法的是它解决的问题…

作者头像 李华
网站建设 2026/10/27 7:33:20

网络协议梳理实战:从抓包到TCP/HTTP排障

上周有人发来一个 pcap 文件,说他们的服务每隔 30 秒就断一次连接,日志里只有一行干巴巴的 timeout。我打开抓包文件翻了两屏就看到了问题:客户端发出的 SYN 一直没等到回包,但目标端口在同一台机器上是通的——中间隔了一层安全组…

作者头像 李华
网站建设 2026/10/27 7:33:20

Nano Banana 2与LiblibAI轻量化AI开发实战指南

1. Nano Banana 2与LiblibAI工具解析Nano Banana 2作为新一代轻量化AI工具包,近期在开发者社区引发广泛讨论。其核心优势在于将常见AI模型推理功能封装为开箱即用的模块化组件,特别适合快速原型开发和小型项目部署。而LiblibAI作为配套的模型托管平台&am…

作者头像 李华
网站建设 2026/10/27 7:33:17

L4自动驾驶实时感知的物理瓶颈与工程破局

1. 这不是技术乐观主义的宣言,而是一份L4落地现场的“感知压力测试报告”“实时感知能否撑起L4级自动驾驶?”——这句话最近在自动驾驶圈子里被反复咀嚼,像一块没 chew 完的口香糖。它表面是个技术设问,内里却裹着三重现实张力&am…

作者头像 李华
网站建设 2026/10/27 7:33:15

交换机与路由器差异:二层转发、三层寻路与选型排障

机房搬迁那阵子,我在现场遇到过一幕挺典型的争执:一位同事坚持要把新采购的接入设备换成带路由功能的型号,理由是“路由器比交换机高级,能省一台设备”。结果真上线之后,办公区里两百多台终端的内网互访全挤在一条千兆…

作者头像 李华
网站建设 2026/10/27 7:32:44

网络协议梳理实战:从分层模型到MTU/MSS与抓包排障

网络协议梳理这件事,很多人第一反应是"这有什么好梳理的,四层模型、七层模型,书上写得明明白白"。我早年也这么想,直到某次线上接口成片超时,前端说请求没到,后端说没收到包,网络组说…

作者头像 李华