Milky 的设计哲学
作为 Milky 的设计者,你对这个协议最满意的地方是什么?
是它声称的“取代了 XXX 协议”吗?不是。
是它尽善尽美地覆盖了 QQ 的特性吗?也并没有。
是它有着完善的基础设施和生态吗?还远远不够。
实际上,作为设计者,笔者对 Milky 最满意的地方,在于它从设计之初就秉持的一些理念,总结有以下几点:
对称性和一致性
在 API 和数据结构设计上,Milky 采用对称的命名结构,例如:
- 列表与单项的对称 ——
get_friend_list和get_friend_info - 私聊和群聊的对称 ——
send_private_message和send_group_message - 接收与发送的对称 ——
IncomingSegment和OutgoingSegment - 增、删、查的对称 ——
get_group_announcements、send_group_announcement和delete_group_announcement
在字段的命名上,Milky 也力求对称。在不引起歧义的情况下,表示同一概念的字段总是采用相同的命名方式。例如:
- 表示用户 QQ 号的字段均命名为
user_id - 表示群号的字段均命名为
group_id - 表示消息序列号的字段均命名为
message_seq
这种对称性的设计使得 API 更加直观和易于记忆。
可空性设计
在 Milky 的数据结构中,你可能会发现有些本来应该可空的字段被设计为不可空。这并非疏忽,而是经过深思熟虑的设计选择。首先,我们定义一个变量的三种状态:
- 有值:变量包含一个有效的非空值
- 无值:变量明确表示没有值(例如 JSON 中的
null) - 默认值:变量未被赋值,采用类型的默认值(例如数字类型的
0,字符串类型的空字符串)
在确定一个字段是否为可空时,主要的原则是:
- 如果一个字段的“无值”态和“默认值”态在逻辑上是等价的,那么该字段设计为不可空。典型的例子是群成员的
card字段:用户将群名片设置为空字符串等价于取消了自己的群名片,因此该字段设计为不可空,空字符串即表示用户没有设置群名片。 - 如果一个字段的“无值”态和“默认值”态在逻辑上不等价,抑或“默认值“态会引发歧义,那么该字段设计为可空。典型的例子是群成员的
shut_up_end_time字段:当用户没有被禁言时,该字段的默认值0会引发歧义——该用户是“解禁时间1970-01-01 00:00:00”(尽管这在今天显然不可能)还是“未被禁言”?因此该字段设计为可空,null表示用户未被禁言。
这样的设计减少了不必要的非空检查,同时给开发者喂了一颗“定心丸”:如果一个字段是可空的,那么就一定用 null 来表示“无值”;而如果一个字段是不可空的,那么就绝不会出现 null。
其实一开始只是为了避免 x.isEmpty() ? null : x 这种幽默代码而已,没想到后来越想越有趣,就变成了现在这样。
可持久化性
不知读者在进行 Bot 相关开发时,是否遇到或设计过这样的“组合式”标识符:
private|12345678|11111
group|34567890|22222
request-private-u_nk******************qg-1765647902
request-group-join-1765647976234567很显然,这样的标识符只有一个目的:将一个包含复杂信息的对象压缩成一个字符串,用于在协议中作为标识符传递。这样的设计虽然保留了信息,但往往无法持久化存储,因为它们的格式可能会随着协议的更新而变化。更典型的情况是,用户从一种 Bot 框架迁移到另一种 Bot 框架时,无法直接使用这些标识符——因为框架 A 用 | 分隔,而框架 B 用 - 分隔!
这是协议设计上一个难以调和的痛点:是抽象出复杂对象的标识符,还是提供持久化存储的能力?所幸,Milky 作为专为 QQ 设计的协议,可以最大限度的采用后者。Milky 的所有标识符在 QQ 协议层面都有其对应的 “backing field”,这些字段直接为 QQ 后端所用,并不会随着协议端的不同而发生变化。典型的例子包括:
- 消息的
message_seq:消息在同一会话中的“绝对坐标”,表示消息在该会话中的顺序位置。协议端在查询、撤回或引用消息时直接使用该字段。 - 好友申请的
initiator_uid:请求者的 UID(NTQQ 引入的不同于 QQ 号的另一种用户标识符)。协议端在批准或拒绝好友申请时直接使用该字段。 - 群通知的
notification_seq:本身是微秒级时间戳,QQ 后端直接使用该字段来标识群通知。协议端在处理群通知时直接使用该字段。 - 图片、语音等媒体资源的
resource_id:资源文件在 QQ 服务器上的唯一标识符。协议端在获取资源的下载链接时直接使用该字段。
协议端与应用端交互时直接使用这些持久化标识符,避免了因标识符格式变化而导致的兼容性问题,同时也方便了协议端的开发。
设计哲学就像底裤,把它套在外面炫耀会显得很奇怪,但绝对不能没有。笔者唯恐被人误以为是在“卖弄设计”,但又觉得有必要把这些理念记录下来,以便日后回顾和改进,于是选择在 Blog 上写下这篇文章。Milky 的这些设计哲学,正是它能够在纷繁复杂的协议设计中保持简洁和一致性的关键所在。
CC BY-NC 4.0 2025 © Wesley Young.