モジューラリティとエラーハンドリングの改善のためのリファクタリング

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

モジューラリティとエラーハンドリングの改善のためのリファクタリングへようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。

この実験では、タスクを分離し、構成変数をグループ化し、意味のあるエラーメッセージを提供し、エラーハンドリングコードを統合することで、プログラムをリファクタリングして、モジューラリティとエラーハンドリングを改善します。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/ErrorHandlingandDebuggingGroup(["Error Handling and Debugging"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/DataTypesGroup -.-> rust/tuple_type("Tuple Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/ErrorHandlingandDebuggingGroup -.-> rust/panic_usage("panic! Usage") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/string_type -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/tuple_type -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/function_syntax -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/expressions_statements -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/method_syntax -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/panic_usage -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/traits -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} rust/operator_overloading -.-> lab-100420{{"モジューラリティとエラーハンドリングの改善のためのリファクタリング"}} end

モジューラリティとエラーハンドリングの改善のためのリファクタリング

プログラムを改善するために、プログラムの構造と潜在的なエラーの処理方法に関係する4つの問題を修正します。まず、main関数は現在2つのタスクを実行しています。引数を解析し、ファイルを読み取ります。プログラムが拡大するにつれて、main関数が処理する個別のタスクの数が増えます。関数が責任を担うようになると、推論しにくくなり、テストしにくくなり、一部を破壊することなく変更しにくくなります。機能を分離するのが最善で、各関数は1つのタスクに責任を持つようにします。

この問題はまた、2番目の問題にも関係しています。queryfile_pathはプログラムの構成変数ですが、contentsのような変数はプログラムのロジックを実行するために使用されます。mainが長くなるほど、スコープに持ち込む必要のある変数が増えます。スコープ内にある変数が多いほど、各変数の目的を把握するのが難しくなります。構成変数を1つの構造体にグループ化して目的を明確にするのが最善です。

3番目の問題は、ファイルの読み取りに失敗したときにexpectを使用してエラーメッセージを表示していますが、エラーメッセージはただShould have been able to read the fileと表示されます。ファイルの読み取りに失敗する場合がいくつかあります。たとえば、ファイルが存在しない場合や、それを開く権限がない場合などです。現在のところ、状況に関係なく、すべてに同じエラーメッセージを表示しており、ユーザーには何の情報も与えていません!

4番目の問題は、異なるエラーを処理するためにexpectを繰り返し使用していることです。ユーザーが十分な引数を指定せずにプログラムを実行すると、Rustからindex out of boundsエラーが表示されますが、これは問題を明確に説明していません。エラーハンドリングロジックを変更する必要がある場合、すべてのエラーハンドリングコードを1か所にまとめておくと、将来の保守担当者がコードを参照する場所が1か所だけになるため最適です。すべてのエラーハンドリングコードを1か所にまとめることで、最終ユーザーにとって意味のあるメッセージを表示することも保証されます。

これらの4つの問題に対処するために、プロジェクトをリファクタリングしましょう。

バイナリプロジェクトの関心事の分離

複数のタスクの責任をmain関数に割り当てるという組織的な問題は、多くのバイナリプロジェクトに共通しています。その結果、Rustコミュニティは、mainが大きくなり始めたときにバイナリプログラムの個別の関心事を分割するためのガイドラインを開発しました。このプロセスには次のステップがあります。

  • プログラムをmain.rsファイルとlib.rsファイルに分割し、プログラムのロジックをlib.rsに移動します。
  • コマンドライン解析ロジックが小さければ、main.rsに残しておくことができます。
  • コマンドライン解析ロジックが複雑になり始めたときに、それをmain.rsから抽出してlib.rsに移動します。

このプロセスの後、main関数に残る責任は以下のものに限定されるはずです。

  • 引数値でコマンドライン解析ロジックを呼び出すこと
  • その他の設定を行うこと
  • lib.rs内のrun関数を呼び出すこと
  • runがエラーを返した場合のエラー処理を行うこと

このパターンは関心事の分離に関するものです。main.rsはプログラムの実行を担当し、lib.rsは手元のタスクのすべてのロジックを担当します。main関数を直接テストすることができないため、この構造により、プログラムのすべてのロジックをlib.rs内の関数に移動することでテストすることができます。main.rsに残るコードは十分に小さく、読むことでその正しさを検証することができます。このプロセスに従ってプログラムを再構築しましょう。

引数解析の抽出

コマンドライン引数を解析する機能を、mainがコマンドライン解析ロジックをsrc/lib.rsに移動する準備として呼び出す関数に抽出します。リスト12-5は、mainの新しい開始部分を示しており、新しい関数parse_configを呼び出しています。この関数は、当面はsrc/main.rsで定義します。

ファイル名: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    --snip--
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}

リスト12-5: mainからparse_config関数を抽出する

コマンドライン引数をまだベクトルに収集していますが、main関数内でインデックス1の引数値をquery変数に、インデックス2の引数値をfile_path変数に代入する代わりに、ベクトル全体をparse_config関数に渡します。そしてparse_config関数が、どの引数がどの変数に入るかを決定するロジックを保持し、値をmainに戻します。main内では依然としてqueryfile_path変数を作成しますが、コマンドライン引数と変数がどのように対応するかを決定する責任はmainにはもはやありません。

この再構築は、私たちの小さなプログラムにとってはやりすぎかのように見えるかもしれませんが、小さな段階で徐々にリファクタリングしています。この変更を行った後、再度プログラムを実行して、引数解析がまだ機能することを確認しましょう。問題が発生したときに原因を特定するのに役立つように、頻繁に進捗状況を確認するのは良いことです。

構成値のグループ化

parse_config関数をさらに改善するために、もう少し小さなステップを踏みましょう。現在、タプルを返していますが、その後すぐにそのタプルを再び個々の部分に分割しています。これは、まだ正しい抽象化ができていないことの兆候です。

改善の余地があることを示すもう一つの指標は、parse_configconfig部分です。これは、返す2つの値が関連しており、両方とも1つの構成値の一部であることを意味します。現在、データの構造では、2つの値をタプルにグループ化する以外に、この意味を伝えていません。代わりに、2つの値を1つの構造体に入れ、構造体の各フィールドに意味のある名前を付けます。これにより、このコードの将来の保守担当者が、異なる値がどのように関連しているか、そしてそれらの目的が何かを理解するのが容易になります。

リスト12-6は、parse_config関数の改善点を示しています。

ファイル名: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

  1 let config = parse_config(&args);

    println!("Searching for {}", 2 config.query);
    println!("In file {}", 3 config.file_path);

    let contents = fs::read_to_string(4 config.file_path)
       .expect("Should have been able to read the file");

    --snip--
}

5 struct Config {
    query: String,
    file_path: String,
}

6 fn parse_config(args: &[String]) -> Config {
  7 let query = args[1].clone();
  8 let file_path = args[2].clone();

    Config { query, file_path }
}

リスト12-6: parse_configを再構築してConfig構造体のインスタンスを返す

queryfile_pathという名前のフィールドを持つように定義されたConfigという名前の構造体を追加しました[5]。parse_configのシグネチャは、現在、Config値を返すことを示しています[6]。parse_configの本体では、以前はargs内のString値を参照する文字列スライスを返していましたが、今ではConfigを定義して所有するString値を含めるようにしました。main内のargs変数は引数値の所有者であり、parse_config関数にのみ借りて使用させています。これは、Configargs内の値の所有権を取得しようとすると、Rustの借用規則に違反することを意味します。

Stringデータを管理する方法はいくつかありますが、最も簡単ですが多少非効率的な方法は、値に対してcloneメソッドを呼び出すことです[7] [8]。これにより、Configインスタンスが所有するデータの完全なコピーが作成され、文字列データへの参照を格納するよりも時間とメモリが多く必要になります。ただし、データをクローンすることでコードが非常に単純になります。なぜなら、参照の生存時間を管理する必要がないからです。このような状況では、単純さを得るために少しのパフォーマンスを犠牲にするのは価値のあるトレードオフです。

cloneを使用するトレードオフ

多くのRustプログラマーは、実行時のコストのために、所有権の問題を解決するためにcloneを使用することを避ける傾向があります。13章では、この種の状況でより効率的な方法を学びます。しかし、今のところ、進歩を続けるためにいくつかの文字列をコピーすることは大丈夫です。なぜなら、これらのコピーは一度だけ行い、ファイルパスとクエリ文字列は非常に小さいからです。最初の段階でコードを超最適化しようとするよりも、少し非効率的な稼働プログラムを持つ方が良いです。Rustに慣れるにつれて、最も効率的なソリューションから始めることが容易になりますが、今のところ、cloneを呼び出すことは完全に許容されます。

mainを更新して、parse_configが返すConfigのインスタンスをconfigという名前の変数に格納し[1]、以前は個別のqueryfile_path変数を使用していたコードを更新して、現在は代わりにConfig構造体のフィールドを使用するようにしました[2] [3] [4]。

今では、コードがより明確に伝えているのは、queryfile_pathが関連しており、それらの目的はプログラムの動作方法を構成することであるということです。これらの値を使用するコードは、目的に応じた名前のフィールドのconfigインスタンスからそれらを見つけることを知っています。

Configのコンストラクタの作成

これまで、コマンドライン引数を解析する責任のあるロジックをmainから抽出し、parse_config関数に配置しました。これにより、queryfile_pathの値が関連していることがわかり、その関係をコードで表現する必要があることがわかりました。そこで、queryfile_pathの関連する目的を名前付けするためにConfig構造体を追加し、parse_config関数から値の名前を構造体フィールド名として返せるようにしました。

ですから、今ではparse_config関数の目的がConfigインスタンスを作成することになっているので、parse_configを単なる関数からConfig構造体に関連付けられたnewという名前の関数に変更することができます。この変更により、コードがより慣例に沿ったものになります。標準ライブラリの型(たとえばString)のインスタンスを作成するには、String::newを呼び出します。同様に、parse_configConfigに関連付けられたnew関数に変更することで、Config::newを呼び出すことでConfigのインスタンスを作成できるようになります。リスト12-7は必要な変更を示しています。

ファイル名: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

  1 let config = Config::new(&args);

    --snip--
}

--snip--

2 impl Config {
  3 fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

リスト12-7: parse_configConfig::newに変更する

mainを更新して、parse_configを呼び出していた部分をConfig::newを呼び出すように変更しました[1]。parse_configの名前をnewに変更し[3]、implブロック内に移動しました[2]。これにより、new関数がConfigと関連付けられます。このコードを再コンパイルして、正常に動作することを確認してみてください。

エラーハンドリングの修正

次に、エラーハンドリングを修正しましょう。argsベクトルのインデックス1またはインデックス2の値にアクセスしようとすると、ベクトルに3つ未満の要素が含まれている場合、プログラムがパニックになります。引数なしでプログラムを実行してみてください。出力は次のようになります。

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but
the index is 1', src/main.rs:27:21
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

index out of bounds: the len is 1 but the index is 1という行は、プログラマー向けのエラーメッセージです。最終ユーザーにとって、代わりに何をすべきかを理解するのに役立ちません。今すぐそれを修正しましょう。

エラーメッセージの改善

リスト12-8では、new関数にチェックを追加して、インデックス1とインデックス2にアクセスする前にスライスが十分に長いことを確認します。スライスが十分に長くなければ、プログラムはパニックになり、より良いエラーメッセージが表示されます。

ファイル名: src/main.rs

--snip--
fn new(args: &[String]) -> Config {
    if args.len() < 3 {
        panic!("not enough arguments");
    }
    --snip--

リスト12-8: 引数の数のチェックを追加する

このコードは、リスト9-13で書いたGuess::new関数に似ています。そこでは、value引数が有効な値の範囲外の場合にpanic!を呼び出していました。ここでは値の範囲をチェックする代わりに、argsの長さが少なくとも3であることをチェックし、この条件が満たされているという前提のもとで関数の残りの部分を動作させます。argsに3つ未満の要素が含まれている場合、この条件はtrueになり、panic!マクロを呼び出してプログラムを即座に終了します。

newにこれらの数行のコードを追加したので、もう一度引数なしでプログラムを実行して、今のエラーの様子を見てみましょう。

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments',
src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

この出力は良くなりました。今では合理的なエラーメッセージがあります。ただし、ユーザーに与える必要のない余分な情報もあります。おそらく、リスト9-13で使用した手法はここでは最適なものではないかもしれません。第9章で説明したように、panic!の呼び出しは、使用問題よりもプログラミング上の問題に対してより適切です。代わりに、第9章で学んだもう一つの手法を使用します。つまり、成功またはエラーを示すResultを返すことです。

panic!を呼び出す代わりにResultを返す

代わりに、Result値を返します。成功した場合にはConfigインスタンスを含み、エラーの場合には問題を説明します。また、関数名をnewからbuildに変更します。なぜなら、多くのプログラマーがnew関数は決して失敗しないことを期待しているからです。Config::buildmainに通信する際、Result型を使って問題があることを知らせることができます。そして、mainを変更して、Errバリアントをユーザーにとってより実用的なエラーに変換します。panic!の呼び出しが引き起こすthread'main'RUST_BACKTRACEに関する周辺のテキストなしです。

リスト12-9は、現在Config::buildと呼び出している関数の返り値と、Resultを返すために必要な関数の本体に対して行う変更を示しています。ただし、mainも更新しない限りコンパイルされません。次のリストでそれを行います。

ファイル名: src/main.rs

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

リスト12-9: Config::buildからResultを返す

build関数は、成功した場合にはConfigインスタンスを含むResultを返し、エラーの場合には&'static strを返します。エラー値は常に'static寿命を持つ文字列リテラルになります。

関数の本体で2つの変更を行いました。ユーザーが十分な引数を渡さない場合にpanic!を呼び出す代わりに、今ではErr値を返し、Configの返り値をOkでラップしました。これらの変更により、関数が新しい型シグネチャに準拠するようになりました。

Config::buildからErr値を返すことで、main関数はbuild関数から返されるResult値を処理し、エラーの場合に処理をよりきれいに終了させることができます。

Config::buildを呼び出してエラーを処理する

エラーケースを処理してユーザーに親切なメッセージを表示するには、mainを更新してConfig::buildが返すResultを処理する必要があります。これはリスト12-10に示されています。また、コマンドラインツールを非ゼロのエラーコードで終了させる責任をpanic!から外し、代わりに手動で実装します。非ゼロの終了ステータスは、プログラムがエラー状態で終了したことを呼び出し元のプロセスに知らせるための慣例です。

ファイル名: src/main.rs

1 use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

  2 let config = Config::build(&args).3 unwrap_or_else(|4 err| {
      5 println!("Problem parsing arguments: {err}");
      6 process::exit(1);
    });

    --snip--

リスト12-10: Configの構築に失敗した場合にエラーコードで終了する

このリストでは、まだ詳細を説明していないメソッドを使用しています。unwrap_or_elseです。これは、標準ライブラリによってResult<T, E>に定義されています[2]。unwrap_or_elseを使用することで、panic!ではないカスタムのエラーハンドリングを定義できます。ResultOk値の場合、このメソッドの動作はunwrapと似ています。つまり、Okがラップしている内部値を返します。ただし、値がErr値の場合、このメソッドはクロージャ内のコードを呼び出します。クロージャは、定義してunwrap_or_elseに引数として渡す匿名関数です[3]。第13章でクロージャについてもっと詳しく説明します。今のところ、unwrap_or_elseErrの内部値を渡すことだけを知っておけば十分です。この場合、それはリスト12-9で追加した静的文字列"not enough arguments"で、縦棒の間に表示される引数errのクロージャに渡されます[4]。その後、クロージャ内のコードは実行時にerr値を使用できます。

標準ライブラリからprocessをスコープ内に持ち込むために新しいuse行を追加しました[1]。エラーケースで実行されるクロージャ内のコードは2行だけです。err値を表示し[5]、その後process::exitを呼び出します[6]。process::exit関数は、プログラムを即座に停止し、終了ステータスコードとして渡された数を返します。これは、リスト12-8で使用したpanic!ベースのハンドリングに似ていますが、もはや余分な出力は得られません。試してみましょう。

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

素晴らしい!この出力はユーザーにとってははるかに親切です。

mainからのロジックの抽出

コンフィグの解析のリファクタリングが完了したので、次にプログラムのロジックに目を向けましょう。「バイナリプロジェクトの懸念事項の分離」で述べたように、runという名前の関数を抽出します。この関数には、現在main関数に含まれているコンフィグの設定やエラーの処理に関係のないすべてのロジックを保持させます。完了したら、mainは簡潔になり、検査によって容易に検証できるようになります。そして、他のすべてのロジックに対してテストを書くことができるようになります。

リスト12-11に抽出したrun関数を示します。今のところ、関数を抽出するという小さな段階的な改善を行っています。まだsrc/main.rsに関数を定義しています。

ファイル名: src/main.rs

fn main() {
    --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
     .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

--snip--

リスト12-11: プログラムの残りのロジックを含むrun関数を抽出する

run関数には、今やmainから残ったすべてのロジックが含まれています。ファイルの読み込みから始まります。run関数はConfigインスタン스를引数として受け取ります。

run関数からのエラーの返却

残りのプログラムロジックをrun関数に分離することで、エラーハンドリングを改善することができます。これは、リスト12-9でConfig::buildを行ったようにです。何か問題が起こったときにexpectを呼び出すことでプログラムをパニックにさせる代わりに、run関数はエラーが発生したときにResult<T, E>を返します。これにより、エラー処理をユーザーにとって親切な方法でmainにさらに統合することができます。リスト12-12には、runのシグネチャと本体に対して行う必要がある変更を示しています。

ファイル名: src/main.rs

1 use std::error::Error;

--snip--

2 fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)3?;

    println!("With text:\n{contents}");

  4 Ok(())
}

リスト12-12: Resultを返すようにrun関数を変更する

ここでは3つの大きな変更を行っています。まず、run関数の返却型をResult<(), Box<dyn Error>>に変更しました[2]。この関数は以前、ユニット型()を返していましたが、Okの場合に返される値としてこれを維持しています。

エラー型については、トレイトオブジェクトBox<dyn Error>を使用しました(トップのuse文でstd::error::Errorをスコープ内に持ち込んでいます[1])。第17章でトレイトオブジェクトについて説明します。今のところ、Box<dyn Error>は関数がErrorトレイトを実装する型を返すことを意味するだけで、返り値がどのような特定の型であるかを指定する必要はありません。これにより、異なるエラーケースで異なる型のエラー値を返す柔軟性が得られます。dynキーワードは「動的」の略です。

第二に、第9章で説明したように、expectの呼び出しを?演算子に置き換えました[3]。エラーが発生したときにpanic!する代わりに、?は呼び出し元が処理するために現在の関数からエラー値を返します。

第三に、run関数は成功した場合にOk値を返します[4]。シグネチャでrun関数の成功型を()と宣言しているため、ユニット型の値をOk値でラップする必要があります。このOk(())構文は最初は少々奇妙に見えるかもしれませんが、このように()を使用するのは、副作用のためだけにrunを呼び出していることを示す慣用的な方法です。返り値は必要なものではありません。

このコードを実行すると、コンパイルは成功しますが、警告が表示されます。

warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be
handled

Rustは、コードがResult値を無視しており、Result値がエラーが発生したことを示す可能性があることを教えてくれます。しかし、エラーが発生したかどうかを確認しておらず、コンパイラがここにエラー処理コードが必要だったはずだと思い返させてくれます! では、この問題を解決しましょう。

mainにおけるrunから返されるエラーの処理

エラーをチェックして、リスト12-10でConfig::buildに対して使用したものと同様の手法を使って処理しますが、少し違いがあります。

ファイル名: src/main.rs

fn main() {
    --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

runErr値を返すかどうかをチェックし、返した場合にprocess::exit(1)を呼び出すために、unwrap_or_elseではなくif letを使用します。run関数は、Config::buildConfigインスタンスを返すのと同じように、unwrapしたい値を返しません。runは成功した場合に()を返すため、エラーを検出することだけに関心があります。したがって、unwrap_or_elseが展開された値(これはただの()になります)を返す必要はありません。

if letunwrap_or_else関数の本体はどちらの場合も同じです。エラーを表示して終了します。

コードをライブラリクレートに分割する

これまでのところ、私たちのminigrepプロジェクトは順調に進んでいます! ここでは、src/main.rsファイルを分割し、一部のコードをsrc/lib.rsファイルに移動します。これにより、コードをテストできるようになり、責任が少ないsrc/main.rsファイルを持つことができます。

src/main.rsにあるmain関数に含まれていないすべてのコードをsrc/lib.rsに移動しましょう。

  • run関数の定義
  • 関連するuse
  • Configの定義
  • Config::build関数の定義

src/lib.rsの内容は、リスト12-13に示すシグネチャになるはずです(簡略化のため、関数の本体は省略しています)。これは、リスト12-14でsrc/main.rsを修正するまではコンパイルされません。

ファイル名: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
}

impl Config {
    pub fn build(
        args: &[String],
    ) -> Result<Config, &'static str> {
        --snip--
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    --snip--
}

リスト12-13: Configrunsrc/lib.rsに移動する

pubキーワードを頻繁に使用しています。Config、そのフィールドとbuildメソッド、およびrun関数に対してです。これで、テストできるパブリックAPIを持つライブラリクレートができました!

次に、src/lib.rsに移動したコードをsrc/main.rsのバイナリクレートのスコープに持ち込む必要があります。これは、リスト12-14に示すようになります。

ファイル名: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    --snip--
    if let Err(e) = minigrep::run(config) {
        --snip--
    }
}

リスト12-14: src/main.rsminigrepライブラリクレートを使用する

use minigrep::Config行を追加して、ライブラリクレートからConfig型をバイナリクレートのスコープに持ち込み、run関数にクレート名を接頭辞として付けます。これで、すべての機能が接続され、正常に動作するはずです。cargo runでプログラムを実行し、すべてが正しく動作することを確認しましょう。

えーっと! 大変な作業でしたが、これで将来の成功のための基盤が整いました。今ではエラー処理がはるかに簡単になり、コードもよりモジュール化されています。これ以降、ほとんどの作業はsrc/lib.rsで行われます。

この新しいモジューラリティを生かして、古いコードでは難しかったことを新しいコードでは簡単にできることを行いましょう。テストを書きます!

まとめ

おめでとうございます! モジューラリティとエラーハンドリングの改善のためのリファクタリングの実験を完了しました。LabExでさらに多くの実験を行って、スキルを向上させることができます。