这两年做 ToB,有一件事我体感很深:

复杂系统不是“写坏”的,很多时候是“接需求那一刻就注定会复杂”。

以前我也会把复杂度治理理解成“写更干净的代码、用更高级的模式”。现在回头看,那些都重要,但都偏后置。拉开差距的往往是前面的几个判断动作:这个需求是不是该进主干、我们到底在解决什么问题、变化应该落在代码还是配置。

下面这篇不是方法大全,我只是想把这几年反复踩出来、而且确实帮我拦过不少坑的几条判断习惯摊开。

先说一个小事:一次“差点进主干”的导出需求

前阵子一个新人接到一线需求,要做一个“特异化数据导出”。他已经准备在大类里加逻辑了,被我拦下来了。

我只问了一个问题:这个需求是高频通用,还是一次性割接?

答案是一次性。

如果是这样,我宁可走一次运维通道,用受控脚本交付,也不愿意把临时逻辑合进主干。原因很现实:一次性需求进主干之后,维护成本是永久的,而且最后通常没人记得“当初为什么这么写”。

你可能会担心安全问题,这个担心非常对。真正的关键不是“用不用 Python”,而是有没有治理闭环。我们内部的底线是:

  1. 研发不直接连线上主库,必须走运维平台或受控执行器。
  2. 先在影子数据 dry-run,看影响范围和校验结果。
  3. 执行有审批,过程留脚本版本、参数、执行人和时间窗。
  4. 高风险操作必须有回滚方案,最好保证幂等。

所以“跑一下脚本”这句话本身没错,错的是把它理解成“随便跑”。

需求做完不难,抽象做对才难

另一个常见场景是“加字段”。

有次产品提需求:给资产加一个新的分类字段。

如果只看当前票据,这事两小时能做完。不过我当时卡住没做,先追问了一句:这是为了“业务组”本身,还是为了“可扩展分类”?

因为一旦今天按业务组分、明天按地域分、后天按部门分,系统就会进入“无限加字段”模式。你每次都交付得很快,但结构会越来越重,最后谁都不敢动。

这个坑我踩了不止一次。

后来我们改成了标签系统(EAV 思路),把“字段”升级成“能力”:客户自己定义标签、自己打标、自己组合筛选。这样一来,不是只解了一个字段,而是解了一类需求。

当然,这类方案一定会被追问性能和一致性,我也很认同这种追问。比如“组合筛选并分页时,同步延迟怎么办?检索组件不可用怎么办?”

我们的处理是:

写侧仍然以主事务存储为准,保证标签变更的确定性;读侧通过异步事件同步到检索侧;当同步延迟超阈值时显式告诉用户“数据更新中”;检索侧不可用时自动降级到主库受限查询,至少保证核心链路可用。

它追求的是可解释、可监控、可降级的最终一致,并不要求完美同步。

把变化放进配置,把风险关进笼子

我们还有一类复杂度来自外部集成。比如对接多种第三方数据源,每家格式都不一样。

一开始第一反应是写适配器类。前几个还行,数量一多就会发现系统已经变成适配器博物馆。

后来我们改成元数据驱动:把字段映射、转换规则放到配置里,核心代码只负责解析和执行规则。接入新数据源时,尽可能做到“配规则即可上线”。

但配置化并不等于放飞。非通用逻辑我们通过插件机制下放,核心系统只定义标准和生命周期,不承担插件内部实现。

这里有个经常被忽略的问题:如果外部团队写的插件死循环或内存泄漏,会不会把主系统拖垮?

如果只是进程内隔离,风险仍然很大。我们后来倾向更强隔离级别,把资源配额、超时、并发都控制住,再配合熔断和舱壁。你可以让插件失败,但不能让核心系统陪葬。

上面三段故事串起来,其实就是一棵需求到达时的判断树:

flowchart TD
    START["新需求到达"] --> Q1{"高频通用<br/>还是一次性割接?"}
    Q1 -->|"一次性"| A1["走运维通道 / 受控脚本"]
    A1 --> OUT1["不进主干"]
    Q1 -->|"高频通用"| Q2{"解决一个实例<br/>还是一类需求?"}
    Q2 -->|"一个实例"| WARN1["无限加字段模式<br/>结构越来越重"]
    Q2 -->|"一类需求"| A2["抽象成能力<br/>标签系统 / EAV"]
    A2 --> Q3{"变化落在代码<br/>还是配置?"}
    Q3 -->|"代码"| WARN2["适配器博物馆"]
    Q3 -->|"配置"| A3["元数据驱动 + 插件隔离"]
    A3 --> OUT2["进主干:先抽象,再实现"]

说到 DDD:我们做的是“轻落地”,不是“教科书落地”

我们落 DDD,初衷很务实——术语完不完整其次,主要是旧系统已经暴露了两个明显信号:

  1. 代码看得懂技术动作,看不懂业务意图。
  2. 服务虽然拆了,改需求还是要跨服务连环改。

我们没有大改工程目录,外壳仍是 MVC,但逻辑上做了几件关键事:

  1. 用事件风暴重画流程,先把边界说清楚,再谈服务拆分。
  2. 推充血模型,把状态流转和校验放回 DO,Service 只做编排。
  3. 用 Repository 做依赖倒置,Domain 不再直接依赖 DAO。
  4. 把 DO 和 PO 分开,用对象映射工具降低转换成本。

这套方式的好处是团队上手成本不高,但边界感明显增强。

另外我们在聚合持久化上吃到了文档型存储的红利。很多团队在关系型数据库上实现聚合根会被级联保存和事务细节拖住,而文档模型下“一个文档一个聚合”天然顺手很多。这不是说某种数据库一定更好,而是它和当时的聚合形态更匹配。

还有一个小习惯:定时任务不写业务逻辑

这件事看起来很小。但确实能少很多事故。

我们把定时任务当成一种输入适配器,角色类似 Controller。它可以触发应用服务,但不应该自己长出业务规则。轻量兜底任务可直接调 Service,重型任务尽量走异步消息链路,统一进入应用服务。

这样至少保证一件事:同一个业务规则只有一份,不会白天 API 一套、夜里 Job 一套。

还有一个体感:结构清楚以后,AI 也不容易乱写

现在大家都在用 AI 写代码,我自己的感觉是:结构越清楚,AI 越靠谱。

这件事和前面讲的其实是一回事。边界清楚、模型清楚、变化落点清楚以后,AI 不太容易乱调别的域;领域层越干净,它生成的逻辑和单测也越稳。像 DTO、PO、Converter 这种体力活,确实很适合交给 AI,但前提还是人先把结构说清楚。

最后留下来的几个判断

这些年做下来,有一条判断一直没变:先判断,再编码;先抽象,再实现。

写好代码当然重要,但那只是中段动作。很多系统分水岭往往在你写第一行代码之前。