1. 为什么你的系统总是越做越重
这几年我带过的项目里,几乎每一个出问题的系统都有同一个病根——不是技术选型不对,也不是程序员水平不够,而是需求边界从一开始就没划清楚。大家闷头写代码,写着写着就把系统写成了“瑞士军刀”,什么功能都想往里塞,到最后连当初为什么要做这个系统都快忘了。
过度设计这东西特别有意思,它很少出现在项目刚启动的时候。刚立项那会儿人人都说“先跑通再说”,可一旦核心功能做完了,各种“顺手加一个”“将来可能用得上”“这个不复杂”的需求就开始冒出来。我一个朋友做过一个内部审批系统,本来需求就三条:提交申请、两级审批、结果通知。结果半年之后,系统里多了消息推送、数据看板、附件预览、消息已读回执、多部门流转、角色权限矩阵……每一个新功能单独看都合情合理,合在一起就是一个谁也不敢动的巨型怪兽,最后光维护这套系统就得一个专职开发。
这个问题的本质是:我们一直在用“加法思维”做软件,没有人用“边界思维”去约束它。需求源源不断进来,开发来者不拒,代码像雪球一样越滚越大。真正的解法不是靠更强的意志力去“忍住不做”,而是从工作方法上就建立一套机制,让需求的边界从一开始就是清晰的、可验证的、能拒绝的。
我用的这套方法就两个核心工具:功能切片和API规约。前者解决“做哪些”的问题,后者解决“做到什么程度算完”的问题。两个工具配合起来,等于给项目装了两道闸门,一道闸在需求侧挡无效功能,一道闸在技术侧挡隐形膨胀。这篇文章是更新版,我把实际用下来的经验和踩过的坑重新梳理了一遍,比初版多了不少实战细节,希望能帮你少走点弯路。
2. 功能切片:从用户价值出发,拆出“最小可交付单元”
2.1 切片不是拆任务,是拆“可感知的价值”
很多团队做需求拆分,拆的是“任务”,比如“前端写个表单”“后端建个表”“联调一下”。这种拆法最大的问题是:任务和用户价值之间隔着好几层。你按任务拆出来的东西,做完了用户也感知不到任何变化,于是需求方就会不断往里加新东西——“既然你们都在做了,顺便把那个也做了吧”。
正确的拆法应该是按用户可感知的价值来切。什么叫可感知?就是用户能明确地说出“哎,现在我能做某件事了”。举个例子,一个电商系统,“用户能用手机号登录”是一个切片,“用户能用验证码找回密码”是另一个切片,“用户能同时用手机号和微信登录”是第三个切片。这三个切片各自独立,每一个交付了,用户都能真实地感受到系统多了一个能力。
我的习惯是拿到需求后先不动手设计,先列场景清单。把用户从头到尾用系统的动作全部过一遍,然后问一个问题:哪个环节是用户可以停下来的?用户能在“选完商品”停下,那“选商品”就是一个切片;用户能停在“提交订单”这里,那“提交订单”就是另一个切片。这样切出来的东西,天然就是可独立交付的,因为用户的使用路径本身就允许在那里暂停。
这套方法对技术选型也有帮助。每个切片独立交付,你在切片之间就有天然的“换引擎”机会。第一个切片用最土的技术栈跑通,第二个切片觉得不行再换,成本是可控的——因为你换的只是某一个切片,不是推倒整个系统。我后来带的几个项目都是这么干的,先上最朴素的技术方案,等确实验证了需求价值,再逐步演进,从来没出过大问题。
2.2 切片粒度怎么定:三种尺度的实践经验
切片粒度是个特别容易走极端的事。切得太粗,一个切片做两个月,中途几乎无法反馈;切得太细,每个切片就几十行代码,光开会评审的时间就比写代码还多。我试下来觉得分三种尺度比较合理:
- 故事级切片:一个开发在1~3天内能完成,交付后用户有明确的感知变化。这个尺度适合日常迭代,是团队里的主要工作单元。
- 能力级切片:由多个故事级切片组合而成,比如“完整的订单流程”,通常在一个迭代(1~2周)内完成。这是和产品经理对齐颗粒度的主要单位。
- 目标级切片:对应一个阶段性的业务目标,比如“支持新用户从注册到首次下单”,可能要两三个迭代。这种切片一般不直接用于排期,更多是用来划定范围和控制节奏。
我见过不少团队把精力花在争论切片粒度上,其实没太大必要。同一个功能,你觉得“发通知”是一个切片,我觉得“发短信”“发邮件”“发站内信”各算一个切片——都对,关键是判定标准要一致。我的标准就两条:第一,能不能独立测试;第二,能不能独立验收。能独立验收的就是一个切片,不能的就继续往下拆。
另一个我比较坚持的原则是:每个切片必须有一个“做完”的定义,而且要白纸黑字写下来。不是“联调通了”就叫完成,应该具体到“用户输入手机号点获取验证码,60秒内收到短信,输入正确验证码后能登录进系统首页,登录态保持7天有效”。这个定义写清楚了,开发和测试对“完成”的理解才不会跑偏,需求方也没法到时候说“我不是这个意思”。
2.3 反向切片:明确定义“这版不做什么”
功能切片这套方法里,我后期觉得最有用的其实是它的对立面——反向切片。就是明确列出“这版明确不做什么”,哪怕这些功能未来确实要做。
为什么一定要这么做?因为需求方心里其实没那么清楚自己要什么,但当你明确说“这版不做X”的时候,他反而会认真想一下——这个X到底需不需要。很多时候,需求方说“我要一个导入功能”,你问“这版临时用Excel模板行不行”,他想了想说行。这时候你就防住了一个系统的复杂度大坑。
我用反向切片时,会在切片清单的最后专门开一个“明确不做”的分区,里面列的是被排除的需求。每一行都写清楚:需求描述、排除原因、可能触发它重新排期的条件。比如“消息已读回执,暂不做,等用户反馈说分不清消息到底看没看再排期”。这样做的价值在于,以后任何人质疑为什么没做某个功能,你都能拿出当时的决策依据,而不是含糊地说“当时没提”。
记住一点,反向切片不是“不干活”的遮羞布,而是和需求方一起做的风险管理。你只是在管理“什么时候做”,不是永远不做。这个区分很重要,否则需求方会觉得你在消极怠工,反而会加剧他想把所有东西都塞进来的冲动。
3. API规约:用契约文本把需求边界“焊死”
3.1 规约不是接口文档,是切片的“法律条文”
功能切片解决的是“做哪些”,但光是知道做哪些还不够,你还得约束“怎么做才算做完”。我在实际项目中遇到过大量这种场景:功能切片看起来清晰,结果开发做的时候,发现“登录”这个切片的API到底返回什么字段、参数校验规则是什么、出错时返回什么错误码,全凭前端和后端现场商量。商量一次两次还行,多了就乱套,而且每个人对边界的理解都会漂。
API规约就是给每个切片立一个“法律条文”。它不是传统意义上那种写完之后就吃灰的接口文档,而是开发过程中所有交互方都必须遵守的技术契约。一份合格的API规约至少要包含四块内容:
- 数据模型:请求和响应里出现的每个字段,名字、类型、是否必填、取值范围、默认值,全部列清楚。
- 状态变化:资源有哪些状态,状态之间允许怎么流转,由哪个操作触发——比如订单从“已创建”到“已支付”,必须经过“支付回调成功”这个事件,不允许直接跳变。
- 错误语义:每种失败情况用什么错误码,要不要重试,重试的退避策略是什么。错误码不能是个数字了事,要有稳定的可读语义。
- 权限约束:哪些角色能调用这个接口,哪些字段是按角色返回不同的值。这块不写清楚,安全审计的时候你头都要大。
我特别强调“法律条文”这个说法,是因为它有约束力。规约评审通过后,任何一方的改动都需要按流程走变更评审,不能谁觉得“加一个字段不影响别人”就私自加。我见过太多次线上事故,就是因为前端以为后端返回的是数组,后端悄悄改成了对象,两边对着文档一看都有道理——但文档早就没人维护了。规约这东西,维护不维护倒不是最关键的,关键是它得是唯一的、权威的、被强制执行的信息源。
3.2 写好一份API规约的关键步骤
写API规约是个技术活,但也不是高深到只有架构师才能干。我建议按这五步走,每一步都有产出物,评审的时候对着过一遍就清楚。
第一步:识别交互方。不是只有前后端才需要规约。第三方系统、定时任务、数据运维脚本、测试脚本,凡是会调这个切片的接口的,都算交互方。每个交互方列出来,想清楚它的角色和数据权限。
第二步:定义核心数据模型。把切片涉及的实体和字段全部列出来。这里我有个习惯:字段的名字和含义,必须和使用它的业务术语一致,不允许开发自己发明缩写。比如业务上叫“下单时间”,API里就不能叫“createTime”然后解释说是“下单时间”,必须统一。
第三步:描述状态流转。这一步最能暴露需求边界是否清楚。一张状态图列出来,如果状态之间有两三条以上的“捷径”跳转,说明状态设计有问题,大概率是需求没想明白。正常业务的状态流转应该是清晰的、单向的、有明确触发条件的。
第四步:写错误语义。这是最容易偷懒的一步,也是最影响使用体验的一步。我的要求是:每个错误码必须有明确的用户侧解释和开发侧处理建议。不要写“E1001:系统错误”这种废话,要写“E1001:余额不足,用户应看到充值引导,开发者应检查账户余额后再发起扣款”。
第五步:评审并冻结。评审会必须有需求方参加。规约里有一个细节和需求理解不一致,当场就能发现——比如“支付成功”到底是指“用户点完支付按钮”还是“收到支付渠道的回调”,这种事在评审会上是必问的问题,也是需求边界最后一次被模糊的机会。评审通过后,契约冻结,后面只走变更流程。
工具方面,我试过几种,现在最顺手的是OpenAPI规范配合JSON Schema做数据模型校验。OpenAPI生态成熟,各种代码生成、Mock工具、文档工具都能接,团队上手成本低。更激进一点的可以考虑微软开源的TypeSpec,它比OpenAPI抽象层次高,声明起来更简洁,适合规约比较多的中大型项目。但工具都只是载体,真正的核心是团队有没有把规约当成“必需品”的认知。
3.3 规约重新定义“完成”:从代码能跑,到契约生效
很多团队的项目管理混乱,根源在于对“完成”的定义不同步。开发觉得“功能能跑了”就算完成,测试觉得“bug清零”才算,产品觉得“用户愿意用”才算,领导觉得“顺利上线不出事故”才算。你们都在说同一个词,但脑子里想的根本不是一件事。
引入API规约之后,我对“完成”重新做了定义:一个切片完成的标志,是它的规约通过评审并冻结,而不是代码跑通。听起来反直觉对吧?代码还没写呢,凭什么就算完成了?
理由其实很朴素的——规约冻结说明这个切片的需求边界已经锁定了,后面不会东加一块西补一块了,这才是真正可以稳定投入开发的起点。代码跑通只是“实现完成”,如果边界一直在动,实现完成就毫无意义。我见过最惨的一个项目,前后端联调了四版,因为需求一直在小步调整,每次调整都改接口,改到后面前端直接摆烂说“你们定死了我再写”。这种浪费纯粹是需求边界没锁死导致的。
所以我现在的项目节奏是这样的:先花一到两天把下一个迭代要做的切片的规约全部写完并评审冻结,然后才进入开发。开发阶段几乎不讨论需求问题,只讨论实现问题,哪个接口该返回什么,打开规约一看便知。新需求来了,先冻结到下一个迭代的切片里,绝不允许插入到当前迭代。这套节奏跑顺之后,开发效率和交付质量都肉眼可见地提升。
4. 实操复盘:一个“会员系统”从失控到回归边界
4.1 失控现场:一个“顺手”引发的一连串灾难
去年我接手过一个会员系统的维护+重构项目,被过度设计坑得够呛,拿来复盘特别有代表性。这个系统的原始需求特别简单:用户付费买会员,会员有效期内能看付费文章。就这么一句话的需求,系统最终长成了什么样呢?我接手的时候,代码库里躺着会员等级、积分体系、签到奖励、连续签到翻倍、分享得积分、积分兑换优惠券、会员日专属折扣、好友邀请得天数、生日双倍积分……光是会员状态就分“未激活、体验中、付费中、已过期、已退款、已封禁、已注销”七种,每种状态还有一堆组合关系。
这些东西是怎么来的呢?我翻了提测记录和聊天记录,发现每一个功能几乎都以同样的方式被加入的——起初就是一句“顺手做一下,不复杂”。会员等级,是产品经理说“用户以后可能会想要身份感,顺手做个等级呗”;签到奖励,是运营说“别的平台都有签到,我们也顺手加一个”;积分体系,是老板说“积分以后能对接很多活动,现在先建个表放着”。每一个“顺手”都有人拍板,每一个“不复杂”单独看确实也不复杂——但它们合在一起,就变成了一个复杂到没有人能说清楚“这个系统到底是干嘛的”的怪物。
更可怕的是,这些功能之间还互相耦合。签到能得积分,积分能兑会员天数,会员等级能加速积分累积,会员日双倍积分还能叠加。你动一个规则,另外三条规则跟着受影响,测试一次要回归的场景几十上百。接手那阵子,团队每天的状态就是救火,这个报表数据不对,那个活动积分没到账,谁也不敢随便改代码,因为牵一发动全身。
4.2 用切片和规约把系统“减”回原形
我做的第一件事是组织了一次全员参加的需求盘点会。过程挺痛苦的,要把系统里所有已有的功能全部列出来,逐个回答三个问题:这个功能当前有谁在用、真实使用量是多少、没了它会导致什么事。答不上来的就标记为“待验证”,不急着删,但也不允许再往里加东西。
盘点结果和我预料的差不多——系统里大概40%的功能属于“有人用但价值存疑”的灰色地带,还有20%属于“做完就没人碰”的僵尸功能。那一堆会员等级、签到、积分规则,使用数据相当惨淡。但我不主张直接删,毕竟有些功能还在跑活动,贸然下线有业务风险。我的做法是:先冻结变更,再把核心路径用功能切片重新梳理出来。
重新梳理之后,核心路径特别清楚,就三个切片:用户购买会员、用户身份验证、用户文章访问权限判定。这三个切片通过API规约重新定义了接口边界:购买会员产生的数据是什么,身份验证返回什么,权限判定的输入输出是什么。至于积分、签到、等级那些,全部通过规约隔离出去——它们的接口仍然能对外提供服务,但内部已经不再影响核心链路的逻辑,都是在主流程外侧挂的扩展点。
最有价值的一件事是,我把当初那些“顺手做”的功能的接口规约全部补了一遍,补的过程中发现了很多逻辑漏洞。比如签到积分规则里有一条“连续签到翻倍”,但翻倍因子和会员等级加成是乘在一起的,这个规则的定义从没写清楚过,线上日志显示有时候翻倍有时候不翻倍。这类问题,如果规约评审时就能发现,根本不用等上线后用户来投诉。
重构后的系统核心代码量砍掉了将近一半,测试回归的场景从上百个降到了不到三十个。更重要的是,团队终于能在一个迭代内完成从需求到上线的完整交付了,这在之前是根本不敢想的事。
4.3 一个被规约“逼出来”的重大设计缺陷
这个项目里还有一个特别典型的案例,我觉得值得单独说。原系统的订单状态定义得很随意,开发过程中前后端各自维护了一个状态枚举,前台显示“已完成”的订单,后台数据库里存的可能是“CLOSED”,也可能是“FINISHED”,还有历史数据是“SUCCESS”。三个值含义相同,但彼此不兼容,查数据统计的时候要么UNION一堆条件,要么就得做数据清洗,别提多痛苦了。
我们用API规约重新定义订单状态机的时候,把线上真实状态全部扒出来归拢了一遍。最终定义成五态:待支付、已支付、已取消、已退款、已完成。每种状态明确写出触发条件:待支付超过30分钟自动取消,已支付后48小时内可以发起退款申请,退款成功后进入已退款状态。转移路径不允许出现“待支付直接跳已完成”这种跳跃,认为这种跳变不符合业务逻辑。
这个状态机定义出来以后,开发实现就变得特别机械——不再需要“看情况”处理各种历史污数据,只要按状态机的转移表写代码就行了。测试用例也好设计了,把每一条合法的转移路径和不合法的跳变组合列出来,就是一张完整的测试矩阵。如果一开始就做了这件事,后面那一堆状态混乱的幺蛾子根本不会发生。
5. 常见问题与排查技巧实录
我把自己和身边团队用这套方法时遇到过的典型问题整理成了一张速查表,遇到的问题基本都是下面这些,你可以直接对照着查。
| 典型症状 | 根本原因 | 排查思路 | 解决建议 |
|---|---|---|---|
| 切片切得太碎,接口数量暴涨,联调成本飙升 | 把任务当成了切片,而不是按用户价值切 | 检查每个切片是否对应一个用户可感知的“完成动作” | 以用户能停下来验收为标准重新合并切片 |
| 规约写了,但开发不看,照样各写各的 | 规约游离在开发流程之外,没有强制约束 | 检查代码评审是否把关了“实现是否遵守规约” | 把规约评审放进完成的定义里,不通过不算完成 |
| 需求方不停加东西,切片清单不断膨胀 | 缺少“明确不做”的反向切片流程 | 看看“明确不做”清单是不是压根没建过 | 每个迭代都做反向切片,明确写出排除项和触发条件 |
| 契约冻结后需求方反悔,说“当时没确认这个细节” | 评审时需求方没参与,或者参与了但没表态 | 查评审记录,需求方是否只在最后签了字 | 评审流程里把需求方的反馈逐条记录并确认 |
| 接口字段经常改动,连带前端和测试返工 | 数据模型定义不完整,枚举和校验规则模糊 | 检查规约里数据模型部分是否覆盖了全部字段 | 用JSON Schema给数据模型做强制校验,不合法直接编译报错 |
| 状态流转混乱,一个订单有多个“完成”含义 | 状态定义不是源头的、唯一的,各端各自维护 | 全量扒线上数据值,梳理真实存在的状态 | 用显式状态机定义状态集和转移条件,并做迁移工具对齐历史数据 |
| 规约文档上线后没人维护,很快腐烂 | 把它当成一次性文档,而不是活着的契约 | 观察代码变更时是否同步更新规约 | 把规约放进代码仓库,走和代码一样的版本管理和评审流程 |
5.1 最隐蔽的坑:功能切片和API规约被做成了两张皮
做功能切片和API规约这件事,我最担心的一种情况是:团队把两个动作完全割裂开了。切片规划是产品经理在做的,感觉像是在开需求评审会,大家把用户故事贴在白板上,然后就没有然后了;API规约是后端开发在做的,感觉像是在写接口文档,写完了传到一个Wiki页面里,也没人真正去校验。两套东西各自为政,谁也不跟谁对齐,那这套方法的效果就大打折扣了。
正确的做法应该是:每个切片落地时,必须同时输出它的API规约。切片描述的“用户能做什么”和规约定义的“系统向外部承诺什么”,是同一枚硬币的两面。我一般在切片看板上的每一项描述后面,都会挂一个规约的链接,点进去就能看到这个切片的接口定义、数据模型、状态流转、错误说明。开发和测试看同一个入口,不会各拿一份信息对不上。
还有一个细节容易忽略:切片是讲给业务听的,规约是讲给机器听的。切片的描述不能太技术化,比如“订单模块增加一个创建订单接口”这种,业务方根本不想听,也听不懂,你提了反而让他觉得你不尊重他的输入;而规约里的字段、状态、枚举又不能太业务化,必须精确到开发人员直接能写代码的程度。这两种语言各有各的受众,别混着用,否则最后就是业务觉得你敷衍,开发觉得你啰嗦。
5.2 两个实用小技巧
最后分享两个我实测下来特别管用的小技巧,都是文档里找不到的。
第一个是给每个切片取一个“克制”的名字。名字不要太崇高,不要叫“用户增长引擎”“智能推荐平台”这种,要叫“用户手机号注册”“文章详情页缓存”“订单支付回调处理”。越具体越克制,越不容易膨胀。我见过不少过度设计都是从名字开始的——“统一消息中心”这个切片,顺理成章地就包含了短信、邮件、站内信、App推送,但实际上可能当前只需要发个通知。改叫“验证码短信发送”之后,整个系统的复杂度预期立刻降下来了。
第二个是用Mock工具把规约“跑”起来。规约评审通过之后,不要等后端实现,直接用Mock工具按规约生成一套模拟接口。前端拿这套Mock开发页面,测试拿Mock数据设计用例,后端在这套Mock的基础上做适配。等到后端真实实现完成,联调时前面已经跑得很顺了。这其实把“契约测试”前置了,谁违反了规约,在Mock阶段就能暴露出来,而不是等到联调时才发现两边藕断丝连的地方对不上。
6. 写在最后的一点个人体会
做软件这行越久,越觉得“做减法”比“做加法”难得多。代码写出来容易,删掉难;功能做出来容易,拒绝难。功能切片和API规约这套组合,刚开始用的时候你可能觉得有点麻烦——多写了切片清单,多写了规约文档,好像拖慢了速度。但我自己的体会是:这些前期投入,几乎总能从后期的返工和联调成本里加倍赚回来。它不能保证你做一个惊艳的产品,但能保证你做一个不失控的、可维护的、团队心里有底的产品。
这两年我也越来越倾向于一个理念:好的设计不是“我做了很多聪明的东西”,而是“我让该存在的存在,让不该存在的尽早止步”。需求边界挖得越清楚,代码就越坦白,架构就越诚实。希望这篇更新版的经验总结也能帮你把项目做得轻一点、稳一点、快一点。