Action.rs
Tui
および EventHandler
を作成したので、 Command
パターンも紹介します。
これらは通常、 Action
または Message
とも呼ばれます。
pub enum Action { Quit, Tick, Increment, Decrement, Noop,}
EventHandler
からのすべての Event
がenumから Action
にマッピングされるように、単純な impl App
を定義しましょう。
#[derive(Default)]struct App { counter: i64, should_quit: bool,}
impl App { pub fn new() -> Self { Self::default() }
pub async fn run(&mut self) -> Result<()> { let t = Tui::new(); t.enter(); let mut events = EventHandler::new(tick_rate); loop { let event = events.next().await; let action = self.handle_events(event); self.update(action); t.terminal.draw(|f| self.draw(f))?; if self.should_quit { break } }; t.exit(); Ok(()) }
fn handle_events(&mut self, event: Option<Event>) -> Action { match event { Some(Event::Quit) => Action::Quit, Some(Event::AppTick) => Action::Tick, Some(Event::Key(key_event)) => { if let Some(key) = event { match key.code { KeyCode::Char('q') => Action::Quit, KeyCode::Char('j') => Action::Increment, KeyCode::Char('k') => Action::Decrement _ => {} } } }, Some(_) => Action::Noop, None => Action::Noop, } }
fn update(&mut self, action: Action) { match action { Action::Quit => self.should_quit = true, Action::Tick => self.tick(), Action::Increment => self.increment(), Action::Decrement => self.decrement(), }
fn increment(&mut self) { self.counter += 1; }
fn decrement(&mut self) { self.counter -= 1; }
fn draw(&mut self, f: &mut Frame<'_>) { f.render_widget( Paragraph::new(format!( "Press j or k to increment or decrement.
Counter: {}", self.counter )) ) }}
handle_events(event) -> Action
を使用して Event
を取得し、それを Action
にマッピングします。update(action)
を使用して Action
を取得し、アプリの状態を変更します。
このアプローチの利点の 1 つは、必要に応じて handle_key_events()
を変更してキー構成を使用できるため、ユーザーがキーからアクションへの独自のマッピングを定義できることです。
この方法のもう 1 つの利点は、Tui
または EventHandler
のインスタンスを作成しなくても、App
構造体のビジネス ロジックをテストできることです。例:
mod tests { #[test] fn test_app() { let mut app = App::new(); let old_counter = app.counter; app.update(Action::Increment); assert!(app.counter == old_counter + 1); }}
上記のテストでは、Tui
または EventHandler
のインスタンスを作成しておらず、run
関数も呼び出していませんが、アプリケーションのビジネス ロジックをテストすることはできます。
Action
でアプリの状態を更新することで、アプリケーションを「状態マシン」にすることに一歩近づき、理解とテスト可能性が向上します。
純粋にしたい場合は、AppState
を不変にして、次のような update
関数を使用します。
fn update(app_state::AppState, action::Action) -> new_app_state::State { let mut state = app_state.clone(); state.counter += 1; // ... state}
まれに、 update
で将来のアクションを選択することもできます。
fn update(app_state::AppState, action::Action) -> (new_app_state::State, Option<action::Action>) { let mut state = app_state.clone(); state.counter += 1; // ... (state, Action::Tick)}
Rust でこのアーキテクチャに従うコードを書くには (私の意見では)、より事前の設計が必要です。
主な理由は、AppState
構造体を Clone
対応にする必要があるためです。
TUI の調査段階またはプロトタイプ段階であれば、そのようなことはしたくないでしょう。
設計を把握してから、このようにリファクタリングすることだけに興味があります。
これに対する私の回避策は (前に説明したように)、update
を &mut self
を受け取るメソッドにすることです。
impl App { fn update(&mut self, action: Action) -> Option<Action> { self.counter += 1 None }}
フィット感のように、コードを自由に再編成できます!
必要に応じてさらにアクションを追加することもできます。たとえば、テンプレート内のすべてのアクションがあります。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Display, Deserialize)]pub enum Action { Tick, Render, Resize(u16, u16), Suspend, Resume, Quit, Refresh, Error(String), Help, ToggleShowHelp, ScheduleIncrement, ScheduleDecrement, Increment(usize), Decrement(usize), CompleteInput(String), EnterNormal, EnterInsert, EnterProcessing, ExitProcessing, Update,}