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,}