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,我们怎么判断一个窗口当前是否可用?每次都要拿当前时间跟两个时间戳比较,对吧?
这样做有两个问题:
- 性能浪费:每次轮换都要做两次时间比较,窗口多了就是 O(n) 的运算量。
- 逻辑分散:判断窗口状态的逻辑散落在各个地方,改起来容易漏。
加上 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 接口的设计思路串起来了,你可以对照着理解:
这张图把接口设计分成了三层:核心属性是地基,扩展属性是可选配件,status 枚举是状态机。你写代码的时候,先搭好地基,再根据业务需要往上加配件就行。
4.7 小结
这一章我们干了件很实在的事——把 Window 接口定义清楚了。核心就四个属性:id、startTime、endTime、status。扩展属性按需添加,别贪多。