if-elseの代わりにHashMapを使ってコードをシンプルにする
はじめに
if-else の条件式が複雑になって、コードが読みにくくなったこと、あると思います。
この記事では、if-else を HashMap に置き換えることで、コードをシンプルにする方法を紹介します。
要約
- 条件と処理の対応を HashMap で管理する
- 対応が一目でわかり、コードもシンプルになる
最近、X11 Window Manager をいちから実装する本を Zenn で公開しました。
その本の中で、Alt + j でウィンドウのフォーカスを移動するという機能の実装を書きました。
この記事では、その実装で考えたことを整理します。素朴な実装の問題点と、その改善策を具体的なコードで見ていきます。
素朴な実装とその問題点
まず、素朴に実装するとどうなるかを見てみます。
キーが押されたときのイベント処理で、if-else を使って分岐するコードは以下のようになります。
fn handle_key_press(&mut self, event: &KeyPressEvent) -> Result<()> {
let modifier = event.state;
let keycode = event.detail;
if modifier == ModMask::M1 && keycode == KEY_J {
self.focus_next()?;
} else if modifier == ModMask::M1 && keycode == KEY_K {
self.focus_prev()?;
} else if modifier == ModMask::M1 && keycode == KEY_M {
self.swap_master()?;
}
Ok(())
}
このコードには、読みやすさの観点からいくつか問題があります。
- Modifier (修飾キー) の組み合わせが増えると条件分岐が複雑になる。今は Alt (M1) だけだが、Ctrl, Super, Shift なども使うと、条件式が長くなり読みにくくなる。
- キーとアクションの対応付けが一覧できない。どのキーが何をするかを確認するには、
if-elseのコードを上から順に読んでいく必要がある。
解決策
これらの問題を解決するために、キーの組み合わせ (Modifier, KeyCode) とアクションの対応を HashMap で管理します。
enum Action {
FocusNext,
FocusPrev,
SwapMaster,
}
struct WindowManager {
keybindings: HashMap<(ModMask, Keycode), Action>,
// ...
}
キーバインドの設定は、HashMap で定義するだけです。
let keybindings = HashMap::from([
((ModMask::M1, KEY_J), Action::FocusNext),
((ModMask::M1, KEY_K), Action::FocusPrev),
((ModMask::M1, KEY_M), Action::SwapMaster),
]);
この設定を見れば、どのキーが何をするかが一目でわかります。
イベント処理は以下のように書けます。
fn handle_key_press(&mut self, event: &KeyPressEvent) -> Result<()> {
let modifier = event.state;
let keycode = event.detail;
if let Some(action) = self.keybindings.get(&(modifier, keycode)).copied() {
match action {
Action::FocusNext => self.focus_next()?,
Action::FocusPrev => self.focus_prev()?,
Action::SwapMaster => self.swap_master()?,
}
}
Ok(())
}
HashMap からアクションを取得し、match で処理を振り分けています。Modifier の組み合わせがどれだけ増えても、この関数の構造は変わりません。
まとめ
キーバインドを HashMap で管理することで、コードが読みやすくなりました。
- キーとアクションの対応が一覧できる
- イベント処理のコードがシンプルになる
また、この設計なら将来、設定ファイルからキーバインドを読み込む形にも拡張しやすいです。
調べてみると、これに似たパターンとして Table-Driven Methods というものがあるようです。
Table-Driven Methods ではデータ構造に関数を直接入れておき、取得してそのまま呼び出します。今回の例で言えば、HashMap の value に enum ではなく関数を入れる形です。そうすると match が不要になりシンプルに書けます。
今回は enum を使ったので、match による網羅性チェックが効くというメリットがあります。
Discussion