以 Actix 为例,探索 Rust 拓展特征在工程中如何解耦

以 Actix 为例,探索 Rust 拓展特征在工程中如何解耦

开篇

Actix 是 Actor 模型 的 Rust 语言实现

Actor 模型是并发计算的数学模型

关于 Actor 模型可简单参考:The actor model in 10 minutes

如果你看过 Actix 源码,你会发现在 actix::handler 中有这样一段代码

// ...
impl<A, M, I, E> MessageResponse<A, M> for Result<I, E>
where
    A: Actor,
    M: Message<Result = Self>,
    I: 'static,
    E: 'static,
{
    fn handle(self, _: &mut A::Context, tx: Option<OneshotSender<Self>>) {
        tx.send(self)
    }
}
// ...
复制代码

实现很简单,我们看看发生了什么。

tx 的类型是 Option<OneshotSender<Self>>OneshotSendertokio::sync::Sender 的别名。

.send(self) 方法定义于 actix::handler 即同一个模块下。源码如下:

// Helper trait for send one shot message from Option<Sender> type.
// None and error are ignored.
trait OneshotSend<M> {
    fn send(self, msg: M);
}

impl<M> OneshotSend<M> for Option<OneshotSender<M>> {
    fn send(self, msg: M) {
        if let Some(tx) = self {
            let _ = tx.send(msg);
        }
    }
}
复制代码

其中泛型M指的是 tokio::sync::Sender<T>

为什么要多定义这个方法呢?

查看源码之后可以知道 MessageResponse 一共被实现了28次。也就是说上面这段代码,把所有 handle 函数中 Option 的判断统一做了处理,降低了代码耦合。

为什么不用其他方式去实现?

下面我们通过两个尝试去探讨一下,为什么采用这种方式实现更好。

尝试1: helper 函数

假设使用 helper 函数,那么函数大概如下:

fn send<M>(tx: Option<OneshotSender<M>>, msg: M) {
    if let Some(tx) = tx {
        let _ = tx.send(msg);
    }
}
复制代码

似乎代码更少更简单了?

使用的时候只需要 send(tx, msg) 就行了。

但是这么做有以下缺点:

  • 代码分散,如果不看函数定义,根本不会知道能用这个函数,在团队协作中容易出现许多重复代码。并且因此为将来的重构埋坑。
  • 缺少语义化的函数表达,这对于团队协作来说也是不利的。

尝试2: macro_rule 声明宏

假设用声明宏来实现,效果将是惨烈的:

macro_rules! send {
    ($tx:expr, $msg:expr) => {
        if let Some(tx) = $tx {
            let _ = $tx.send($msg);
        }
    };
}
复制代码

虽然这里编译期没有阻止你,但任何人都不能猜到 $tx$msg 是什么类型,除非是你自己写的。

况且,用 macro_rule 来实现这个有种高射炮打蚊子的感觉 :)

为什么用 helper trait 更好?

trait 是 Rust 的规范,实现的功能更加强大,它可以利用泛型为多个类型复用,并且可以参与 trait bound 作为类型约束。

不过 Rust 为了防止依赖地狱,在2015年引入了孤儿原则 "Orphan Rule",使用时还是有一些限制,简而言之 traitstruct/enum 必须有一个是来自于自身的 crate。

关于孤儿原则,可参考:Little Orphan Impls

实践 - 开胃菜

众所周知,内置类型也是来自外部,想要为原始类型拓展方法可以在本地编写 trait ,举个栗子:

trait ColorExtend {
    fn is_color(&self) -> bool;
}
复制代码

我们编写了一个颜色拓展方法,用于判断该类型是否可以表达为颜色。

我们为 &str 类型实现一下:

// 让 matching 可以匹配 char
#![feature(exclusive_range_pattern)]
impl ColorExtend for &str {
    fn is_color(&self) -> bool {
        if self.len() == 4 || self.len() == 7 { 
            for elem in self.char_indices().next() {
                match elem {
                    (0, '#') => (),
                    (0, _) => return false,
                    (_, '0'..'9') => (),
                    (_, 'a'..'z') => (),
                    (_, 'A'..'Z') => (),
                    _ => return false,
                }
            }
            true
        } else {
            false
        }
    }
}
复制代码

在这之后,我们就可以这样来使用了: "#0000FF".is_color()

同样的,这个方法可以给其他类型实现,比如 String , Vec<u8> 等,甚至可以为 Option<&str> 实现。所有的泛型类型都属于一个独立的类型,这是由于 Rust 的零成本抽象,在编译器就会对泛型展开,生成独立的类型。基于此我们开始对 Actix 中的一些类型做拓展吧。

编写 Actor

先定义一个用于计数的 actor 作为例子,并为其实现一些基本特征:

use actix::{Actor, Context};

pub struct MyActor {
    pub counter: u16,
}

impl Default for MyActor {
    fn default() -> Self {
        Self { counter: Default::default() }
    }
}

impl Actor for MyActor {
    type Context = Context<Self>;
}
复制代码

获取计数 GetCounter

use actix::{Handler, Message};

/// counter add message
#[derive(Debug, Message)]
#[rtype("u16")]
pub struct GetCounter;

impl Handler<GetCounter> for MyActor {
    type Result = u16;

    fn handle(&mut self, _: GetCounter, _: &mut Self::Context) -> Self::Result {
        self.counter
    }
}
复制代码

计数增加 CounterAdd

/// counter add message
#[derive(Debug, Message)]
#[rtype("u16")]
pub struct CounterAdd(pub u16);

impl Handler<CounterAdd> for MyActor {
    type Result = u16;

    fn handle(&mut self, msg: CounterAdd, _: &mut Self::Context) -> Self::Result {
        println!("add {}", msg.0);
        self.counter += msg.0;
        self.counter
    }
}
复制代码

n 秒内计数变化 GetDelta

此处由于需要异步,所以返回了 ResponseActFuture

use std::time::Duration;
/// get counter's value change during the [`Duration`]
#[derive(Debug, Message)]
#[rtype("u16")]
pub struct GetDelta(pub Duration);

impl Handler<GetDelta> for MyActor {
    type Result = ResponseActFuture<Self, u16>;

    fn handle(&mut self, msg: GetDelta, _: &mut Self::Context) -> Self::Result {
        let init_value = self.counter; // 初始值
        Box::pin(
            async move {
                actix::clock::sleep(msg.0).await;
            }
                .into_actor(self)
                .map(move |_, actor, _| {
                  	 // 等待 future 结束后获取最新的 counter,与初始值相减即使变化值
                    actor.counter - init_value
                })
        )
    }
}
复制代码

actix::clock::sleep 并不阻塞,这是由于 actix 使用了基于 tokio 的运行时

这里的 into_actor 方法是 actix::fut::future::WrapFuture 定义的本文所提的 helper trait ,在一些库中这个思想很流行。

WrapFuture 定义:

pub trait WrapFuture<A>
where
    A: Actor,
{
    /// The future that this type can be converted into.
    type Future: ActorFuture<A>;

    /// Convert normal future to a ActorFuture
    fn into_actor(self, a: &A) -> Self::Future;
}
impl<F: Future, A: Actor> WrapFuture<A> for F {
    type Future = FutureWrap<F, A>;

    fn into_actor(self, _: &A) -> Self::Future {
        wrap_future(self)
    }
}
复制代码

这里为 Future 增加了一个 into_actor 方法,返回了一个 FutureWrap ,而 FutureWrap 的定义恰好用了上回提到的 pin_project ,有机会再细讲。

参考译文链接:为什么 Rust 需要 Pin, Unpin ?

模拟需求

假设这时候产品经理提出了这样一个需求:在 n 秒内,统计出counter值的变化,然后再在 n 秒后额外添加同等的值

/// an odd mission required by the lovely PM
#[derive(Debug, Message)]
#[rtype("()")]
pub struct DoubleAfterDelta {
    pub secs: u64
}

impl Handler<DoubleAfterDelta> for MyActor {
    type Result = ResponseActFuture<Self, ()>;

    fn handle(&mut self, msg: DoubleAfterDelta, ctx: &mut Self::Context) -> Self::Result {
        Box::pin({
            let addr = ctx.address();
            addr.send(GetDelta(
                Duration::from_secs(msg.secs)
            ))
                .into_actor(self)
                .map(move |ret, actor, ctx| {
                  	// 手动添加的函数
                    ret.handle_mailbox(|delta| {
                      	// 这也是手动添加的函数
                        ctx.add_later(delta, msg.secs);
                    });
                })
        })
    }
}
复制代码

对于 map 的参数函数,三个参数的类型分别为:

  1. Result<T, MailboxError>

  2. &mut MyActor

  3. &mut Context<MyActor>

假设我们暂时用朴素的方式写,大概会是这样:

match ret {
    Ok(data) => ctx.notify_later(CounterAdd(data), Duration::from_secs(msg.secs)),
    Err(e) => eprintln!("common handle MailboxError: {}", e),
}
复制代码

Emmm... 貌似也还好。

不过当 Err 分支需要处理一些特殊情况时(比如重置 Actor ),代码量比较多的情况下,如此往复也是不个好办法,我们封装一下 Result<T, MailboxError>&mut Context<MyActor>,让这部分代码抽离出来。

封装 Result<T, MailboxError>

首先,定义一个 trait

pub trait ActixMailboxSimplifyExtend<T> {
    fn handle_mailbox<F>(self, handle_fn: F)
    where
        F: FnOnce(T) -> ();
}
复制代码

这个方法接收一个闭包函数 handler ,在 handler 中只处理正常返回的情况,而在 handle_mailbox 中统一处理 MailboxError ,实现如下:

impl<T> ActixMailboxSimplifyExtend<T> for Result<T, MailboxError> {
    fn handle_mailbox<F>(self, handle_fn: F)
    where
        F: FnOnce(T) -> () {
        match self {
            Ok(data) => handle_fn(data),
            Err(e) => eprintln!("common handle MailboxError: {}", e),
        }
    }
}
复制代码

封装 Context<MyActor>

pub trait ActixContextExtend {
    fn add_later(&mut self, add_num: u16, secs: u64) -> ();
}
impl ActixContextExtend for Context<MyActor> {
    fn add_later(&mut self, add_num: u16, secs: u64) -> () {
        println!("counter will add {}, after {} second(s)", add_num, secs);
        self.notify_later(CounterAdd(add_num), Duration::from_secs(secs));
    }
}
复制代码

add_later 函数封装了 notify_later ,实现了在 secs 秒后自动发送 CounterAdd 消息。

封装了这两个方法之后,在遇到类似的需求时就 Don't Repeat Yourself 了。

缺陷

当然这种方式也是有缺陷的,在使用时需要手动引入声明。编译器不会寻找其的所有实现,因为就会产生依赖混淆问题,如果遇到两个重复的方法,编译器会报如下错误:

error[E0034]: multiple applicable items in scope
复制代码

所以尽量命名要避免与现有一些函数名重复,否则重构起来特别容易陷入混乱。

主函数

铺垫了这么多,看看实际结果吧。

fn main() {
    let run_ret = actix::run(async move {
        let actor = MyActor::default();
        let addr = actor.start();
        println!("=[case 1]=========================");
        let current_value = addr.send(GetCounter).await.unwrap();
        println!("init value is: {}", current_value);
        let fut = addr.send(DoubleAfterDelta {
            secs: 1,
        });

        // add during DoubleAfterDelta's Handler waiting
        sleep(Duration::from_millis(200)).await; // actix::clock::sleep
        addr.do_send(CounterAdd(3));

        sleep(Duration::from_millis(200)).await;
        addr.do_send(CounterAdd(5)); 

        let _ = fut.await; // wait a seconds.

        let current_value = addr.send(GetCounter).await.unwrap();
        println!("value is: {}", current_value);
        sleep(Duration::from_secs(2)).await;
        
        let current_value = addr.send(GetCounter).await.unwrap();
        println!("value is: {}", current_value);

        println!("=[case 2]=========================");
        addr.do_send(ShutDown); // 关闭的 actor 的消息
        let ret = addr.send(GetCounter).await;
        // use the added method in ActixMailboxSimplifyExtend
        ret.handle_mailbox(|_| {
            unreachable!("unpossible to reach here due to MailboxError must be encountered.");
        });
    });
    println!("actix-run: {:?}", run_ret);
}
复制代码

这里测试了函数 add_later 的可用性,和 handle_mailbox 处理错误时是否正确。

运行结果:

=[case 1]=========================
init value is: 0
add 3
add 5
counter will add 8, after 1 second(s)
value is: 8
add 8
value is: 16
=[case 2]=========================
common handle MailboxError: Mailbox has closed
actix-run: Ok(())
复制代码

总结

Rust 开创者根据多年的语言经历总结了前人开发的经验,推崇组合大于继承,可谓是集大成者。多用组合的方式去拓展你的代码,更你的代码更加 Rusty!

本文代码已推送至 github,如果对你有所启发欢迎 star。

github.com/oloshe/rust…

转载请声明出处

猜你喜欢

转载自juejin.im/post/7014051305706684429