命令是将请求或简单操作转换为对象的行为设计模式。
首先看看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。参考这里