--- 摄于 2017 年 9 月 藏川线前段
写了几年(没几年)代码,大部分时间不是在用别人设计的 api 就是在考虑怎么设计自己的 api,然后让自己用得相对舒服一点点,如果不舒服,默默互道 fuck 之后,包装一层让自己舒服的 api 完事,再也不用看里面是什么了——大概是写代码的日常(不是)。
API 设计其实是个挺烧脑的事情,它应该有几个方面的表现:
大部分情况下,自己写给自己用的 API 都仅仅只是可以用而已,谈不上约束和限制,只有在写库的时候,才会碰到上述的烧脑问题,以及,如果不是静态类型系统,那就谈不上强约束类型,void**
大法其实也可以,就是用户日常不懂调用它的假设前提是什么而已(狗头
直到前几天,我才终于写了一个自己比较满意的 API 设计,一个非常小的 PR ,实现的是 jsonrpc pubsub client,功能简单到没朋友,但 Client 与 Handle 的相互转换及泛型的处理,我觉得我可以~~吹半个月~~(实际上已经过去了一个月,因为懒的缘故,现在才把东西写出来,延时吹)。
好,我开始讲设计(吹)了,pubsub client 通过一个长连接从 server 订阅某个奇怪的 topic,在发生这个事件的时候,server 推送 Notification 给 client,长连接可以基于各种奇怪的协议,比如 TCP,Websocket,IPC等。同时,因为是静态类型的原因,每个 topic 对应的通知值类型大概率是不一样的,建模时需要添加两个泛型约束,一个用于约束底层 io 类型,另一个用于约束 topic 类型,并且这两个泛型的约束时间是不一样的,一个是在建立连接的时候必须指定,一个是建立订阅关系的时候必须指定。
那么自然而然,就有了类似于代理模式的设计,有两个结构出现了,一个表达当前已经建立了长连接,一个表达当前已经进行了订阅动作:
pub struct Client<T> {
inner: Framed<T, StreamCodec>,
id: usize,
}
pub struct Handle<T, F> {
inner: Framed<T, StreamCodec>,
topic: String,
sub_id: String,
output: PhantomData<F>,
rpc_id: usize,
}
而且两者可以通过订阅和取消订阅相互转换,达到 io 复用的目的,接口就类似如下:
impl impl<T> Client<T>
where
T: AsyncWrite + AsyncRead + Unpin,
{
pub async fn subscribe<F: for<'de> serde::de::Deserialize<'de>>(
mut self,
name: &str,
) -> io::Result<Handle<T, F>>
}
impl<T, F> Handle<T, F>
where
T: AsyncWrite + AsyncRead + Unpin,
{
pub async fn unsubscribe(mut self) -> io::Result<Client<T>>
}
上面的 F
就是订阅的瞬间,需要明确返回类型是什么,同时使用了一个一般不太用得上的 Rust 语法 —— HRTB,ok,上述的 API 完成了一切我想要的核心功能:
通过 io 泛型让 client 能适应任何底层协议的实现
通过两个结构的相互转换让用户能复用 io
通过 ownership 的迁移,强制实现状态的迁移(订阅或未订阅)
通过延时绑定泛型让 Handle
能够适应任何想要的返回值
当然,上述设计也有没有覆盖的地方,理论上说,jsonrpc 的订阅是可以单连接订阅多个 topic 的,而这里的设计并没有实现,而是强制要求一个 io 只能订阅一个 topic,在上述的核心 API 设计里,并不是不能实现,而是当前来说,暂时不需要,为了简化代码,而遭到了遗弃。实现的方法很简单,订阅接口上将 &str
变成 &[&str]
,订阅过程中出现 notification
事件返回的话,暂时缓存起来,直到转换为 Handle
结构,才允许释放给用户,相应的,需要缓存多个 sub_id,用来取消订阅。
ok,我吹完了,讲道理,写了几年代码,终于实现了一个相对满意的设计,我觉得吧,挺不容易的(逃
请登录后评论
评论区
加载更多