问题陈述
我有一组结构体A
、B
、C
和D
,它们都实现了一个trait Runnable
。
trait Runnable {
fn run(&mut self);
}
impl Runnable for A {...}
impl Runnable for B {...}
impl Runnable for C {...}
impl Runnable for D {...}
我还有一个结构体Config
,它用作构造A
、B
、C
和D
示例的规范。
struct Config {
filename: String,
other_stuff: u8,
}
impl From<Config> for A {...}
impl From<Config> for B {...}
impl From<Config> for C {...}
impl From<Config> for D {...}
在我的程序中,我想解析一个Config
示例,并根据filename
字段的值构造一个A
、B
、C
或D
,然后在其上调用Runnable::run
。结构体的选择方法是根据filename
字符串顺序检查每个结构体,然后选择第一个与该字符串“匹配”的结构体。
简单实现
下面是一个简单的实现。
trait CheckFilename {
fn check_filename(filename: &str) -> bool;
}
impl CheckFilename for A {...}
impl CheckFilename for B {...}
impl CheckFilename for C {...}
impl CheckFilename for D {...}
fn main() {
let cfg: Config = get_config(); // Some abstract way of evaluating a Config at runtime.
let mut job: Box<dyn Runnable> = if A::check_filename(&cfg.filename) {
println!("Found matching filename for A");
Box::new(A::from(cfg))
} else if B::check_filename(&cfg.filename) {
println!("Found matching filename for B");
Box::new(B::from(cfg))
} else if C::check_filename(&cfg.filename) {
println!("Found matching filename for C");
Box::new(C::from(cfg))
} else if D::check_filename(&cfg.filename) {
println!("Found matching filename for D");
Box::new(D::from(cfg))
} else {
panic!("did not find matching pattern for filename {}", cfg.filename);
};
job.run();
}
这是可行的,但有一些代码气味:
- 巨人
if else if else if else if else...
声明是臭海事组织 - 很多重复:用于检查文件名、打印所选结构类型以及从配置构造示例的代码对于每个分支都是相同的,只是它们处理的结构类型不同。有没有办法把这种重复抽象出来?
- 非常容易出错:很容易由于不能同步结构和 predicate 而意外地搞砸文件名字符串和结构之间的Map;例如写这样的东西:
if D::check_filename(&cfg.filename) {
println!("Found matching filename for D");
Box::new(B::from(cfg)) // Developer error: constructs a B instead of a D.
}
,编译器不会捕获它。
- 添加新结构(例如
E
,F
,G
等)到程序不是很符合人体工程学。它需要为每个分支添加一个新的分支到主if else
语句中。简单地将结构体添加到某种结构体类型的“主列表”中会更好。
有没有更优雅或更习惯的方法来处理这些气味?
2条答案
按热度按时间3hvapo4f1#
由于转换会消耗
Config
,因此统一所有类型的逻辑的挑战在于,您需要有条件地移动config值以进行转换。标准库有多种易出错的消费函数的情况,它们使用的模式是返回Result
,在Err
情况下返回可能被消费的值。例如,Arc::try_unwrap
提取Arc
的内部值,但如果这失败了,它会在Err
变体中返回Arc
。我们可以在这里做同样的事情,创建一个单一的函数,如果文件名匹配,它会产生一个适当的结构体,但在错误时返回配置:
然后,您可以编写另一个函数,其中包含此函数的特定示例化的静态切片,并且它可以按顺序尝试每个示例化,直到其中一个成功为止。由于我们将配置移动到每个加载器函数中,因此我们必须将其放回
Err
案例中,以便下一次循环迭代可以再次移动它。这解决了您的所有问题:
try_convert_config_to
一次实现了所有类型的逻辑。try_convert_config_to
,就不可能在不同类型上意外调用进程的两个部分(check_filename
和into
)。CONFIG_LOADERS
切片中添加一个新元素。(Playground)
h5qlskok2#
提出我自己的问题来清理一些被接受的答案留下的小问题,并提供一些额外的解释和参考。向@cdhowie大喊,因为它提供了我解决问题所需的公认答案和知识。
概述
通过重构一些东西,就有可能解决朴素实现的所有问题。技巧是创建一个简单的易错转换函数,如果文件名匹配,则将
Config
值转换为Box<dyn Runnable>
,但如果文件名不匹配,则将Config
值返回到Err
变量中,以便下一个专门化可以尝试。定义需求
首先,让我们列出每种类型必须满足的最低要求,以支持我们的应用程序:
1.必须是
Runnable
,因为我们打算run()
它一旦示例化。1.必须是
From<Config>
,因为我们打算从Config
创建一个1.必须有一个名称与它相关联,这样我们就可以打印日志消息,标识选择了哪种类型。
1.必须能够确定它是否“匹配”
filename
字符串定义了这些需求后,我们定义了一个Trait来执行它们:
需求1和需求2使用超级特征编码,需求3使用相关常量编码
注意:在问题陈述中,这个trait被命名为
CheckFilename
,但在这里它被重命名为Job
,因为考虑到额外的需求,这似乎更合适。为每个结构体实现
Job
非常简单:创建通用转换函数
接下来,我们创建一个简单的易错函数,它尝试将
Config
值转换为Box<dyn Runnable>
,但如果不匹配,则返回Config
值(带所有权)。此函数对所有Job
类型通用。把它连在一起
最后,一个(盒装的)
Runnable
trait object可以通过调用try_convert_config_to::<A>
来初始化,并通过一个Result::or_else
链顺序传递错误值,剩余的专门化为Job
。(Playground)