rust中间层设计,告别tower,RPITIT值得你拥有更简洁的方案

前言

rust中最常用的层次设计框架,当属tower。被广泛集成在各种框架中,比如rust中几乎所有web服务的都用到的hyper

tower框架,初看很简单,只有ServicLayer两个抽象,再细一看,what?这是21世纪程序员能搞出来的设计?

但仔细一琢磨,好像也只能这样干,在很长的时间里,大家只能捏着鼻子认了。(每次用这个东西,我都骂骂咧咧,你让我写,我也写不出更好的)

但是上个月rust在23年最后一个版本推出了RPITIT。这是个啥东西哪?我们下文再说。这个特性稳定后,中间层的设计将会非常方便。

tower

俗话说,没有对别就没有伤害,先看看tower中service是怎么抽象的,下面这一坨我都不想逐一字段讲是干嘛的。

整个中间层的设计和异步模式耦合严重,一眼看上去根本不能突出重点。徒增大量工作。

rust 复制代码
pub trait Service<Request> {
    type Response; //返回值类型
    type Error; //错误类型
    type Future: Future<Output = Result<Self::Response, Self::Error>>; //异步类型
    
    //询问是否准备ok
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>; 
    //调用
    fn call(&mut self, req: Request) -> Self::Future;
}

再看看具体用法,我们这里贴一段tower自己实现的超时Service。

这几乎是最简洁的例子了,仍然需要一大堆声明,在实际体验中,对齐类型就非常费劲,完全不符合我心目中整洁的rust形象。

rust 复制代码
#[derive(Debug, Clone)]
pub struct Timeout<T> {
    inner: T,
    timeout: Duration,
}

impl<S, Request> Service<Request> for Timeout<S>
where
    S: Service<Request>,
    S::Error: Into<crate::BoxError>,
{
    type Response = S::Response;
    type Error = crate::BoxError;
    type Future = ResponseFuture<S::Future>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        match self.inner.poll_ready(cx) {
            Poll::Pending => Poll::Pending,
            Poll::Ready(r) => Poll::Ready(r.map_err(Into::into)),
        }
    }
    fn call(&mut self, request: Request) -> Self::Future {
        let response = self.inner.call(request);
        let sleep = tokio::time::sleep(self.timeout);

        ResponseFuture::new(response, sleep)
    }
}

#[derive(Debug, Clone)]
pub struct TimeoutLayer {
    timeout: Duration,
}

impl TimeoutLayer {
    /// Create a timeout from a duration
    pub fn new(timeout: Duration) -> Self {
        TimeoutLayer { timeout }
    }
}

impl<S> Layer<S> for TimeoutLayer {
    type Service = Timeout<S>;

    fn layer(&self, service: S) -> Self::Service {
        Timeout::new(service, self.timeout)
    }
}

RPITIT

RPITIT是rust在1.75版本中发布的最新的特性(传送门),功能是 允许在trait中使用impl trait

举个例子:在下面的代码中,我们返回一个迭代器traitimpl Iterator,不需要指定它是内部type,也不需要指定它是泛型。就像在普通方法那样加一个impl参数,非常的好用。

rust 复制代码
trait Container { 
    fn items(&self) -> impl Iterator<Item = Widget>; 
}

那异步特征也就是Future,同样可以被这样使用,

当然,rust团队给了一个语法糖,可以用async fn代替-> impl Future<Output=XX>

我们对上面的Service进行一下重新抽象,会发现变的简单很多:

rust 复制代码
trait Service<Req,Resp>{
    async fn call(&self, req: Req) -> Resp;
}

实现一个简单的洋葱模型

tower就是一个典型的洋葱模型,就是一层套一层。调用的时候一层一层进去,再一层一层出来。

为了实现一个简单的洋葱模型,光有service是不行的,我们也抽象一个类似tower的Layer:

rust 复制代码
trait Layer<S,T>{
    fn layer(self,svc:S)->T;
}

有了Layer之后,就可以构造一个组装器:

rust 复制代码
struct ServiceBuilder<S>{
    inner: S,
}

impl<S> ServiceBuilder<S>{
    //创建
    fn new<Req,Resp>(inner:S)->Self
    where S:Service<Req,Resp>
    {
        ServiceBuilder {inner}
    }
    
    //用Layer特征来组装
    fn layer<Req,Resp,L,T>(self,l:L)-> ServiceBuilder<T>
    where S:Service<Req,Resp>,T:Service<Req,Resp>,L:Layer<S,T>,
    {
        let inner = l.layer(self.inner);
        ServiceBuilder {inner}
    }
    
    //构建完成,将Service吐出来
    fn build<Req,Resp>(self)->S
        where S:Service<Req,Resp>
    {
        self.inner
    }
    
    //允许直接调起执行
    async fn call<Req,Resp>(self,req:Req)->Resp
        where S:Service<Req,Resp>
    {
        self.inner.call(req).await
    }
}

实现一个日志中间层,和构建器

rust 复制代码
struct LogMiddle<S>{
    svc:S,
    log:String
}

impl<S,Req,Resp> Service<Req,Resp> for LogMiddle<S>
where S:Service<Req,Resp>
{
    async fn call(&self, req: Req) -> Resp {
        println!("start {} --->",self.log);
        let resp = self.svc.call(req).await;
        println!("end   {} <---",self.log);
        resp
    }
}

struct LogMiddleLayer{
    log:String,
}
impl<S> Layer<S,LogMiddle<S>> for LogMiddleLayer{
    fn layer(self, svc: S) -> LogMiddle<S> {
        LogMiddle{svc,log:self.log}
    }
}

再来一个超时的中间池和构建器

rust 复制代码
struct Timeout<S>{
    svc:S,
    sec:u64
}
impl<S,Req,Resp> Service<Req,Resp> for Timeout<S>
    where S:Service<Req,Resp>
{
    async fn call(&self, req: Req) -> Resp {
        println!("timeout{} sec",self.sec);
        if let Ok(resp) = tokio::time::timeout(Duration::from_secs(self.sec), self.svc.call(req)).await{
            return resp;
        }else{
            panic!("timeout")
        }
    }
}
struct TimeoutLayer{
    sec:u64
}
impl<S> Layer<S,Timeout<S>> for TimeoutLayer{
    fn layer(self, svc: S) -> Timeout<S> {
        Timeout{svc,sec:self.sec}
    }
}

调用

在真正的调用之前,先为service实现lambda表达式支持,方便直接写service实现。

rust 复制代码
impl<Req,Resp,F,Fut> Service<Req,Resp> for F
    where F:Fn(Req)->Fut,
        Fut:Future<Output=Resp>,
{
    async fn call(&self, req: Req) -> Resp {
        let future = self(req);
        let resp = future.await;
        return resp
    }
}

如下,进行一个简单的组装和调用:

rust 复制代码
#[tokio::main]
async fn main() {
    ServiceBuilder::new(|req:u64|async move{
        println!("exec {} second",req);
        tokio::time::sleep(Duration::from_secs(req)).await;
    })
        .layer(LogMiddleLayer{log:"log middle".into()})
        .layer(TimeoutLayer{sec:2})
        .call(1).await;
}

如此,是不是简单方便了很多。

尾语

  1. RPITIT 不是银弹,

    当前还有很多缺陷。它不是object safe,所以不能动态调用。也不能添加边界,比如-> impl Future<Outpt=XX> + Send 这是不允许的,当然你一定要加,可以用#[trait_variant::make]

  2. #[async_trait] 还要不要用?

    我的建议是,先用着,让子弹飞一会。但是,如果你和我一样属螃蟹的,可以直接用最新特性。老代码做个兼容。

  3. tower还用不用? 如果有框架上的依赖,比如你用hyper,或者势单力薄,写不了那么多中间层,那接着用。反之,你还是不要折磨自己,远离tower,自己实现一套。当然如果有一天tower升级到这个特性了,那当我没说。

最后,祝大家新年快乐,也祝自己不被裁员(回家前被通知组织调整)^(* ̄(oo) ̄)^

相关推荐
晨米酱17 小时前
JavaScript 中"对象即函数"设计模式
前端·设计模式
数据智能老司机1 天前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机1 天前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机1 天前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机1 天前
精通 Python 设计模式——性能模式
python·设计模式·架构
使一颗心免于哀伤1 天前
《设计模式之禅》笔记摘录 - 21.状态模式
笔记·设计模式
数据智能老司机2 天前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
数据智能老司机2 天前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
烛阴2 天前
【TS 设计模式完全指南】懒加载、缓存与权限控制:代理模式在 TypeScript 中的三大妙用
javascript·设计模式·typescript
李广坤2 天前
工厂模式
设计模式