首页 > 解决方案 > 我怎样才能让这个 Rust 代码更地道

问题描述

最近我开始学习 Rust,我的主要困难之一是将多年的面向对象思维转化为过程代码。

我正在尝试解析具有由特定处理程序处理的标记的 XML,该处理程序可以处理从子项获取的数据。

此外,我还有一些它们之间共有的字段成员,我不希望将相同的字段写入所有处理程序。

我试过了,我的代码是这样的:

use roxmltree::Node; // roxmltree = "0.14.0"

fn get_data_from(node: &Node) -> String {
   let tag_name = get_node_name(node);
   let tag_handler: dyn XMLTagHandler = match tag_name {
      "name" => NameHandler::new(),
      "phone" => PhoneHandler::new(),
      _ => DefaultHandler::new()
   }
   if tag_handler.is_recursive() {
     for child in node.children() {
         let child_value = get_data_from(&child);
         // do something with child value
     }
   }
   let value: String = tag_handler.value()

   value
}

// consider that handlers are on my project and can be adapted to my needs, and that XMLTagHandler is the trait that they share in common.

我的主要问题是:

标签: rust

解决方案


  • 这感觉像是一种面向对象的方法

事实上,我不这么认为。此代码明显违反了告诉-不询问原则,该原则脱离了面向对象编程的中心思想:将数据和相关行为封装到对象中。对象(NameHandlerPhoneHandler等)没有足够的知识来了解它们自己要做什么,因此get_data_from必须向它们查询信息并决定要做什么,而不是简单地发送消息并让对象弄清楚如何处理它。

因此,让我们首先将有关如何处理每种标签的知识转移到处理程序本身:

trait XmlTagHandler {
    fn foreach_child<F: FnMut(&Node)>(&self, node: &Node, callback: F);
}

impl XmlTagHandler for NameHandler {
    fn foreach_child<F: FnMut(&Node)>(&self, _node: &Node, _callback: F) {
        // "name" is not a recursive tag, so do nothing
    }
}

impl XmlTagHandler for DefaultHandler {
    fn foreach_child<F: FnMut(&Node)>(&self, node: &Node, callback: F) {
        // all other tags may be recursive
        for child in node.children() {
            callback(child);
        }
    }
}

这样你foreach_child就可以调用每一种Handler,并让处理程序自己决定正确的操作是否是递归。毕竟,这就是他们有不同类型的原因——对吧?

为了摆脱dyn不必要的部分,让我们编写一个小通用帮助函数,用于XmlTagHandler处理一种特定类型的标签,并进行修改get_data_from,使其仅调度到正确的参数化版本。(我假设它XmlTagHandler也有一个new功能,以便您可以通用地创建一个。)

fn handle_tag<H: XmlTagHandler>(node: &Node) -> String {
    let handler = H::new();
    handler.foreach_child(node, |child| {
        // do something with child value
    });
    handler.value()
}

fn get_data_from(node: &Node) -> String {
    let tag_name = get_node_name(node);
    match tag_name {
        "name" => handle_tag::<NameHandler>(node),
        "phone" => handle_tag::<PhoneHandler>(node),
        _ => handle_tag::<DefaultHandler>(node),
    }
}

如果你不喜欢handle_tag::<SomeHandler>(node),也可以考虑制作handle_tag一个提供的方法XmlTagHandler,这样你就可以改写SomeHandler::handle(node)

请注意,我并没有真正改变任何数据结构。XmlTagHandler您对特征和各种实现者的假设Handler是组织代码的一种非常正常的方式。但是,在这种情况下,与仅编写三个单独的函数相比,它并没有提供任何真正的改进:

fn get_data_from(node: &Node) -> String {
    let tag_name = get_node_name(node);
    match tag_name {
        "name" => get_name_from(node),
        "phone" => get_phone_from(node),
        _ => get_other_from(node),
    }
}

在某些语言中,例如 Java,所有代码都必须是某个类的一部分——因此您会发现自己编写的类由于任何其他原因不存在,而只是将相关事物组合在一起。在 Rust 中,您不需要这样做,因此请确保任何增加的复杂性,例如XmlTagHandler实际上是在拉扯它的重量。

  • is_recursive 需要重新实现到每个结构,因为它们的特征不能有字段成员,我以后必须添加更多字段,这意味着每个新字段都有更多样板

如果没有有关这些领域的更多信息,就不可能真正理解您在这里面临的问题;然而,一般来说,如果有一个structs 族有一些共同的数据,你可能想要创建一个泛型struct而不是一个特征。请参阅如何重用二叉搜索树、红黑树和 AVL 树的代码?更多建议。

  • 我可以为 Handler 使用一种类型并将函数指针传递给它,但这种方法似乎很脏

优雅有时是有用的东西,但它是主观的。我会推荐闭包而不是函数指针,但这个建议对我来说似乎并不“脏”。制作闭包并将它们放入数据结构中是编写 Rust 代码的一种非常正常的方式。如果您可以详细说明您不喜欢它的哪些方面,也许有人可以指出改进它的方法。


推荐阅读