Rust - 访问者模式 vs 枚举

621次阅读  |  发布于1年以前

访问者模式是为一组类添加一个操作,而不修改每个类本身。

Rust中实现访问者模式,假设我们有两种具体的数据类型:X和Y,为简单起见,我们假设这些类只是没有方法的纯结构体,因此称之为Data。同时我们定义Action Trait表示我们想要应用于Data上的操作。

下面的代码是实现访问者模式的最小样板代码:

trait Data {
    fn call<A: Action>(&self, action: &A) -> A::Result;
}

trait Action {
    type Result;
    fn act_x(&self, x: &X) -> Self::Result;
    fn act_y(&self, y: &Y) -> Self::Result;
}

struct X {
    // ...
}

impl Data for X {
    fn call<A: Action>(&self, action: &A) -> A::Result {
        action.act_x(self)
    }
}

struct Y {
    // ...
}

impl Data for Y {
    fn call<A: Action>(&self, action: &A) -> A::Result {
        action.act_y(self)
    }
}

为了添加一个动作,我们创建了实现action的结构体:

struct Action1;
impl Action for Action1 {
    type Result = i32;
    fn act_x(&self, x: &X) -> Self::Result { 1 }
    fn act_y(&self, x: &Y) -> Self::Result { 2 }
}

struct Action2;
impl Action for Action2 {
    type Result = &'static str;
    fn act_x(&self, x: &X) -> Self::Result { "x" }
    fn act_y(&self, x: &Y) -> Self::Result { "y" }
}

现在我们有了Data = {X, Y}和Action = {Action1, Action2},我们就可以得到所有可能的组合。

fn main() {
    let x = X{ /* ... */ };
    let y = Y{ /* ... */ };
    let a1 = Action1;
    let a2 = Action2;

    // {X, Y} x {Action1, Action2}
    let result_x1 = x.call(&a1);
    let result_x2 = x.call(&a2);
    let result_y1 = y.call(&a1);
    let result_y2 = y.call(&a2);
}

通过这种方式,我们可以在不修改类本身的情况下向现有的类添加任意多个操作。缺点是它使代码冗长。访问者模式的替代方案是什么?

对于Rust,我们可以使用enum模式匹配:

enum Data {
    X(X),
    Y(Y),
}

struct X {
    // fill in
}

struct Y {
    // fill in
}

fn action1(data: &Data) -> i32 {
    match data {
        Data::X(x) => 1,
        Data::Y(y) => 2,
    }
}
fn action2(data: &Data) -> &'static str {
    match data {
        Data::X(x) => "x",
        Data::Y(y) => "y",
    }
}
fn perform_all_combinations() {
    let x = Data::X(X{ /* ... */ });
    let y = Data::Y(Y{ /* ... */ });

    let x1 = action1(&x);
    let x2 = action2(&x);
    let y1 = action1(&y);
    let y2 = action2(&y);
}

这极大地简化了代码,这是否意味着我们应该总是使用枚举模式匹配而不是访问者模式呢?

通常情况下,天下没有免费的午餐。枚举模式匹配的一个缺点是,通过创建枚举,Data的对象大小将被设置为其变体中最大的,因此可能会浪费内存空间。可以通过将X和Y包装到Box中来缓解这个问题,但这本身又引入了另一种间接方式,为动态内存分配带来了额外的开销。

然而,访问者模式的典型用例可能需要在运行时动态确定数据类型,因此访问者模式也可能需要Box开销。

总之,这两种模式让我们实现了同样的事情,我认为没有哪一种模式在所有方面都比另一种更好。更确切地说,这可能取决于个人对代码组织方式的偏好。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8