mcpField
R
rust-refactor

rust-refactor

github.com/pproenca開発218317 回インストール11 週間前に更新
リポジトリ

rust-refactor について

Rust 開発者向けのリファクタリングスキルです。診断質問、変換パターン、レビュー用チェックリストを使い、不要な複雑さを見つけて減らします。

主な機能

  1. 01防御的・積極的なモード
  2. 024つの診断質問
  3. 036種類のリファクタリング変換
  4. 04ドメイン別のモジュール分割
  5. 05段階的な型移行
  6. 06作業後のレビュー用チェックリスト

活用シーン

  1. 01転送するだけのパラメーターの削除
  2. 02複数の責務を持つファイルの分割
  3. 03レガシー型の段階的な移行
  4. 04排他的なオプションの enum 化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/pproenca/dot-skills --skill rust-refactor

インストール後、Claude Code で/rust-refactorを入力して読み込まれたか確認します。

同梱ファイルAGENTS.mdmetadata.json

詳細

Rust リファクタリングフレームワーク

このスキルでは、動作する Rust コードを見て、不要な複雑さを見つける方法を学びます。すべてのリファクタリングは診断のための問いから始まり、変換パターンに従って進み、最後にセルフレビューチェックリストで締めくくります。


2 つのリファクタリング方針

コードに手を加える前に、どちらのモードで作業するか決めます。

防御モード: 安全性のために抽象化を追加します。型を分割します。From による橋渡しを作成します。すべての利用箇所を監査します。セキュリティが重要なコード、型の進化、複数クレートにまたがる依存関係で使用します。

攻めのモード: 間接参照を削除します。転送されるだけのパラメーターを削除します。レイヤーを統合します。抽象化が安全性を高めず複雑さだけを増している場合、仲介役が単に転送しているだけの場合、パラメーターが常に None の場合に使用します。

どちらのモードで作業しているかを把握してください。削除すべきときに抽象化を追加せず、追加すべきときに安全性を高めるレイヤーを削除しないでください。


4 つの診断の問い

コードに手を加える前に、これらの問いを確認してください。何をリファクタリングするかを判断できます。

1. 「このパラメーターは単に転送されているだけか?」

兆候: 関数本体では、そのパラメーターを別の関数に渡しているだけです。ローカルロジックはそれに依存していません。 対応: パラメーターを削除します。実際に使用する箇所で、環境変数やグローバルアクセスに置き換えます。最下層から削除し、コンパイルエラーを上位の層へ順に修正します。

2. 「この機能フラグはまだ必要か?」

兆候: ステージが Stable または同等の状態で、デフォルトで有効になっており、ロールバックの予定もありません。 対応: フラグを削除します。条件分岐を削除します。フラグで制御されていた関数名を変更します(init_if_enabled -> init)。監視用の足場も削除します。

3. 「このファイルは多くのことをしすぎていないか?」

兆候: ファイルが約 500 行を超えており、異なるドメイン(スレッド AND ログ AND ジョブなど)を扱う impl ブロックが含まれています。 対応: レイヤーではなく、扱うドメインごとに分割します。以下のモジュール分割ガイドを参照してください。

4. 「この構造体には、同時に存在しないはずのオプションフィールドがあるか?」

兆候: 一度に設定されるのは 1 つだけであるべき Option<T> フィールドが 2 つ以上あります。if a.is_some() { assert!(b.is_none()) } のようなコードや、特定のモードでのみ適用される x_config という名前のフィールドがあります。 対応: 各バリアントに関連するデータだけを持たせた enum に変換します。

// BEFORE: invalid states representable
struct Auth { api_key: Option<String>, oauth_token: Option<String>, storage: Option<TokenStore> }
// AFTER: each variant carries only what it needs
enum Auth { ApiKey(String), OAuth { token: String, storage: TokenStore } }

6 つのリファクタリング変換

変換 1:転送されるパラメーター -> 環境経由のアクセス
// BEFORE: metrics threaded through every signature, never used locally
pub async fn process_batch(db: &Database, config: &Config,
    metrics: Option<&MetricsClient>) {           // forwarded
    for item in db.pending_items(config).await? {
        process_item(db, item, metrics).await?;  // forwarded again
    }
}
// AFTER: ambient access at the single point of use
pub async fn process_batch(db: &Database, config: &Config) {
    for item in db.pending_items(config).await? {
        process_item(db, item).await?;
    }
}
pub async fn process_item(db: &Database, item: Item) {
    let result = transform(item)?;
    db.save(result).await?;
    if let Some(m) = metrics::global().as_ref() { m.counter("items_processed", 1); }
}

調整: 最下層から削除します。中間コミットはすべてコンパイルできる状態にします。

変換 2:モノリシックなファイル -> ドメインごとに分割したモジュール
// BEFORE: src/runtime.rs (950 lines, four concerns)
// AFTER:
src/runtime/mod.rs       (~30 lines: struct def, init, re-exports)
src/runtime/threads.rs   (~200 lines)
src/runtime/logs.rs      (~250 lines)
src/runtime/jobs.rs      (~300 lines)
src/runtime/cache.rs     (~120 lines)

各ファイルは、同じ構造体を拡張する impl ブロックです。テストは対応するコードと一緒に移動します。

変換 3:一括での型変更 -> From による段階的な橋渡し
// STEP 1: New types alongside old, with From bridges
impl From<&LegacyPolicy> for NetworkPolicy {
    fn from(value: &LegacyPolicy) -> Self {
        match value {
            LegacyPolicy::FullAccess => NetworkPolicy::Enabled,
            _ => NetworkPolicy::Restricted,
        }
    }
}
// STEP 2: Runtime carries both representations simultaneously
pub struct Permissions {
    pub legacy: LegacyPolicy,         // existing consumers keep working
    pub network_policy: NetworkPolicy, // new consumers use richer types
}

積み重ねた変更 -- 各ステップを、個別にコンパイル可能なコミットとして実施します。

  1. 新しい型と From による橋渡しを追加する
  2. 古い型と並行して、新しい型をランタイム全体に渡す
  3. 利用箇所を 1 つずつ移行する(サブシステムごとに 1 コミット)
  4. すべての利用箇所の移行が完了してから、レガシー型を削除する
変換 4:不要になった機能フラグ -> すっきりと削除
// BEFORE: init_if_enabled(config) -> Option<Handle>
// AFTER:  init(config) -> Handle  (unconditional, renamed)

完全なクリーンアップ手順:

  1. すべての呼び出し箇所からフラグのチェックを削除する
  2. フラグで制御されていた関数名を変更する:init_if_enabled -> init
  3. 戻り値の型を簡素化する:Option<Handle> -> Handle
  4. 機能レジストリでフラグを Stage::Removed に移動する
  5. 条件分岐を完全に削除する(デフォルトを変更するだけにしない)
  6. Cargo.toml の #[cfg(feature = "...")] を確認する -- これらの依存関係を常時有効にする
  7. 旧経路と新経路を追跡していた比較・差異メトリクスを削除する
  8. 機能フラグの定義を削除する

順序:リリース -> 安定化 -> 構造を整理 -> 最適化 -> フラグを削除 -> 足場を削除。

変換 5:計測を伴うクレートの抽出

コードの整理だけではなく、ビルドのパフォーマンスを改善します。

# 1. Measure baseline
cargo build -p parent-crate --timings   # record check + test compile time
# 2. Extract: create new crate, move code, add re-exports for backward compat
# 3. Measure after
cargo build -p parent-crate --timings   # compare
# 4. Report in commit message:
#    cargo check: 57.08s -> 53.54s (~6.2% faster)
#    cargo test --no-run: 2m39.9s -> 2m20s (~12.4% faster)

抽出の手順:

  1. 依存関係が自己完結している、ドメインとして一貫性のある境界を特定します
  2. 最小限の Cargo.toml を含む新しいクレートを作成します
  3. コードを移動し、後方互換性を保つために元のクレートで再エクスポートします
  4. ワークスペースのマニフェストとロックファイルを更新します
  5. 次のコマンドで検証します:cargo test -p parent-crate && cargo test -p new-crate
変換 6:相互排他的なオプションから列挙型へ
// BEFORE: three options, only one valid at a time
struct ShellMode {
    direct_cmd: Option<String>,
    zsh_fork_config: Option<ZshForkConfig>,
    pty_handle: Option<PtyHandle>,
}
// AFTER: disjoint union, invalid states unrepresentable
enum ShellMode { Direct(String), ZshFork(ZshForkConfig), Pty(PtyHandle) }

手順: 有効な組み合わせについて、構築箇所を監査する → 列挙型のバリアントを定義する → 構造体の構築を置き換える → if x.is_some() を match に置き換える → 防御的なアサーションを削除する。


「配線し直すのではなく、書き直す」原則

バグが関数の配線ではなくロジックにある場合は、明示的な順序付きチェックを使って本体を書き直します。たまたま正しい結果を返す既存の API に委ねないでください。

// WRONG: rewire through existing API ("happens to work")
fn resolve_policy(input: &Input) -> Policy {
    default_policy(input).override_with(input.overrides())
}
// RIGHT: explicit ordered checks
fn resolve_policy(input: &Input) -> Policy {
    if input.is_admin() { return Policy::FullAccess; }
    if input.has_restriction("network") { return Policy::NetworkDenied; }
    if input.is_sandbox_mode() { return Policy::ReadOnly; }
    Policy::Standard
}

明示的な実装にすることで、監査やテストがしやすくなり、委譲先の実装が将来変更された場合にも、より堅牢になります。


モジュール分割ガイド

アーキテクチャ上のレイヤーではなく、何を操作するコードなのかに基づいてグループ化します。各ドメインファイルには、共有構造体のそのドメインのメソッドに関する impl ブロックを含めます。

誤り:models.rs / services.rs / controllers.rs(レイヤーごとの分割)。 正しい例:runtime/threads.rs / runtime/logs.rs / runtime/jobs.rs(ドメインごとの分割)。

ルール:

  • テストはコードと一緒に移動します。元のファイルにテストを残してはいけません。
  • mod.rs には再エクスポートのみを記述します。目安は約30行です。
  • 各ファイルは500行未満にします。超える場合はサブドメインに分割します。
  • 関連のない引数を並べるのではなく、名前付きの型を使います。関連する値が2つ以上常に一緒に渡される場合は、構造体にまとめます。
  • 抽出コミットではロジックを変更しません。コードをひとかたまりで移動します。cargo test で検証します。

セルフレビューのチェックリスト

リファクタリングのたびに実行します。すべての項目に合格する必要があります。

After refactoring, verify:
[ ] Every intermediate state compiles (no big-bang rewrites)
[ ] From bridges exist for any split types
[ ] Tests moved with their code (not left behind)
[ ] No new single-use helper functions introduced
[ ] Removed more code than you added (or justified why not)
[ ] No forwarded parameters remain (each param is used locally)
[ ] Module re-exports are clean (public API in mod.rs)
[ ] Feature flags for stable features removed
[ ] Structs with mutually exclusive options converted to enums
[ ] Build performance measured before/after crate extraction
[ ] Feature flag removal includes function renames and branch deletion
[ ] Chose the right refactoring mode (defensive vs offensive)
[ ] Function logic rewritten, not just rewired through delegation

1つでも不合格なら、まだ完了していません。リファクタリングの完了を宣言する前に修正してください。

コメント