藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

浅谈 API 设计

写了几年(没几年)代码,大部分时间不是在用别人设计的 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 完成了一切我想要的核心功能:

当然,上述设计也有没有覆盖的地方,理论上说,jsonrpc 的订阅是可以单连接订阅多个 topic 的,而这里的设计并没有实现,而是强制要求一个 io 只能订阅一个 topic,在上述的核心 API 设计里,并不是不能实现,而是当前来说,暂时不需要,为了简化代码,而遭到了遗弃。实现的方法很简单,订阅接口上将 &str 变成 &[&str],订阅过程中出现 notification 事件返回的话,暂时缓存起来,直到转换为 Handle 结构,才允许释放给用户,相应的,需要缓存多个 sub_id,用来取消订阅。

结束

ok,我吹完了,讲道理,写了几年代码,终于实现了一个相对满意的设计,我觉得吧,挺不容易的(逃

评论区

加载更多

登录后评论