六个月时,我们开一个PR要花20分钟才能搞清楚哪个slice管着预约状态。代码没坏,甚至看不出错——它只是悄悄变成了别的东西。
团队一直叫这"技术债",但不对。技术债是主动交易:先上线,后还债。我们遇到的东西更隐蔽:没人做错决定,每个选择在当时都站得住脚。但12个站得住脚的选择叠在一起,就把架构推向了没人想去的方向。
我们后来叫它架构决策退化(Architecture Decision Degradation,ADD):通过累积妥协逐渐侵蚀架构质量。没有戏剧性崩溃,只有缓慢爬行——直到sprint velocity断崖式下跌才显形。
这是一个医疗CRM日历项目的真实复盘:18个月数据、真实代码、一次3个月的重构让我们活过来。
ADD不是技术债,是"正确的死亡"
技术债是 conscious trade-off:现在ship,以后fix。ADD是无意侵蚀——即使团队每一步都选"正确",架构仍在退化。最可怕的是:没人做"坏"决定,每一步孤立看都合理,但堆叠六个月,架构面目全非。
它遵循可预测的生命周期:
• 第1-3月:slice增殖,模式不一致
• 第4-9月:实时更新撞上乐观状态
• 第10-16月:40%+ bug来自状态,团队开始讨论重写
我们在第16个月才察觉。那时已经需要3个月重构。
拖拽式医生预约日历,听起来简单。团队 upfront 做了所有"正确"决定:Redux-Toolkit 集中状态(告别prop drilling)、createAsyncThunk 处理异步、乐观更新支持拖拽。代码长这样:
// store/slices/appointmentsSlice.ts const appointmentsSlice = createSlice({ initialState: [] as Appointment[], addAppointment: (state, action) => { ... }, updateAppointment: (state, action) => { const index = state.findIndex(a => a.id === action.payload.id); ... } }) export const selectAppointments = (state: RootState) => ...
团队velocity很高,架构感觉"真干净"——而这种"干净"的感觉,是我们完全错过的第一个警告信号。
第1阶段:复杂度爬行(第3-9月)
新需求逐个抵达:WebSocket实时后端更新、拖拽时主动冲突检测、按医生/专科/日期筛选。团队从2人扩到5人,每人加自己的slice。
3个slice变成10个:
// 第3月:appointmentsSlice, doctorsSlice, timeSlotsSlice // 第9月:10个slice还在增加... // store/slices/appointmentsSlice.ts 已膨胀到800行
每个slice都"合理":实时更新需要websocketSlice,冲突检测需要conflictSlice,筛选需要filterSlice。但没人问:这些状态该放哪?
架构图开始像意大利面。一个预约拖拽操作要触达4个slice:appointmentsSlice改状态、websocketSlice发更新、conflictSlice验冲突、uiSlice管加载态。开发者在Slack问:"我该dispatch哪个action?"
第6个月的velocity数据:feature完成时间从3天→5天,bug反弹率从12%→23%。团队归因于"新成员上手慢",继续堆功能。
第2阶段:状态爆炸(第9-14月)
乐观更新和实时更新的冲突爆发了。
用户拖拽预约时,前端先乐观更新(appointmentSlice),同时WebSocket推送后端真实状态(websocketSlice)。网络延迟200ms时,用户看到预约"跳回去"——不是bug,是两个真相在打架。
团队加了merge策略:timestamp比对、last-write-wins、自定义冲突解决。代码变成这样:
// 第11月的"解决方案" const reconcileState = (optimistic, server, pendingOps) => { // 300行边界条件处理 // 包括:离线恢复、多标签页、医生同时编辑... }
这个函数没单元测试。为什么?"太依赖全局状态,mock不起来。"
第12月,bug分类显示:41%的bug与状态同步相关。不是业务逻辑错,是"哪个版本的数据为准"错。最讽刺的一个:医生确认预约后,日历显示"已确认",详情页显示"待确认"——两个组件选了不同的selector。
团队开始讨论"要不要重写"。但产品路线图排满,"等Q3有空"。
第3阶段:诊断与重构(第15-18月)
第16月,我们停掉所有feature开发,用两周做架构审计。方法很土:打印所有slice依赖图,贴在墙上,用红线标循环依赖。
发现触目惊心:
• 10个slice,7个有双向依赖
• 43个selector,19个跨slice组合
• 单个拖拽操作触发11个action,6个middleware拦截
核心问题不是"技术栈选错",是决策上下文丢失。每个slice的创建都有Jira ticket、有PR描述、有代码评审通过——但没人记录"为什么状态要拆成这样"。6个月后,当初的理由成了考古。
重构方案:不是换框架,是重新划定边界。把日历领域拆成三个 bounded context:Scheduling(预约调度)、Availability(医生可用时间)、Sync(与后端同步)。每个context内部用Redux,跨context用事件总线。
关键规则:context之间不直接读对方状态,只订阅事件。这强制我们显式定义"什么变了需要别人知道"。
重构花了3个月。第18月velocity数据:feature完成时间回到2.5天,bug反弹率降至9%。更意外的是:新成员上手时间从2周→3天——因为边界清晰了。
ADD的预警信号(我们错过的)
回头看,有五个信号足够早该警觉:
1. "感觉干净"的幻觉。早期代码的优雅感,让团队对复杂度增长脱敏。
2. slice数量增速 > 功能增速。第3-9月,功能增40%,slice增233%。
3. selector复杂度隐性增长。从selectAppointments到selectFilteredAvailableAppointmentsForDoctorInRange,命名变长就是警告。
4. "暂时"的解决方案存活超过1个月。我们的reconcileState函数,"临时方案"活了4个月。
5. 开发者开始问"该用哪个"。当选择成本超过实现成本,架构已病。
现在团队每两个月做一次"架构体检":统计slice依赖数、selector跨域率、action触发链长度。不追求数字完美,看趋势。
最近一次 retro,后端工程师说:「你们前端终于不再为'一个预约有几种状态'吵架了。」
我们还没找到预防ADD的银弹。但有个问题现在每个RFC都要回答:"6个月后,新成员能猜到这个决策的理由吗?" 猜不到,就写进ADR(Architecture Decision Record)。
你的团队有没有"没人做错决定,但架构就是烂了"的时刻?最后是怎么发现的——是velocity暴跌,还是某个深夜的线上故障?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.