在 Rust 中实现模板方法设计模式

问题描述 投票:0回答:1

这实际上来自另一个案例,但这里进行了简化。假设我们想要实现模板方法设计模式,并提供一个库来实现主要算法,并允许库的用户通过实现特征来定制它。可以在此处找到此模式的示例。

然后我们将创建一个像这样的特征:

trait Command {
    fn open(&mut self, file: &str);
    fn read(&mut self, buf: &mut [u8]) -> bool;
    fn close(&mut self);
}

这将创建一个库用户可以实现的接口,并且库中也提供了主要算法,如下所示:

fn execute<C: Command>(mut command: C, path: &str) -> String {
    command.open(path);
    let mut sink = Sink::new();
    let mut buf = [0u8; 512];
    while command.read(&mut buf) {
        sink.send(&buf);
    }
    command.close();
}

请注意,在我的特定情况下,这是我正在使用的库,它经过简化以专注于问题。我在这里故意使用了有限大小的缓冲区,尽管可以以更大的块传回数据,并且我将数据发送到某个虚构的接收器只是为了说明我们有一种转发数据而不存储数据的算法。另请注意,与 Rust 中的模板方法相比,它使用

&mut self
而不是
&self

我们现在可以实现此模板方法,以这种方式从文件中读取:

struct ReadFromFile {
    stream: Option<Box<dyn Read>>,
}

impl ReadFromFile {
    fn new() -> ReadFromFile {
        ReadFromFile { stream: None }
    }
}

impl Command for ReadFromFile {
    fn open(&mut self, path: &str) {
        self.stream = Some(Box::new(File::open(path).unwrap()));
    }

    fn close(&mut self) {
        self.stream = None;
    }

    fn read(&mut self, buf: &mut [u8]) -> bool {
        match &mut self.stream {
            Some(stream) => stream.read(buf).unwrap() > 0,
            None => false,
        }
    }
}

驱动它的主要函数可能如下所示:

fn main() {
    let read_all = ReadFromFile::new();
    let args: Vec<String> = std::env::args().collect();
    let result = execute(read_all, &args[1]);
    println!("result: {:?}", result);
}

效果很好,一切顺利。现在,假设我们想要从 PostgreSQL 读取数据,而不是使用

postgres
crate。按照文件示例中的模式,实现将如下所示:

struct ReadFromDatabase {
    client: Client,
    stream: Option<Box<dyn Read>>,
}

impl ReadFromDatabase {
    fn new() -> Result<ReadFromDatabase, postgres::Error> {
        let client = Client::connect("host=localhost user=mats", NoTls)?;
        Ok(ReadFromDatabase {
            client,
            stream: None,
        })
    }
}

impl Command for ReadFromDatabase {
    fn open(&mut self, path: &str) {
        let stmt = format!("COPY {} TO stdout", path);
        self.stream = Some(Box::new(self.client.copy_out(&stmt).unwrap()));
    }

    fn close(&mut self) {
        self.stream = None;
    }

    fn read(&mut self, buf: &mut [u8]) -> bool {
        match &mut self.stream {
            Some(stream) => stream.read(buf).unwrap() > 0,
            None => false,
        }
    }
}

但是,在编译时,我们收到以下错误:

error: lifetime may not live long enough
  --> examples/template-method.rs:74:28
   |
72 |     fn open(&mut self, path: &str) {
   |             - let's call the lifetime of this reference `'1`
73 |         let stmt = format!("COPY {} TO stdout", path);
74 |         self.stream = Some(Box::new(self.client.copy_out(&stmt).unwrap()));
   |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cast requires that `'1` must outlive `'static`

寻找其他案例,发现了这个案例,和上面的问题很接近,需要设置回调函数。

但是,问题集中在使用闭包上,建议的解决方案有两种选择:将生命周期添加到

ReadFromDatabase
(但没有详细说明如何),或者将对象移动到闭包中。由于此示例没有闭包,因此更难以遵循该建议。

我认为这里的问题是

stream
可能比
client
更长寿,因此代码可以防止虚构的
execute
方法在调用
read
后调用
close

这个问题类似于如何处理自引用结构,就像Shepmaster为什么我不能存储一个值和对该值的引用中的回复中的“具有对自身的引用的类型”一样在同一个结构中?

struct WhatAboutThis<'a> {
    name: String,
    nickname: Option<&'a str>,
}

这里唯一的区别是,我们存储的是对特征的引用,这不是

Sized
,而调整其大小的规范方法是将其包装在
Box
中。

所以,问题是:

  1. 是否可以通过添加生命周期来编写
    ReadFromDatabase
    实现以及它看起来如何?我已经做了一些尝试,但正如JMAA所说,它“很快变得混乱”,所以我不确定是否可以以合理的方式做到这一点。
  2. 是否有更好的模式可以实现
    ReadFromDatabase
    而无需更改
    Command
postgresql rust traits
1个回答
0
投票

据我所知,截至本答案时,在 Rust 中实现此目的的唯一方法(没有夜间/实验功能)需要稍微更改在

&self
特征/实现中引用
Command
的方法签名。

可以调整签名以删除自引用,接受命令作为参数(而不是期望它是

self
):
open(command: &mut dyn SomeTrait, path: &str)

© www.soinside.com 2019 - 2024. All rights reserved.