第三讲:统一数据结构的四大设计原则

设计 Schema 的时候,我见过太多人一上来就开干。字段堆了一堆,接口调着调着就发现——哎?这个字段怎么既表示状态又表示类型?那个结构怎么改了 A 服务 B 服务就崩了?

说白了,这些问题根子都在设计原则上没想清楚。今天我们就聊聊四个最核心的原则:单一职责、开闭原则、接口隔离、依赖倒置。它们不是空话,是能直接帮你少踩坑的。

核心观点:这四个原则不是互相独立的。单一职责管“一个结构该干什么”,开闭原则管“怎么扩展不伤旧代码”,接口隔离管“调用方该看到什么”,依赖倒置管“谁依赖谁”。合在一起,就是一套稳定的 Schema 设计方法论。

3.1 单一职责原则:一个 Schema 只做一件事

单一职责,英文叫 Single Responsibility Principle,简称 SRP。放在 Schema 设计里,意思很简单:一个数据结构只描述一个业务概念。

我在项目中遇到过这样一个案例:有个团队把“用户信息”和“用户配置”塞到了同一个 JSON 对象里。结果前端只需要展示用户名,却不得不拉下来一整套配置数据。后来配置结构变了,用户信息接口也跟着改,两边都受影响。

我的习惯:写 Schema 之前,先问自己三个问题——这个结构描述的是什么业务实体?它会不会被多个场景共用?如果拆开会不会更清晰?

举个例子,一个典型的错误设计:

// ❌ 错误:用户信息 + 配置 + 权限混在一起
{
  "userId": "u1001",
  "name": "张三",
  "email": "zhangsan@example.com",
  "theme": "dark",
  "language": "zh-CN",
  "notifyEmail": true,
  "notifySms": false,
  "role": "admin",
  "permissions": ["read", "write", "delete"]
}

这个结构至少包含了三个职责:用户基本信息、用户偏好配置、用户权限。任何一个职责变化,都会导致整个结构变更。

正确的做法是拆成三个独立的 Schema:

// ✅ 正确:按职责拆分
// 用户基本信息
{
  "userId": "u1001",
  "name": "张三",
  "email": "zhangsan@example.com"
}

// 用户配置
{
  "userId": "u1001",
  "theme": "dark",
  "language": "zh-CN",
  "notifyEmail": true,
  "notifySms": false
}

// 用户权限
{
  "userId": "u1001",
  "role": "admin",
  "permissions": ["read", "write", "delete"]
}

你想想看,这样拆完之后,每个结构只关心自己的事。配置变了不影响权限,权限改了不波及基本信息。维护起来轻松多了。

3.2 开闭原则:对扩展开放,对修改封闭

开闭原则,Open-Closed Principle,OCP。意思是:你的 Schema 应该允许新增字段,但不应该因为新增字段而修改已有的结构。

嗯,这里要注意。很多人理解成“不能改已有的字段”,其实不是。开闭原则的核心是:通过扩展来应对变化,而不是通过修改。

我曾经维护过一个订单系统。一开始订单结构很简单:

{
  "orderId": "ord_001",
  "amount": 99.00,
  "status": "paid"
}

后来业务方说,要加一个“优惠券信息”。如果直接在原结构上加字段:

{
  "orderId": "ord_001",
  "amount": 99.00,
  "status": "paid",
  "couponId": "cpn_001",      // 新加
  "discountAmount": 10.00      // 新加
}

看起来没问题对吧?但问题是,所有消费这个 Schema 的服务都得重新编译、重新部署。哪怕它们根本不关心优惠券。

更好的做法是使用 扩展点 或 可选容器:

{
  "orderId": "ord_001",
  "amount": 99.00,
  "status": "paid",
  "extensions": {               // 扩展容器
    "coupon": {
      "couponId": "cpn_001",
      "discountAmount": 10.00
    }
  }
}

避坑指南:我曾经在一个项目里用“扩展字段”过度了,结果 extensions 里塞了几十个 key,变成了一个“大垃圾箱”。开闭原则不是让你把所有东西都塞进扩展里,而是让你有节制地扩展。能用标准字段表达的,就别往扩展里塞。

3.3 接口隔离原则:不给调用方不需要的东西

接口隔离原则,Interface Segregation Principle,ISP。放在 Schema 里就是:一个接口只暴露调用方真正需要的字段。

说白了,就是别让前端拉一堆用不上的数据。也别让下游服务收到一堆它不关心的字段。

我见过一个典型的反面教材:一个“获取用户信息”的接口,返回了 50 多个字段。前端只需要用户名和头像,却要解析整个大 JSON。后来这个接口的响应体从 2KB 涨到了 15KB,移动端用户叫苦连天。

解决方案是:按场景定义视图。

视图名称 适用场景 包含字段
UserBasicView 列表展示、头像显示 userId, name, avatar
UserDetailView 个人中心、编辑页面 userId, name, email, phone, avatar, bio
UserAdminView 后台管理、权限分配 userId, name, role, permissions, status, createTime

每个视图只包含对应场景需要的字段。调用方按需选择,谁也不多拿。

我的建议:在 GraphQL 或 RESTful API 中,可以用 ?fields= 参数让客户端指定需要的字段。但更稳妥的做法是在 Schema 层面就定义好视图,这样后端可以针对不同视图做优化。

3.4 依赖倒置原则:抽象不依赖细节

依赖倒置原则,Dependency Inversion Principle,DIP。原文是“高层模块不应该依赖低层模块,两者都应该依赖抽象”。

翻译成 Schema 设计语言就是:你的数据结构不应该依赖具体的实现细节,而应该依赖抽象的业务概念。

举个例子。假设你要设计一个“支付结果”的 Schema。如果直接依赖具体的支付渠道:

// ❌ 错误:依赖具体实现
{
  "paymentId": "pay_001",
  "wechatPayResult": "success",    // 支付结果
  "alipayResult": "success",       // 支付宝结果
  "unionPayResult": "pending"      // 银联结果
}

这个结构的问题很明显:每增加一个支付渠道,就得加一个新字段。而且调用方得知道所有渠道的名字。

正确的做法是抽象出一个“支付结果”的概念:

// ✅ 正确:依赖抽象
{
  "paymentId": "pay_001",
  "channel": "wechat",             // 支付渠道
  "status": "success",             // 支付状态
  "amount": 99.00,
  "paidAt": "2024-01-15T10:30:00Z"
}

这样不管底层是、支付宝还是银联,上层接口看到的都是统一的“支付结果”。新增渠道时,只需要在 channel 字段里加一个枚举值,不需要改结构。

关键点:依赖倒置的核心是“面向接口编程”。在 Schema 层面,这个“接口”就是抽象的业务模型。你设计的每个结构,都应该是对业务概念的建模,而不是对某个具体实现的映射。

3.5 四个原则的关系与落地

这四个原则不是孤立的。我习惯把它们看作一个整体:

  • 单一职责 决定了结构的边界——一个结构只做一件事。
  • 开闭原则 决定了结构的扩展方式——通过扩展而非修改。
  • 接口隔离 决定了结构的暴露范围——只给调用方需要的。
  • 依赖倒置 决定了结构的抽象层次——依赖抽象而非具体。

下面这张图展示了它们之间的关系:

四大设计原则关系图 统一数据结构 单一职责 一个结构只做一件事 开闭原则 对扩展开放 接口隔离 不给不需要的 依赖倒置 依赖抽象 落地建议 先按单一职责拆分结构 → 用开闭原则设计扩展点 按接口隔离定义视图 → 用依赖倒置抽象业务模型 稳定、可扩展、易维护的 Schema

在实际项目中,我通常这样落地:

  1. 先画业务边界——用单一职责把不同业务概念拆开。
  2. 再定扩展策略——用开闭原则设计扩展字段或容器。
  3. 然后切分视图——用接口隔离原则定义不同场景的视图。
  4. 最后抽象模型——用依赖倒置原则确保结构不绑定具体实现。

注意:这四个原则不是银弹。过度设计比没有设计更可怕。我见过有人为了“开闭”,每个结构都加一个 extensions 字段,结果代码里到处都是 extensions["xxx"],可读性极差。原则是工具,不是教条。用的时候多想想:这个场景真的需要吗?

好了,这一讲就到这里。四个原则听起来简单,但真正用好需要反复练习。下次设计 Schema 的时候,不妨拿这四个原则逐个检查一遍,你会发现很多问题在写代码之前就能暴露出来。