4、基础数据结构设计:定义 Window 接口

好,咱们进入第四步。前面几章我们把轮换窗口的「是什么」和「为什么」讲清楚了,现在该动手了。我个人习惯,写任何复杂逻辑之前,先把数据结构定死。数据结构定好了,代码怎么写都不会跑偏。

说白了,轮换窗口机制的核心,就是管理一批「时间窗口」。每个窗口长什么样?需要记录哪些信息?这就是本章要解决的问题。

4.1 窗口的核心属性

一个窗口,至少得知道三件事:你是谁、什么时候开始、什么时候结束。嗯,这是最基本的。但实际项目中,光有这三个还不够。

我在做第一个版本的时候,就只定义了 id、startTime、endTime,结果上线第一天就出问题了——窗口过期了还在轮换列表里,导致用户看到一堆无效选项。所以,状态这个字段必须加上。

来看看我最终沉淀下来的核心属性清单:

属性名 类型 说明
id string | number 窗口唯一标识,用于索引和关联
startTime number (timestamp) 窗口开始时间戳,单位毫秒
endTime number (timestamp) 窗口结束时间戳,单位毫秒
status enum 窗口状态:pending / active / expired

你可能会问,为什么不用 Date 对象?我个人习惯用时间戳,原因很简单:序列化方便,比较运算快。Date 对象在 JSON 传输时还得转字符串,徒增麻烦。

4.2 定义 Window 接口

有了属性清单,接口定义就水到渠成了。我用 TypeScript 来写,因为类型系统能帮我们在编译期就发现错误——这个坑我踩过太多次了。

// 窗口状态枚举
enum WindowStatus {
  Pending = 'pending',   // 等待中
  Active  = 'active',    // 进行中
  Expired = 'expired'    // 已过期
}

// 窗口核心接口
interface Window {
  id: string | number;
  startTime: number;  // 毫秒时间戳
  endTime: number;    // 毫秒时间戳
  status: WindowStatus;
}

这段代码看着简单,但有几个细节我想强调一下:

  • id 用 string | number:别只写 string。我在项目中遇到过后端用数字 id 的场景,如果接口只允许 string,还得额外做类型转换,烦得很。
  • 时间戳统一用毫秒:JavaScript 的 Date.now() 返回的就是毫秒,保持一致能避免很多换算错误。我曾经见过同事混用秒和毫秒,结果窗口提前关闭了 1000 倍……
  • status 用枚举:别用字符串字面量。枚举能约束取值范围,IDE 还能自动补全,写起来舒服多了。

核心原则:接口定义要「够用但不过度」。只放当前业务必须的属性,不要提前设计未来可能用到的字段。我见过太多人一开始就加一堆「以后可能会用」的属性,结果代码变得臃肿,还没人敢删。

4.3 为什么需要 status 字段?

你想想看,如果只有 startTime 和 endTime,我们怎么判断一个窗口当前是否可用?每次都要拿当前时间跟两个时间戳比较,对吧?

这样做有两个问题:

  1. 性能浪费:每次轮换都要做两次时间比较,窗口多了就是 O(n) 的运算量。
  2. 逻辑分散:判断窗口状态的逻辑散落在各个地方,改起来容易漏。

加上 status 字段后,我们可以把「判断状态」的逻辑集中到一处,其他地方直接读 status 就行。这就是典型的「计算一次,多次复用」的思路。

小技巧:status 的更新时机要把握好。我一般会在窗口创建时设为 pending,在轮换调度器启动时统一检查并更新状态。不要每次读取都重新计算,那样就失去了缓存的意义。

4.4 扩展属性:实际项目中的常见需求

上面定义的是最小可用接口。但在真实项目中,你大概率还需要加一些扩展字段。我列几个常见的:

属性名 类型 说明
name string 窗口名称,用于展示
priority number 优先级,数值越小优先级越高
metadata Record<string, any> 扩展信息,比如关联的活动 ID、展示文案等
createdAt number 创建时间,用于审计
updatedAt number 最后更新时间,用于追踪变更

这些字段不是必须的,但加上之后,窗口的「表达能力」会强很多。比如 metadata 字段,我在做活动运营系统时就靠它传入了「跳转链接」「展示图片」等动态数据,避免了频繁改接口定义。

注意:metadata 用 Record<string, any> 虽然灵活,但容易失控。我建议在项目初期就约定好 metadata 的 key 命名规范,或者在文档里列出所有可能的 key。否则半年后回来,你根本不知道 metadata 里到底塞了什么东西。

4.5 完整接口定义

把核心属性和扩展属性整合起来,一个更完整的 Window 接口长这样:

interface Window {
  // 核心属性
  id: string | number;
  startTime: number;
  endTime: number;
  status: WindowStatus;

  // 扩展属性
  name?: string;
  priority?: number;
  metadata?: Record<string, any>;
  createdAt?: number;
  updatedAt?: number;
}

注意,扩展属性我都加了问号,表示可选。这样在创建窗口时,只需要传核心属性就能跑起来,扩展属性按需添加就行。

4.6 本章知识结构图

下面这张图把 Window 接口的设计思路串起来了,你可以对照着理解:

Window 接口设计结构 核心属性 • id: string | number • startTime: number • endTime: number • status: WindowStatus (最小可用接口) 扩展属性 • name?: string • priority?: number • metadata?: Record<string, any> • createdAt?: number • updatedAt?: number 按需扩展 设计原则:够用但不过度 核心属性保证可用,扩展属性按需添加 status 枚举值 pending → active → expired

这张图把接口设计分成了三层:核心属性是地基,扩展属性是可选配件,status 枚举是状态机。你写代码的时候,先搭好地基,再根据业务需要往上加配件就行。

4.7 小结

这一章我们干了件很实在的事——把 Window 接口定义清楚了。核心就四个属性:id、startTime、endTime、status。扩展属性按需添加,别贪多。