拒绝过度设计:中小团队架构如何"留白"?

配图

2019年,我作为架构师接手过一个电商SaaS项目。当时为了彰显"技术前瞻性",我在设计初期就引入了一整套复杂的插件化机制,号称"未来支持任何形式的促销规则"。

结果呢?直到两年后项目重构,那个被我精心设计的PromotionStrategyFactory里,依然只躺着孤零零的两个实现类:Discount(打折)和Reduction(满减)。而为了维护这套复杂的抽象层,团队新人在写每一个简单业务逻辑时,都要被迫穿透三层接口。

那个周末我深刻反思:架构的扩展性,从来不是靠"预测未来"实现的,而是靠"降低犯错成本"来预留的。

对于中小团队而言,资源有限,生存是第一要务。我们不需要能抗住亿级并发的架构,我们需要的是一个**“现在够用,未来好改”**的系统。

今天我想结合这几年的实战经验,聊聊如何在不增加额外成本的前提下,给架构做"留白"。

一、 警惕"一步到位":用"三法则"对抗预测焦虑

很多资深开发在做设计时,容易陷入一种"完备性强迫症"。总觉得如果现在不把接口定义得足够通用,未来改起来会很麻烦。

但这往往是一个巨大的误区。过早的抽象,比没有抽象更可怕。 它会增加代码的认知负荷,让简单的逻辑变得晦涩难懂。

“在这个阶段,所有的抽象都是猜测。而猜测,通常都是错的。”

我现在的做法非常简单粗暴,就是严格执行**“三法则”(Rule of Three)**:

  1. 第一次做(The First Time): 只为当前的一个具体需求写代码,甚至可以硬编码,追求最快上线验证业务;
  2. 第二次做(The Second Time): 当遇到第二个类似需求时,允许一定程度的复制粘贴(Copy-Paste),不要急着重构;
  3. 第三次做(The Third Time): 当第三个类似需求出现时,这时候你已经拥有了三个具体的样本。此时再停下来,提取公共逻辑,进行抽象和封装。

真实案例: 去年我们在做一个CRM系统的通知模块。

  • 阶段一: 业务只要短信通知。我们直接写了一个SmsService,没有定义任何接口,代码直白简单。
  • 阶段二: 业务增加了邮件通知。有些同事建议马上搞个NotificationInterface,支持策略模式。我拦住了,建议直接加个EmailService,虽然有点重复代码,但半天就上线了。
  • 阶段三: 业务又要加飞书通知,且逻辑变得复杂(需要失败重试、优先级排序)。这时候,我们才引入了统一的消息接口和消息队列。

结果: 前两个阶段我们没浪费时间在设计模式上,业务跑得飞快。到了第三阶段,我们对业务痛点有了极其清晰的认知(比如重试机制比协议适配更重要),这时候做的架构升级,才是一针见血的。

二、 数据层的"留白":结构化与非结构化的博弈

代码难写可以重写,但数据难搞就真的要命了。很多架构死局,都是因为数据库表结构设计得太死板,导致后期数据迁移成本极高。

在传统关系型数据库设计中,我们习惯把所有字段都定义得清清楚楚。但在快速迭代的中小项目中,业务属性变化极快。今天商品需要"产地",明天就需要"保质期",后天又要加"网红推荐指数"。

如果每次都要去改表结构(DDL),不仅运维风险大,代码里的实体类(Entity)也要跟着改,简直是灾难。

我的实操建议:混合存储策略。

核心字段(用于索引、检索、聚合统计的)必须严格结构化;而非核心的、展示类的、多变的属性字段,请大胆使用 JSON 类型(如 MySQL 的 JSON 或 PostgreSQL 的 JSONB)。

代码示例:

-- 不推荐:为了未来可能出现的字段预留一大堆 column
CREATE TABLE products (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10, 2),
    attr1 VARCHAR(255), -- 这种预留字段最坑
    attr2 VARCHAR(255)
);

-- 推荐:核心明确,边缘模糊
CREATE TABLE products (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255), -- 核心检索字段
    category_id INT,   -- 核心关联字段
    status TINYINT,    -- 核心状态字段
    properties JSON    -- 扩展属性:颜色、尺寸、保质期等统统扔这里
);

实战效果: 我曾负责的一个物流SaaS系统,客户的运单字段千奇百怪。有的要记录"司机体温",有的要记录"车辆消毒时间"。我们将这些非核心字段全部甩进 properties JSON 字段。

这带来的好处是:

  1. 前端可配置化: 甚至不需要后端发版,前端改改配置,存进 JSON,读出来直接渲染。
  2. 查询并不慢: 现代数据库对 JSON 的索引支持已经非常好了(特别是 Postgres 的 GIN 索引),完全能满足中小规模的查询需求。

这才是真正的预留空间:把变化的复杂性,锁在这一列 JSON 中。

三、 服务边界的"留白":模块化单体优于微服务

这可能是争议最大的一点。但在中小团队,我见过太多被"微服务"拖垮的案例。

一个5-10人的开发团队,为了所谓的"扩展性",硬生生拆出了8个微服务。结果是:

  • 一个简单的需求要跨3个服务,联调花了一下午;
  • 分布式事务(Seata/TCC)搞得头皮发麻;
  • 无论改哪里,都要重新部署一堆东西。

对于中小团队,扩展性的敌人不是"单体应用",而是"高耦合"。

我强烈推崇**“模块化单体”(Modular Monolith)**架构。

我们在物理上部署为一个单体服务(一个 jar 包或一个镜像),但在代码结构上,严格按照"领域模块"进行隔离。

目录结构示例:

src/main/java/com/company/app
├── inventory (库存模块)
│   ├── api (对外暴露的接口,DTO)
│   ├── internal (内部实现,Entity,Repository,外部不可见)
│   └── InventoryService.java
├── order (订单模块)
│   ├── internal
│   └── OrderService.java
└── shared (公共基础组件)

配图

关键规则:

  1. 禁止跨模块数据库访问: 订单模块绝对不能写 SQL 去查库存表,必须调用 InventoryService 的接口。
  2. 明确依赖方向: 只能上层调下层,或者同层通过事件总线解耦。

这种架构的"留白"在于: 如果不遵守模块隔离,单体很快会变成大泥球;但如果遵守了,当某一天(大概率是1-2年后)库存模块真的撑不住了,或者需要独立团队维护了,你可以直接把 inventory 文件夹剪切出去,包一层 HTTP/RPC 协议,它瞬间就变成了一个微服务。

配图

这种"可分可合"的状态,才是最高级的扩展性。它避免了过早引入分布式系统的复杂性(网络延迟、数据一致性),又保留了未来拆分的可能性。


最后,留两个小问题供大家自查:

  1. 你现在的代码里,有多少接口只有一个实现类,且未来半年大概率不会有第二个?
  2. 你有没有为了"以后可能会用到"而引入过中间件(比如还没啥量就上了 Elasticsearch),结果大部分时间在修它的运维配置?

架构设计没有银弹,只有取舍。

如果你想给系统预留升级空间,建议下周一上班时尝试这3个动作:

  1. 清理"死代码": 删掉那些注释掉的代码块和半年没用到的"通用接口",降低系统噪音。
  2. Review 表结构: 检查最近三次需求变更,如果频繁修改表结构,考虑引入 JSON 字段重构那部分逻辑。
  3. 收敛边界: 别急着拆服务,先检查你的代码包结构(Package),看看能不能做到"订单模块删了,用户模块一行代码都不用改"。

只有轻装上阵,未来想转身时,才不会扭到腰。