Rust语言之GoF设计模式:命令模式

22-09-26 banq

命令是将请求或简单操作转换为对象的行为设计模式

首先看看stackoverflow上一个朴素天真的案例

struct SomeStruct {
    pub some_field: String,
}

impl SomeStruct {
    pub fn new(field: String) -> SomeStruct {
        let some_struct = SomeStruct {
            some_field: field,
        };
        return some_struct;
    }

    pub fn change_field(&mut self) {
        self.some_field = "Something else".to_string();
    }
}

struct SomeCommand<'a> {
    pub some_struct: &'a mut SomeStruct,
}

impl<'a> SomeCommand<'a> {
    pub fn new(the_struct: &'a mut SomeStruct) -> SomeCommand {
        let some_command = SomeCommand {
            some_struct: the_struct,
        };
        return some_command;
    }

    pub fn execute(&mut self) {
        self.some_struct.change_field();
    }
}

fn main() {
    let mut some_struct = SomeStruct::new("hey".to_string());
    let some_command1 = SomeCommand::new(&mut some_struct);

     // 编译器抱怨报错:因为当前代码正在对some_struct进行另一个可变的绑定
    let some_command2 = SomeCommand::new(&mut some_struct);
}


这里问题是:作为Command的接口SomeCommand这个trait,只能设计为接受通用类型,而不是接受特定类型Some_struct。
使用mut作为可变变量时,Rust 编译器将不喜欢对单个对象的多个可变引用。



Command标准案例
实现Command模式关键是如何激活Command命令,也就是说,如何响应Command,一般一个command只有一个执行方法execute,在这个执行方法中,每个Command实现都会做不同的业务逻辑,这个业务逻辑是与当前上下文有关的,需要客户端调用时传入。
一个命令实例不应该持有对全局上下文的永久引用,相反,应该作为 "execute "方法的一个可变mut参数从上到下传递,如下面cursive。

fn execute(&mut self, app: &mut cursive::Cursive) -> bool;


以界面按钮为例子:

command.rs

mod copy;
mod cut;
mod paste;

pub use copy::CopyCommand;
pub use cut::CutCommand;
pub use paste::PasteCommand;

/// 声明了一个执行(和撤销)命令的方法。
///
/// 每个命令都会收到一个应用程序上下文来访问
/// 界面组件(如编辑视图)和一个剪贴板。
pub trait Command {
    fn execute(&mut self, app: &mut cursive::Cursive) -> bool;
    fn undo(&mut self, app: &mut cursive::Cursive);
}


三个命令的实现:
  • copy.rs

    use cursive::{views::EditView, Cursive};
    
    use super::Command;
    use crate::AppContext;
    
    #[derive(Default)]
    pub struct CopyCommand;
    
    impl Command for CopyCommand {
        fn execute(&mut self, app: &mut Cursive) -> bool {
            let editor = app.find_name::<EditView>("Editor").unwrap();
            let mut context = app.take_user_data::<AppContext>().unwrap();
    
            context.clipboard = editor.get_content().to_string();
    
            app.set_user_data(context);
            false
        }
    
        fn undo(&mut self, _: &mut Cursive) {}
    }
    
  • cut.rs
  • paste.rs


cursive上下文信息是在调用者客户端这里定义:

mod command;

use cursive::{
    traits::Nameable,
    views::{Dialog, EditView},
    Cursive,
};

use command::{Command, CopyCommand, CutCommand, PasteCommand};

/// 一个将被传递到视觉组件回调的应用程序上下文。
/// 它包含一个剪贴板和一个要撤销的命令的历史。
#[derive(Default)]
struct AppContext {
    clipboard: String,
    history: Vec<Box<dyn Command>>,
}

fn main() {
    let mut app = cursive::default();

    app.set_user_data(AppContext::default());
    app.add_layer(
        Dialog::around(EditView::default().with_name("Editor"))
            .title("Type and use buttons")
            .button("Copy", |s| execute(s, CopyCommand::default()))
            .button("Cut", |s| execute(s, CutCommand::default()))
            .button("Paste", |s| execute(s, PasteCommand::default()))
            .button("Undo", undo)
            .button("Quit", |s| s.quit()),
    );

    app.run();
}

/// 执行一个命令,然后把它推到历史数组中
fn execute(app: &mut Cursive, mut command: impl Command + 'static) {
    if command.execute(app) {
        app.with_user_data(|context: &mut AppContext| {
            context.history.push(Box::new(command));
        });
    }
}

///弹出最后一条命令并执行撤销操作。
fn undo(app: &mut Cursive) {
    let mut context = app.take_user_data::<AppContext>().unwrap();
    if let Some(mut command) = context.history.pop() {
        command.undo(app)
    }
    app.set_user_data(context);
}


接受通用数据结构的命令
上面案例时使用一个通用的上下文结构Cursive作为Command通用trait的传入参数,如果不想使用全局上下文概念,可以在每个Command实现中自己对自己的结构操作。

trait Command {
    fn execute(&mut self);            // Execute command.
    fn is_reversible(&self) -> bool;  // Undoable operation?
    fn unexecute(&mut self);          // Undo command.
}

// ------ Using RefCell ------

struct ChangeFontSizeCommand<'a> {
    text: &'a RefCell<dyn Text>,
    old_size: Option<usize>,
    new_size: usize
}

impl<'a> Command for ChangeFontSizeCommand<'a> {
    // Implementation... (many calls to .borrow() and .borrow_mut())
}

impl<'a> ChangeFontSizeCommand<'a> {
    pub fn new(text: &'a RefCell<dyn Text>, new_size: usize) -> Self {
        // Implementation...
    }
}

// ------ Using Arc and Mutex ------

struct ChangeFontColorCommand {
    text: Arc<Mutex<dyn Text>>,
    old_color: Option<Color>,
    new_color: Color
}

impl Command for ChangeFontColorCommand {
    // Implementation... (many calls to .lock().unwrap())
}

impl ChangeFontColorCommand {
    pub fn new(text: Arc<Mutex<dyn Text>>, new_color: Color) -> Self {
        // Implementation...
    }
}

注意到,这里Command 没有全局上下文作为参数传入了,在每个Command实现中分别使用了

RefCell<dyn Text>
Arc<Mutex<dyn Text>>


两种方式,在这两个示例中,实例RefCell或Arc<Mutex<>>必须在对象初始化器之外创建,我们不能传入可变引用并在命令实现结构内创建它们,这将违反 Rust 的借用检查器规则。

这样做的理由:Rust 编译器将不喜欢对单个对象的多个可变mut引用,全局上下文局限在于:Command对于我们想要对其执行命令的任何给定对象,我们只能拥有一个实例,并且我们只能调用一次 execute。而当前引入这两种方式则可以打破这个局限。

注意:RefCell它不是线程安全的,因此Command如果选择以这种方式实现它,则不能跨线程共享相同的对象;而是Arc<Mutex<>>线程安全的。

当然,命令实现中,如果能将命令的各种实现通过类似Java动态反射机制通过配置加入,也是一种灵活的方式,但是Rust 没有反射;反射意味着您可以在运行时获取有关类型的详细信息,例如字段、方法、它实现的接口等。
Rust最接近反射的是:显式实现(或派生)提供此信息的trait。参考这里