工程中的‘不强求’:如何避免过度设计,让系统保持简单
有些事不再去强求。第一次意识到这句话跟技术工作有关是在一次架构回退之后。当时我们要做一个内部数据处理工具核心动作非常简单定时拉取一批文件、做字段校验、写入数据库然后触发一个状态通知。业务刚起步我们给出的方案却很“宏大”任务编排引擎、分布式调度、统一执行器、动态配置、审计日志甚至前端可视化流程设计器。理由是以后一定会有很多任务类型业务版图还会继续扩大。结果上线之后业务方最常用的方式是把一个脚本挂到定时器上甚至手动执行一次。我们没等到想象中的复杂场景反而先等到了一套越来越难维护的抽象层改一个字段格式要动模板、动执行器、动状态机还要重新梳理几个模块之间的调用链。复盘时我们聊到一句话我们不是在解决业务问题是在强求系统承载那些并不确定存在的未来。这件事后我做技术决策多了一条原则有些事不再去强求。这里说的“不强求”不是不动脑、不做抽象、不优化、不追求质量而是在任何一个决策点要先分清楚“我能把系统做得复杂”和“当下的业务是否真的需要这种复杂”。工程能力的上限不止看你把多少功能做进去也看你能不能承受住不做某些事的压力。1. 先理解清楚工程里的“强求”到底是在强求什么“强求”在工程里最常见的形态不是某个人特别倔强而是一段看起来合理的设计过程。需求还不完整但为了“架构完整性”先把接口设计成层层可扩展的抽象基类业务只运行在一个机房但为了“未来上云”先引入多活和集群路由单日调用量只有几千次但为了“高并发准备”先铺开复杂的请求链路和降级策略。我们把这种投入叫作“提前设计”“架构预留”“能力储备”但从投入产出看它更像是一种对系统的单方面要求——系统还不具备支撑复杂模式的事实基础我们却已经让它提前承担一个并不存在的未来。这里有一个很容易被忽略的问题软件系统的复杂度不是免费的。每多一个抽象层、多一个中间件、多一个动态配置系统的可理解性、可调试性和可预测性都会同步下降。它不会让你立即崩溃但会在某个时间点变成一种系统性负债。这个负债的利息是每次排查问题时需要多翻的代码是每次上线时需要额外验证的前置条件是每个新人加入时需要补的知识也是业务调整时你不敢轻易动旧模块的心理压力。所以“不再强求”不是一个消极选项它更像是把资源从“未来的可能性”转向“当下的确定性”。你不再执着于让系统显得周全而是把注意力放在它现在为什么存在以及它真正要稳定交付什么。工程实践中的“强求”通常有一个共同特征投入与收益脱节。你花大量时间做了一个通用能力但这个通用能力并没有命中真实用户。真正有用的通用能力往往是先解决一个具体问题过程中发现了可以复用的部分然后在第二个、第三个使用场景出现之后逐步抽象出来的。反过来在没有场景的情况下先建抽象最后大概率是让一两个具体业务去迁就抽象然后进入持续修补的循环。一个系统能不能稳定运行设计方向是否正确本质上取决于它和当前阶段的适配程度而不是它覆盖了多少种可能性。很多时候我们觉得“技术能力不够”其实不是能力不够是方向不对——把大量能力花在了一个系统不常去的地方。2. 三种最常见的高投入错误在项目里我见到最多的“强求”基本可以归为三类。它们表面目标不同本质上都是拿高复杂度去交换低确定性。2.1 把“简单需求”硬拗成“统一能力”我见过不少团队业务接口只有几十个用户量是内部团队几百人却一上来就规划接口网关、统一鉴权、统一权限模型、统一异常码、统一消息推送、动态配置中心。听起来非常“平台化”但实际开发时会发现麻烦事全部堆在这套“统一”里某个接口要返回文件流统一响应包装器需要单独改造某个业务需要自定义鉴权逻辑统一权限模型变成绕不开的墙不同模块对错误码的定义天然不一致为了保持“统一”每次新功能都要先去改总表。我们把这种工程习惯叫“先统一再说话”。它不是基于重复现象做的抽象而是为了避免混乱故意搭一个看起来更有序的结构。可现实是真正的混乱来自业务理解不够不是接口不够统一。更合理的顺序是先允许局部存在当多个局部开始重复时再讨论统一。统一应该是一个自然演进的结果而不是提前定义的目标。判断标准也很简单加一个新需求时你需要改动的层数是否超过两层。如果改了 A 还得同时改 B 和 C那这套“通用”大概率已经在帮倒忙。2.2 为还没发生的爆发流量提前优化第二类强求是用未来的流量标准要求今天的代码架构。流量每天几千就开始设计十万并发下怎么平滑扩展一条数据库查询 50ms就急着把查询拆成多个缓存、索引和异步任务一个模块总共只有两个调用方却要求上下游全部改造成消息驱动。这些优化不是没有价值问题是你不知道那个“未来”会不会来以及什么时候来。预测本身高度不确定。你以为会爆发的业务可能一直没爆发你以为很重的入口可能长期没人用。你提前写的分片策略未必适合两年后的数据模型你提前引入的消息队列反而拉长了普通请求的处理链路。更关键的是过早优化会掩盖真正的瓶颈——如果一条查询真的慢了原因大概率是数据量变大、索引不合理、或 SQL 写得有问题而不是缺少一个复杂异步框架。比较稳妥的思路是先做基础压力测试发现真实瓶颈然后针对瓶颈做优化只有当下已经出现并发冲突、响应超时或资源不足才去引入复杂架构。未来的流量可以用扩容、限流、拆分这些成熟手段去承接不需要在今天就把系统压成一部庞大的机器。2.3 让所有代码先为“未来可维护性”服务还有一种更隐蔽的强求不是为功能服务而是为“可维护性”这个抽象概念服务。典型表现是所有模块都必须有 controller、service、mapper、dto 五层所有接口都必须继承统一基类所有表都必须有 create_time、update_time、version 字段哪怕模块只有 200 行也要套成几个包结构。表面上看起来规范实际团队调试时要穿好几层才能找到逻辑改一个字段要同步改多个文件。当“可维护性”开始降低效率时它已经不再是可维护性而是一种伪装成纪律的负担。真正有价值的规范应该集中在容易出错、影响上线安全的地方比如接口兼容性、权限控制、数据一致性。代码组织方式则建议根据模块复杂度弹性处理而不是用一个模板锁死所有场景。这三类问题背后的原因可以归成一句话我们缺乏对“不做”的评估同时把“设计完整”误当成“产品正确”。在一个快速变化的项目里提前锁定的细节越多后续调整成本越高。复杂度的引入不应该来自“我们能做到”而应该来自“问题已出现”。3. 怎样的系统才真正值得继续加码那么是不是所有复杂设计都不值得做当然不是。关键是要有一套判断方式区分“现在就应该做”和“以后再做也不迟”。我会用四个问题来压住自己“想强求”的冲动。3.1 一个可复用的判断框架目标、成本、风险、时间这套判断框架不算复杂但每次都能把问题从“感觉该做”拉回到“实际是否需要做”。四问问自己的具体问题如果回答不清目标它解决什么问题对哪个环节有可量化改进先不投入收集数据成本后续每次改动要付出多少开发、维护、认知成本先选低成本路径风险如果现在不做最坏结果是什么只要没触到底线可以推迟时间最晚必须做决策的时间点是什么不要提前消耗今天资源第一个问题目标是什么我为某个能力投入复杂度时必须说清楚它最终解决什么问题最好能用指标描述。比如“把接口响应 P95 从 2 秒降到 500ms”“把任务失败导致的人工介入次数从每周 10 次降到 2 次”。如果连指标都列不出来很可能还没搞清楚在优化什么。第二个问题成本是多少开发时间只是成本的一部分维护成本、认知成本、部署成本和迁移成本更容易被漏掉。一个抽象层写完可能只要一天但它以后会出现在每个接入方的逻辑里。即使未来的好处确实存在也得先看现在付出的认知成本是否压过了业务收益。第三个问题不做的风险有多大要区分“没有它会出问题”和“没有它效率低一点”。凡是关联到资金、合规、数据正确性、线上安全的部分即使很小也值得足够投入凡是不影响正确性、不影响安全、不影响主流程稳定性的能力大都可以往后放。“不强求”不是降低底线而是把底线建立在真实风险上。第四个问题最晚什么时间做很多架构决策都存在一个最晚决策点。业务只有几百个用户时不需要决定用户到一个亿时怎么分片业务没有跨境需求时不需要预先设计多机房同步。等数据、团队和需求都更清楚时再做时机通常更合理成本也不一定更高。把未来问题强行拉到现在决策最容易出现的后果是今天所有简单任务都为一个不确定的“更大挑战”做出各种奇怪让步。3.2 什么时候不能“不强求”这套判断主要用来约束“可做可不做”的复杂度。但工程里确实有一些事情无论项目大小都不该放松数据不能错。金额、库存、订单状态、积分、审计记录这些字段一旦错了后续任何优化都没有意义。上线前必须有可验证的验收。哪怕是内部工具也要有输入、输出和失败场景的验证方式。权限和敏感信息不能靠自觉。该做的最小权限、脱敏和审计日志不会因为项目小而自动变得无害。核心链路必须有可观测性。你需要知道一次请求走到哪一步、卡在哪、失败在哪日志和指标不是锦上添花而是底线。如果一个领域本身就是为这些高确定性环节服务的比如支付、账务、医疗健康记录那么该强求的事务一致性、审计和合规能力反而必须是第一优先级。我们常说复杂度不是原罪用错地方的复杂度才是。4. 已经过度建设了怎么温和地退回来如果你已经身处一个被强求出来的复杂系统里怎么办直接推倒重来太危险我通常会走一条相对稳妥的“退层”路径。4.1 先量化再缩小边界不要凭感觉删抽象。第一步是给系统加可观测性统计每个抽象层、每个统一接口的实际调用量、耗时、错误率和依赖关系。很多通用能力调用方并不多有些甚至已经被新业务绕过。这些数据可以帮助我们判断哪些是最重、最不值得保留的部分。可以试着把请求路径临时记下来对着调用拓扑图做决策。比如某个能力设计时有 6 种使用场景实际只有 1 种那先不用管那 6 种场景的通用性优先保证这一种场景的高效稳定。若出现报错排查顺序也是先看直接链路再看通用中间层最后看业务代码而不是默认先去翻抽象配置。4.2 用兼容层接管外部契约大规模重构最怕影响调用方。如果外部系统已经依赖现有接口不要直接改接口字段。更稳妥的做法是保留接口契约新增一个薄兼容层把外部请求直接映射到内部服务逻辑绕过原来那个很复杂的中间层。换句话说外部可以先看不出任何变化但内部已经不再依赖原来的统一层。等兼容层稳定运行一段时间再把旧路线下线。这样既降低了风险也给团队留出观察和回退的窗口。4.3 剥洋葱式地拆掉无用层接下来可以按“从外到内”的顺序拆掉无用层。先给新调用路径做灰度观察核心指标确认稳定后把旧的转发、校验、日志、动态配置逐步摘除每摘一层都跑一遍完整回归同时把原先散落在中间层的校验规则、权限检查、审计逻辑显式落到真正该负责的模块里。这里有一个很容易踩的坑为了简化很容易把原本存在的安全校验一起删掉。正确的“简化”是把逻辑放到更靠近数据源和业务模型的位置而不是直接丢弃。每一步都要对着原来的规则清单逐项确认这个逻辑现在由谁负责如果没人负责就不能上线。简化不是把现有逻辑删掉而是把逻辑放到它该在的地方。持续这样操作系统会慢慢变成一个“调用方直接面对业务模块”的结构中间少掉很多不必要的跳转。这个过程不浪漫也很考验耐心但它比一次性重构要安全得多也更容易被团队接受。5. 把“不再强求”变成一套可复用的迭代习惯如果只是把“不再强求”当成一次复盘后的感悟它很快就会失效。更有效的方式是把这套判断固化到每一次设计和代码评审里。5.1 默认先简化指标证明后再加复杂度我会在项目里默认一段很简单的基线先跑通最小可用路径然后按照“必要 被验证”的方式逐步增加复杂度。不是不愿意做抽象而是要求每个抽象都有证据不是不允许优化而是要求优化建立在真实指标和瓶颈上。如果有人在评审里提出“加一层抽象以备未来”我会请他补上是哪个未来为什么判断它一定会发生如果没有这个抽象到时候迁移成本是多少通常问到这里大多数“为了将来”的设计都会回到自己本来该待的位置。5.2 在代码评审里设置“强求检查”清单做设计评审时可以尝试用下面这些对话来替代“我感觉还是多封装一层好”这个设计会让复杂度出现在哪里是出现在业务丰富度上还是出现在通信和框架规则上去掉某个通用层立刻会损失什么如果损失的是业务正确性那就保留如果损失的是一个统一的观念那就可以再想想。新模块是围绕真实数据、权限和接口做的设计还是围绕“看起来规范”做的设计有没有更简单的方案可以在性能可接受的前提下先把业务跑起来这些问题不排斥复杂只是要求复杂给出理由。时间久了团队会有一个共识复杂度的代价由所有人共同承担所以决策也必须被共同检验。复杂度和成本一样只有被持续追问才不会被悄悄堆高。5.3 用“三个默认”守住一个常见项目的基准线如果需要更落地的入门基准可以试试这组“三个默认”。它适合大多数常规 Web 项目或后台系统默认同步请求只有当前调用耗时、削峰场景或上下游稳定性明显出问题时才引入异步消息默认单库单服务先在一个边界内把业务逻辑和数据模型做稳再根据真实瓶颈拆分默认错误码在模块内部定义跨团队协同真正出现后再整理全局规范。这套默认不是行业铁律。金融支付、物联网、实时数据处理这些领域需求本来就不同需要按场景调整。它真正的作用是帮团队建立一个容易修正的起点而不是让系统一开始就按最终理想态落成。某种程度上“有些事不再去强求”说的不是放弃追求而是把追求放回对的地方。对的地方是当前业务最需要被解决的确定性是团队认知成本可负担的范围是系统在被人长期维护之后仍然能继续演进的结构。敢于对不存在的未来说不敢于让系统保持简单这本身就是一种很强的工程能力。