1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【5/37】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第5回になります!

記事投稿を溜めすぎてしまったため、本回から解説を軽くできるところは軽くしていく所存です、ご了承ください :bow:

今回の関連ページ

では早速参りましょう!

[03_ticket_v1/02_validation] バリデーション

問題はこちらです。

lib.rs
struct Ticket {
    title: String,
    description: String,
    status: String,
}

impl Ticket {
    // TODO: implement the `new` function.
    //  The following requirements should be met:
    //   - Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
    //   - The `title` and `description` fields should not be empty.
    //   - the `title` should be at most 50 bytes long.
    //   - the `description` should be at most 500 bytes long.
    //  The method should panic if any of the requirements are not met.
    //
    // You'll have to use what you learned in the previous exercises,
    // as well as some `String` methods. Use the documentation of Rust's standard library
    // to find the most appropriate options -> https://doc.rust-lang.org/std/string/struct.String.html
    fn new(title: String, description: String, status: String) -> Self {
        todo!();
        Self {
            title,
            description,
            status,
        }
    }
}
テストを含めた全体
lib.rs
struct Ticket {
    title: String,
    description: String,
    status: String,
}

impl Ticket {
    // TODO: implement the `new` function.
    //  The following requirements should be met:
    //   - Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
    //   - The `title` and `description` fields should not be empty.
    //   - the `title` should be at most 50 bytes long.
    //   - the `description` should be at most 500 bytes long.
    //  The method should panic if any of the requirements are not met.
    //
    // You'll have to use what you learned in the previous exercises,
    // as well as some `String` methods. Use the documentation of Rust's standard library
    // to find the most appropriate options -> https://doc.rust-lang.org/std/string/struct.String.html
    fn new(title: String, description: String, status: String) -> Self {
        todo!();
        Self {
            title,
            description,
            status,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use common::{overly_long_description, overly_long_title, valid_description, valid_title};

    #[test]
    #[should_panic(expected = "Title cannot be empty")]
    fn title_cannot_be_empty() {
        Ticket::new("".into(), valid_description(), "To-Do".into());
    }

    #[test]
    #[should_panic(expected = "Description cannot be empty")]
    fn description_cannot_be_empty() {
        Ticket::new(valid_title(), "".into(), "To-Do".into());
    }

    #[test]
    #[should_panic(expected = "Title cannot be longer than 50 bytes")]
    fn title_cannot_be_longer_than_fifty_chars() {
        Ticket::new(overly_long_title(), valid_description(), "To-Do".into());
    }

    #[test]
    #[should_panic(expected = "Description cannot be longer than 500 bytes")]
    fn description_cannot_be_longer_than_500_chars() {
        Ticket::new(valid_title(), overly_long_description(), "To-Do".into());
    }

    #[test]
    #[should_panic(expected = "Only `To-Do`, `In Progress`, and `Done` statuses are allowed")]
    fn status_must_be_valid() {
        Ticket::new(valid_title(), valid_description(), "Funny".into());
    }

    #[test]
    fn done_is_allowed() {
        Ticket::new(valid_title(), valid_description(), "Done".into());
    }

    #[test]
    fn in_progress_is_allowed() {
        Ticket::new(valid_title(), valid_description(), "In Progress".into());
    }
}

不正なデータがプログラムに混入しないようにする、いわゆる「バリデーション」の問題ですね。問題指示を和訳要約するとこんな感じです。

  • TODO: new する関数を作成してください!
  • 以下を守ってね:
    • ステータスは To-Do, In Progress, Done のみが許されます!
    • titledescription について空文字は許されません。
    • title は50文字以下
    • description は500文字以下
  • new は上記を満たしていない時パニックさせてください。
  • String 型に関するドキュメントを読むと良いことあるよ! -> https://doc.rust-lang.org/std/string/struct.String.html

解説

lib.rs
struct Ticket {
    title: String,
    description: String,
    status: String,
}

impl Ticket {
    fn new(title: String, description: String, status: String) -> Self {
        // Result型使いたい...!!

        // Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
        // Enum 使いたい...!!
        if status != "To-Do" && status != "In Progress" && status != "Done" {
            panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
        }

        // The `title` and `description` fields should not be empty.
        if title.is_empty() {
            panic!("Title cannot be empty");
        }

        if description.is_empty() {
            panic!("Description cannot be empty");
        }

        // the `title` should be at most 50 bytes long.
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }

        // the `description` should be at most 500 bytes long.
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        Self {
            title,
            description,
            status,
        }
    }
}

コメントにところどころお気持ちが漏れていますが、この問題はRustに慣れている人からすると改善したい点が2つあります...!しかし100 Exercisesでは後ほど紹介されるため今は我慢...ちなみに同じチケット管理システムのプログラムでちゃんと改善します。

  • Result型を使いたい! -> 第15回 5.6. Fallibility
    • パニックは「発生箇所がわかりにくくなる」「一度発生してしまうとコントロールしにくい」という理由よりなるべく避けたいものです...!
  • Enumを使いたい! -> 第13回 5.1. Enums
    • 「特定の値しか取らないフィールド」はRustでは列挙型(Enum)の枕詞です。こちらも後ほど登場します!

どの回であったかは覚えていませんが、エクササイズ本編の方でも「後でもっと良い方法に改善します」と書かれており、「とりあえず手を動かして覚える」という方針で設計されているのだということがわかりますね。

今回は if 式により単に期待する条件を満たしていない時にパニックさせるようにしています。 is_empty という専用メソッドを使ったりしていますが、この手のメソッドはセマンティックな感じがして好きです!(セマンティックって言いたいだけ) ちなみにここで title.len() == 0 みたいに書くとclippy辺りが「 is_empty 使ったほうが良いですよ!」と親切に教えてくれたりします。

new は普通のメソッド!
Rustでも構造体のコンストラクタは今回のように new メソッドとして定義しますが、あくまでも慣例であり new はキーワードになっていません! コンストラクタという厄介な要素ではなく、ただのメソッドだから気軽に呼べる良さがあります!

ちなみにただのメソッドなので、 new があろうがなかろうが今まで通り直接フィールドを指定する形で構造体のいわゆるインスタンスを作成できます。え?じゃあどうやって不正な値の生成を防ぐかって?それは次のエクササイズです!

[03_ticket_v1/03_modules] モジュール

問題はこちらです。

lib.rs
mod helpers {
    // TODO: Make this code compile, either by adding a `use` statement or by using
    //  the appropriate path to refer to the `Ticket` struct.

    fn create_todo_ticket(title: String, description: String) -> Ticket {
        Ticket::new(title, description, "To-Do".into())
    }
}

struct Ticket {
    title: String,
    description: String,
    status: String,
}

impl Ticket {
    fn new(title: String, description: String, status: String) -> Ticket {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }
        if status != "To-Do" && status != "In Progress" && status != "Done" {
            panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
        }

        Ticket {
            title,
            description,
            status,
        }
    }
}

use ステートメントを使用するか、適切なパスを設定するを設定することでコンパイルを通してね!」という問題です。100 Exercisesでは「コンパイルを通してね!」問題がしばしば出るみたいですね。

解説

lib.rs
mod helpers {
    // TODO: Make this code compile, either by adding a `use` statement or by using
    //  the appropriate path to refer to the `Ticket` struct.
+    use super::Ticket;

    fn create_todo_ticket(title: String, description: String) -> Ticket {
        Ticket::new(title, description, "To-Do".into())
    }
}

本問題は先のエクササイズの最後に言及した不正な値の生成を防ぐための書き方、すなわち「 new メソッドで検査せずにそれ以外の方法で構造体インスタンスを生成することを防ぐ方法」の布石になります。

プログラムを適切な「モジュール」単位に分割することで、モジュール外からのアクセスについて制限をかけようというわけです。他言語でいう「 名前空間 」に該当するものなのでそこまで複雑ではないかと思います。

use 文は他のモジュールで定義されている要素を"短く"書ける機能と考えておくとわかりやすいです。つまり、上記の場合使うたびに super::Ticket と書いても良いところを、 Ticket と書いても通じるようにする以上の意味(つまり import みたいな意味)は持ちません。ちなみに as を使うことでエイリアスをつけることも可能です。

lib.rs
mod helpers {
    use super::Ticket as MyTicket;

    fn create_todo_ticket(title: String, description: String) -> MyTicket {
        MyTicket::new(title, description, "To-Do".into())
    }
}

また、 use 文は、関数の中など結構意外な場所に書けます。どこに書けるか探ってみるのも面白いかもしれません。

[03_ticket_v1/04_visibility] use の有効範囲

問題はこちらです。

lib.rs
mod ticket {
    struct Ticket {
        title: String,
        description: String,
        status: String,
    }

    impl Ticket {
        fn new(title: String, description: String, status: String) -> Ticket {
            if title.is_empty() {
                panic!("Title cannot be empty");
            }
            if title.len() > 50 {
                panic!("Title cannot be longer than 50 bytes");
            }
            if description.is_empty() {
                panic!("Description cannot be empty");
            }
            if description.len() > 500 {
                panic!("Description cannot be longer than 500 bytes");
            }
            if status != "To-Do" && status != "In Progress" && status != "Done" {
                panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
            }

            Ticket {
                title,
                description,
                status,
            }
        }
    }
}

// TODO: **Exceptionally**, you'll be modifying both the `ticket` module and the `tests` module
//  in this exercise.
#[cfg(test)]
mod tests {
    // TODO: Add the necessary `pub` modifiers in the parent module to remove the compiler
    //  errors about the use statement below.
    use super::ticket::Ticket;

    // Be careful though! We don't want this function to compile after you have changed
    // visibility to make the use statement compile!
    // Once you have verified that it indeed doesn't compile, comment it out.
    fn should_not_be_possible() {
        let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());

        // You should be seeing this error when trying to run this exercise:
        //
        // error[E0616]: field `description` of struct `Ticket` is private
        //    |
        //    |              assert_eq!(ticket.description, "A description");
        //    |                         ^^^^^^^^^^^^^^^^^^
        //
        // TODO: Once you have verified that the below does not compile,
        //   comment the line out to move on to the next exercise!
        assert_eq!(ticket.description, "A description");
    }

    fn encapsulation_cannot_be_violated() {
        // This should be impossible as well, with a similar error as the one encountered above.
        // (It will throw a compilation error only after you have commented the faulty line
        // in the previous test - next compilation stage!)
        //
        // This proves that `Ticket::new` is now the only way to get a `Ticket` instance.
        // It's impossible to create a ticket with an illegal title or description!
        //
        // TODO: Once you have verified that the below does not compile,
        //   comment the lines out to move on to the next exercise!
        let ticket = Ticket {
            title: "A title".into(),
            description: "A description".into(),
            status: "To-Do".into(),
        };
    }
}

テストの方に色々書いていますが、要約するとモジュール分割によって

  • new を使う方法以外での構造体インスタンス生成はできなくなったよ!
  • もうフィールドに直接アクセスすることはできなくなったよ!

という旨が書かれています。

解説

先程の問題で普通に new メソッドにアクセスできていたのはアクセス先が「親」モジュールだったからでした。子モジュールや親子関係にないモジュールの要素(構造体のフィールドも含みます!)にアクセスするには pub をつける必要があります!

lib.rs
mod ticket {
    pub struct Ticket {
        title: String,
        description: String,
        status: String,
    }

    impl Ticket {
-        fn new(title: String, description: String, status: String) -> Ticket {
+        pub fn new(title: String, description: String, status: String) -> Ticket {
            // ...
        }
    }
}

これで let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into()); の部分はコンパイルが通るようになります。他の行は構造体のプライベートフィールドにアクセスするせいでそもそもコンパイルが通らなくなるので、コメントアウトしましょう。

このように書くと他のモジュールはチケットのフィールドに直接アクセスできなくなってしまいますが、ではどうするかというと他のOOP言語同様、ゲッター :robot: を設けます!それは後のエクササイズのようです。

では次の問題に行きましょう!

次の記事: 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?