Command
説明
Command パターンの基本的な考え方は、アクションを独立したオブジェクトとして切り出し、それらをパラメーターとして渡すことです。
動機
オブジェクトとしてカプセル化された一連のアクションまたはトランザクションがあるとします。 これらのアクションまたはコマンドを、後で異なる時点に、何らかの順序で実行または呼び出したいとします。 これらのコマンドは、何らかのイベントの結果としてトリガーされることもあります。 たとえば、ユーザーがボタンを押したときや、データパケットが到着したときです。 さらに、これらのコマンドは取り消し可能である場合があります。 これは、エディターの操作で役立つことがあります。 システムがクラッシュした場合に後で変更を再適用できるように、実行されたコマンドのログを保存したい場合もあります。
例
2つのデータベース操作 create table と add field を定義します。
これらの操作はそれぞれコマンドであり、そのコマンドを取り消す方法を知っています。たとえば、drop table と remove field です。
ユーザーがデータベースマイグレーション操作を呼び出すと、各コマンドは定義された順序で実行され、ユーザーがロールバック操作を呼び出すと、一連のコマンド全体が逆順で呼び出されます。
アプローチ: トレイトオブジェクトを使用する
コマンドをカプセル化する共通のトレイトを定義し、execute と rollback の2つの操作を持たせます。
すべてのコマンド structs は、このトレイトを実装する必要があります。
pub trait Migration {
fn execute(&self) -> &str;
fn rollback(&self) -> &str;
}
pub struct CreateTable;
impl Migration for CreateTable {
fn execute(&self) -> &str {
"create table"
}
fn rollback(&self) -> &str {
"drop table"
}
}
pub struct AddField;
impl Migration for AddField {
fn execute(&self) -> &str {
"add field"
}
fn rollback(&self) -> &str {
"remove field"
}
}
struct Schema {
commands: Vec<Box<dyn Migration>>,
}
impl Schema {
fn new() -> Self {
Self { commands: vec![] }
}
fn add_migration(&mut self, cmd: Box<dyn Migration>) {
self.commands.push(cmd);
}
fn execute(&self) -> Vec<&str> {
self.commands.iter().map(|cmd| cmd.execute()).collect()
}
fn rollback(&self) -> Vec<&str> {
self.commands
.iter()
.rev() // イテレーターの方向を反転する
.map(|cmd| cmd.rollback())
.collect()
}
}
fn main() {
let mut schema = Schema::new();
let cmd = Box::new(CreateTable);
schema.add_migration(cmd);
let cmd = Box::new(AddField);
schema.add_migration(cmd);
assert_eq!(vec!["create table", "add field"], schema.execute());
assert_eq!(vec!["remove field", "drop table"], schema.rollback());
}
アプローチ: 関数ポインターを使用する
各個別のコマンドを別々の関数として作成し、それらの関数を後で異なる時点に呼び出すために関数ポインターを保存する、別のアプローチを取ることもできます。
関数ポインターは Fn、FnMut、FnOnce の3つのトレイトすべてを実装しているため、関数ポインターの代わりにクロージャを渡して保存することもできます。
type FnPtr = fn() -> String;
struct Command {
execute: FnPtr,
rollback: FnPtr,
}
struct Schema {
commands: Vec<Command>,
}
impl Schema {
fn new() -> Self {
Self { commands: vec![] }
}
fn add_migration(&mut self, execute: FnPtr, rollback: FnPtr) {
self.commands.push(Command { execute, rollback });
}
fn execute(&self) -> Vec<String> {
self.commands.iter().map(|cmd| (cmd.execute)()).collect()
}
fn rollback(&self) -> Vec<String> {
self.commands
.iter()
.rev()
.map(|cmd| (cmd.rollback)())
.collect()
}
}
fn add_field() -> String {
"add field".to_string()
}
fn remove_field() -> String {
"remove field".to_string()
}
fn main() {
let mut schema = Schema::new();
schema.add_migration(|| "create table".to_string(), || "drop table".to_string());
schema.add_migration(add_field, remove_field);
assert_eq!(vec!["create table", "add field"], schema.execute());
assert_eq!(vec!["remove field", "drop table"], schema.rollback());
}
アプローチ: Fn トレイトオブジェクトを使用する
最後に、共通のコマンドトレイトを定義する代わりに、Fn トレイトを実装する各コマンドをベクターに個別に保存することもできます。
type Migration<'a> = Box<dyn Fn() -> &'a str>;
struct Schema<'a> {
executes: Vec<Migration<'a>>,
rollbacks: Vec<Migration<'a>>,
}
impl<'a> Schema<'a> {
fn new() -> Self {
Self {
executes: vec![],
rollbacks: vec![],
}
}
fn add_migration<E, R>(&mut self, execute: E, rollback: R)
where
E: Fn() -> &'a str + 'static,
R: Fn() -> &'a str + 'static,
{
self.executes.push(Box::new(execute));
self.rollbacks.push(Box::new(rollback));
}
fn execute(&self) -> Vec<&str> {
self.executes.iter().map(|cmd| cmd()).collect()
}
fn rollback(&self) -> Vec<&str> {
self.rollbacks.iter().rev().map(|cmd| cmd()).collect()
}
}
fn add_field() -> &'static str {
"add field"
}
fn remove_field() -> &'static str {
"remove field"
}
fn main() {
let mut schema = Schema::new();
schema.add_migration(|| "create table", || "drop table");
schema.add_migration(add_field, remove_field);
assert_eq!(vec!["create table", "add field"], schema.execute());
assert_eq!(vec!["remove field", "drop table"], schema.rollback());
}
考察
コマンドが小さく、関数として定義できる、またはクロージャとして渡せる場合は、動的ディスパッチを利用しないため、関数ポインターを使用する方が望ましい場合があります。
しかし、コマンドが多数の関数と変数を持つ1つの構造体であり、独立したモジュールとして定義されている場合は、トレイトオブジェクトを使用する方が適しています。
適用例は actix に見られます。これは、ルートに対するハンドラー関数を登録するときにトレイトオブジェクトを使用します。
Fn トレイトオブジェクトを使用する場合、関数ポインターの場合と同じようにコマンドを作成して使用できます。
パフォーマンスについては、パフォーマンスとコードの単純さおよび構成の間には常にトレードオフがあります。 静的ディスパッチはより高速なパフォーマンスをもたらしますが、動的ディスパッチはアプリケーションを構造化する際に柔軟性を提供します。