Rustの設計と実装Tipsを学ぶ

RTKに学ぶRust設計パターン: Traitとモジュール構造による拡張可能なシステム構築 Part 3

解析日: 2026/7/7
対象コミット: d823aaf
リポジトリ: rtk-ai/rtk
RustTraitモジュール構造デザインパターンCLI拡張性

1. 概要

rtk (Rust Token Killer) は、大規模言語モデル (LLM) のトークン消費量を劇的に削減するために開発された、高性能なRust製CLIプロキシです。gitcargodocker など、多様なCLIコマンドの出力を最適化し、LLMのコスト削減と効率向上に貢献しています。

前回の記事では、rtkの堅牢なCLI設計とclapクレートを用いたコマンドパターンの活用方法に焦点を当てました。Part 2では、rtkの高性能化戦略、特に同期処理の選択と積極的なコンパイラ最適化について深掘りしました。

本記事「Part 3」では、rtkがどのようにしてこれほど多種多様なCLIコマンドに対応し、将来的な拡張性も維持しているのか、そのモジュール構造とRustのTraitを駆使した拡張可能なシステム構築に焦点を当てて解説します。複雑なドメインロジックを整理し、変更に強いコードベースを構築するための実践的なパターンを学びましょう。

2. rtkのモジュラーアーキテクチャ:ドメインごとの責務分離

rtkプロジェクトは、100を超える多様なCLIツールの出力を処理するという複雑な課題を抱えています。このような大規模かつ多機能なアプリケーションにおいて、コードの管理性、保守性、そして拡張性を維持するためには、明確なモジュール構造が不可欠です。

rtkは、src/cmdsディレクトリ以下に、gitawsdockerrustpythonjsといったドメイン(CLIツール種別)ごとにサブモジュールを設ける「Module-per-Domain (ドメインごとのモジュール)」パターンを強力に採用しています。これにより、特定のCLIツールに関するロジック(引数のパース、出力のフィルタリング規則など)がそのドジュール内にカプセル化され、他のドメインのロジックと衝突することなく独立して管理されます。

これにより、以下のようなメリットが生まれます。

以下に、rtksrc/cmdsモジュール構造の概念図を示します。

graph TD A[rtk アプリケーション] --> B["Command Handling Layer ('src/cmds')"]; B --> C["src/cmds/git"]; B --> D["src/cmds/aws"]; B --> E["src/cmds/docker"]; B --> F["src/cmds/rust"]; B --> G["src/cmds/python"]; B --> H["src/cmds/js"]; B --> I["... その他多数 (100+)"]; C --> C1["git.rs (git status, diff などのロジック)"]; D --> D1["aws.rs (aws s3, ec2 などのロジック)"]; F --> F1["cargo_cmd.rs (cargo build, test などのロジック)"];

3. RustのTraitによる拡張性の確立:戦略パターンの応用

モジュール構造がコードの物理的な配置を整理する一方で、RustのTraitは、異なるモジュールやデータ型間で共通の振る舞いを定義し、柔軟な抽象化を実現するための強力なメカニズムです。

rtkの分析から、core::filter::FilterLevelという列挙型(none, minimal, aggressive)が存在することが分かります。これは、同じCLIコマンドの出力であっても、ユーザー設定や状況に応じて異なるレベルのフィルタリング(出力の圧縮や削除)戦略を適用できることを示唆しています。

このような「実行時に異なるアルゴリズムや振る舞いを切り替えたい」という要件は、GoFのデザインパターンの一つである「戦略パターン (Strategy Pattern)」の典型的なユースケースです。Rustでは、この戦略パターンをTraitを用いて効果的に実装することができます。

たとえば、以下のような概念的なTraitを定義し、各フィルタリングレベルやコマンドの種類ごとに具体的な実装を提供することが考えられます。

pub trait OutputFilter {
    /// 入力文字列をフィルタリングし、最適化された文字列を返す
    fn apply(&self, input: &str) -> String;
}

// 具体的なフィルタリング戦略の例 (概念)
pub struct MinimalFilter;
impl OutputFilter for MinimalFilter {
    fn apply(&self, input: &str) -> String {
        // 最小限のフィルタリングロジック
        input.to_string() // 例としてそのまま返す
    }
}

pub struct AggressiveGitFilter {
    // Gitコマンド特有のアグレッシブなフィルタリング状態
}
impl OutputFilter for AggressiveGitFilter {
    fn apply(&self, input: &str) -> String {
        // Git出力に特化した、積極的なフィルタリングロジック
        input.replace("\n", " ") // 例
    }
}

// コマンドを実行し、フィルタを適用するコンテキスト (概念)
pub struct FilterContext<'a> {
    filter_strategy: &'a dyn OutputFilter,
}

iml FilterContext<'_> {
    pub fn process_output(&self, output: &str) -> String {
        self.filter_strategy.apply(output)
    }
}

これにより、FilterContextは具体的なフィルタリングの実装を知ることなく、OutputFilter Traitを通じて統一的にフィルタリング処理を実行できます。新たなフィルタリング戦略を追加する際も、OutputFilter Traitを実装する新しい構造体を定義するだけで済み、既存のコードを変更する必要がありません。これは、rtkのような多岐にわたるコマンドとフィルタリング要件を持つプロジェクトにおいて、非常に強力な拡張性を提供します。

実際のrtkのコードでは、clapクレートのValueEnumを用いたAgentTargetのような列挙型もTraitのような役割を果たし、CLI引数レベルでの柔軟な選択を可能にしています。これは、Traitオブジェクト(dyn OutputFilter)のような動的ディスパッチとは異なりますが、型安全な列挙とマッチングによって同様に異なる振る舞いを切り替える手段となります。

4. この記事で学べること

  1. 大規模CLIアプリケーションにおけるモジュラー設計の利点: Module-per-Domainパターンを活用したコードベースの整理方法。
  2. RustのTraitを用いた柔軟な処理ロジックの抽象化: 異なる実装を持つが共通のインターフェースで扱いたいロジックにTraitを適用する方法。
  3. 戦略パターンによる多様なフィルタリング要件への対応: 実行時に切り替え可能なアルゴリズムをTraitで実装するアプローチ。
  4. 変更に強く、拡張しやすいコードベースを構築するための実践的アプローチ: これらの設計パターンが長期的なプロジェクト成長にどう貢献するか。

5. 実践的な実装・コード解説

ドメインごとのモジュール構造 (Module-per-Domain)

rtkでは、src/cmds以下に各ツールに対応するディレクトリとファイルが存在します。例えば、gitコマンドの処理はsrc/cmds/git/git.rsに、cargoコマンドの処理はsrc/cmds/rust/cargo_cmd.rsに集約されています。

これは、main.rsで各サブモジュールを宣言し、CLIのコマンドディスパッチロジックからそれぞれのハンドラーを呼び出すことで実現されます。

// src/main.rs (簡略化された概念コード)

mod cmds;
mod core;
// ...

#[derive(Debug, Subcommand)]
enum Commands {
    // `rtk git ...` を処理する
    Git { #[clap(subcommand)] command: cmds::git::GitCommand },
    // `rtk cargo ...` を処理する
    Cargo { #[clap(subcommand)] command: cmds::rust::CargoCommand },
    // ... 他のコマンド
    Proxy { /* ... */ },
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Git { command } => cmds::git::handle_git_command(command)?,
        Commands::Cargo { command } => cmds::rust::handle_cargo_command(command)?,
        // ...
        _ => unimplemented!(),
    }
    Ok(())
}

このように、clapクレートのSubcommandとRustのモジュールシステムを組み合わせることで、CLIの階層構造とコードベースの物理的なモジュール構造を密接に連携させ、高い凝集度と低い結合度を実現しています。

Traitを用いた戦略パターン

rtkの内部フィルタリングロジックでは、異なる「フィルタレベル」や「コマンド種別」に応じて最適な出力処理を行う必要があります。これを実現するために、概念的に以下のようなTraitと実装が考えられます。

graph TD A["FilterContext (core::runner.rs)"] --> B["OutputFilter Trait"]; B <|-- C["MinimalFilterStrategy"]; B <|-- D["AggressiveFilterStrategy"]; B <|-- E["GitStatusFilterStrategy"]; B <|-- F["CargoTestFilterStrategy"]; C --> G["apply(input: &str) -> String"]; D --> H["apply(input: &str) -> String"]; E --> I["apply(input: &str) -> String"]; F --> J["apply(input: &str) -> String"];

FilterContextは、OutputFilter Traitオブジェクト(&dyn OutputFilter)を受け取ることで、具体的なフィルタリングロジックに依存せずに処理を実行できます。これにより、新しいフィルタリング戦略の導入や、既存戦略の変更が容易になります。例えば、rtk proxyコマンドは生の出力をそのまま通しつつ計測を行うような「無操作フィルタリング戦略」を適用することもできます。

補足: rtkのコードでは、src/core/filter.rssrc/core/toml_filter.rsがフィルタリングの核となっており、TOMLファイルで定義されたルールをロードして適用するメカニズムがあります。これは、Traitベースの戦略パターンと組み合わせることで、非常に柔軟でデータ駆動型のフィルタリングシステムを構築できます。つまり、Traitがフィルタリングの「インターフェース」を、TOMLがフィルタリングの「設定データ」を提供する形です。

6. 実務に持ち帰れるTips

  1. モジュール構造の早期設計: プロジェクト初期からModule-per-Domainを意識し、責務を明確にすることで、将来的な拡張と保守が格段に楽になります。特にCLIツールやライブラリでは、サブコマンドや機能ごとにモジュールを分けるのが効果的です。
  2. Traitで振る舞いを抽象化: 異なる実装を持つが共通のインターフェースで扱いたいロジック(例: データ保存、API呼び出し、フィルタリング)にはTraitを使用しましょう。これにより、コードの再利用性が高まり、テストが容易になります。
  3. 戦略パターンによる柔軟性の確保: 実行時に切り替え可能なアルゴリズムや設定(例: ログレベル、キャッシュ戦略、データ変換)が必要な場合、Traitで戦略パターンを適用することで、コードの変更を最小限に抑えつつ対応できます。
  4. clapValueEnum活用: CLI引数に列挙型を使うことで、型安全で表現豊かなCLIインターフェースを構築できます。これは、ユーザーフレンドリーなだけでなく、内部ロジックでの条件分岐を明確にする効果もあります。
  5. テスト容易性の向上: モジュール分割とTraitによる抽象化は、各コンポーネントを独立して単体テストすることを容易にします。特にTraitはモックやスタブの導入を支援し、複雑な依存関係を持つシステムのテスト負担を軽減します。

7. トレードオフと注意点

8. まとめ

rtkは、その堅牢なCLI設計と高性能化戦略に加え、RustのモジュールシステムとTraitを巧みに組み合わせることで、多種多様なCLIコマンドに対応する高い拡張性と保守性を実現しています。ドメインごとのモジュール分割はコードベースを整理し、Traitを用いた戦略パターンは、柔軟なフィルタリングロジックの切り替えを可能にします。

これらの設計パターンは、単にコードを動かすだけでなく、将来の機能追加や変更に強く、チームでの開発を容易にするための重要な基盤となります。皆さんのRustプロジェクトにおいても、これらのパターンを参考に、より堅牢で拡張性の高いシステム構築を目指してください。