オブジェクト指向のデザインパターンを実装する

Beginner

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

はじめに

オブジェクト指向のデザインパターンの実装へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。

この実験では、オブジェクト指向のデザインにおいて状態パターンを実装して、ブログ投稿の構造体を作成します。この構造体は、その動作に基づいて異なる状態(下書き、レビュー、公開)を経て遷移し、公開されたブログ投稿のみがコンテンツを返すことができるようにします。

オブジェクト指向のデザインパターンの実装

「状態パターン」はオブジェクト指向のデザインパターンです。このパターンの要点は、値が内部的に持つことができる一連の状態を定義することです。状態は一連の「状態オブジェクト」で表され、値の動作はその状態に基づいて変化します。ここでは、ブログ投稿の構造体の例を通じて作業します。この構造体には、状態を保持するフィールドがあり、これは「下書き」、「レビュー」、または「公開」のセットからの状態オブジェクトになります。

状態オブジェクトは機能を共有します。もちろん Rust では、オブジェクトや継承ではなく構造体とトレイトを使用します。各状態オブジェクトは独自の動作と、他の状態に変化するタイミングを管理する責任があります。状態オブジェクトを保持する値は、状態の異なる動作や状態間の遷移タイミングについて何も知りません。

状態パターンを使用する利点は、プログラムのビジネス要件が変更された場合、状態を保持する値のコードや値を使用するコードを変更する必要がないことです。状態オブジェクトの 1 つの内部コードを更新してルールを変更するか、もしくは状態オブジェクトを追加するだけです。

まず、より伝統的なオブジェクト指向の方法で状態パターンを実装します。その後、Rust でより自然なアプローチを使用します。状態パターンを使ってブログ投稿のワークフローを段階的に実装していきましょう。

最終的な機能は以下のようになります。

  1. ブログ投稿は空の下書きから始まります。
  2. 下書きが完了すると、投稿のレビューが依頼されます。
  3. 投稿が承認されると、公開されます。
  4. 公開されたブログ投稿のみがコンテンツを返して印刷できるようになり、承認されていない投稿は誤って公開されることがなくなります。

投稿に対して試みられる他の変更は、何の影響も与えません。たとえば、レビューを依頼する前に下書きのブログ投稿を承認しようとした場合、投稿は未公開の下書きのままでなければなりません。

リスト 17-11 は、このワークフローをコード形式で示しています。これは、blogという名前のライブラリクレートで実装する API の使用例です。まだコンパイルされません。なぜなら、blogクレートを実装していないからです。

ファイル名:src/main.rs

use blog::Post;

fn main() {
  1 let mut post = Post::new();

  2 post.add_text("I ate a salad for lunch today");
  3 assert_eq!("", post.content());

  4 post.request_review();
  5 assert_eq!("", post.content());

  6 post.approve();
  7 assert_eq!("I ate a salad for lunch today", post.content());
}

リスト 17-11: 私たちが望むblogクレートの動作を示すコード

Post::newを使って新しい下書きのブログ投稿を作成できるようにしたいと思います[1]。ブログ投稿にテキストを追加できるようにしたいと思います[2]。承認前に投稿のコンテンツをすぐに取得しようとした場合、投稿がまだ下書きなので何のテキストも取得できないはずです。このコードには、示す目的でassert_eq!を追加しています[3]。これに対する優れた単体テストは、下書きのブログ投稿がcontentメソッドから空の文字列を返すことをアサートすることですが、この例ではテストを書きません。

次に、投稿のレビュー依頼を可能にしたいと思います[4]。レビュー待ちの間、contentが空の文字列を返すようにしたいと思います[5]。投稿が承認されると[6]、公開されるはずで、contentを呼び出したときに投稿のテキストが返されるはずです[7]。

このクレートとやり取りしている唯一の型はPost型であることに注意してください。この型は状態パターンを使用し、投稿がとりうるさまざまな状態(下書き、レビュー、または公開)を表す 3 つの状態オブジェクトの 1 つである値を保持します。状態の変更はPost型の内部で管理されます。状態の変更は、ライブラリのユーザーがPostインスタンスに対して呼び出すメソッドに応答して起こりますが、ユーザーは直接状態の変更を管理する必要はありません。また、ユーザーは状態に関して間違いを犯すこともありません。たとえば、レビューする前に投稿を公開するなどです。

投稿の定義と下書き状態での新しいインスタンスの作成

さて、ライブラリの実装を始めましょう!コンテンツを保持するパブリックなPost構造体が必要だとわかっているので、まずは構造体の定義と、Postのインスタンスを作成する関連するパブリックなnew関数から始めます。リスト 17-12 に示すようになります。また、Postのすべての状態オブジェクトが持たなければならない動作を定義するプライベートなStateトレイトも作成します。

そして、Postはプライベートなフィールドstateの中のOption<T>の中にBox<dyn State>のトレイトオブジェクトを保持して、状態オブジェクトを保持します。少し後でOption<T>が必要な理由がわかります。

ファイル名:src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
          1 state: Some(Box::new(Draft {})),
          2 content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

リスト 17-12: Post構造体と新しいPostインスタンスを作成するnew関数、Stateトレイト、およびDraft構造体の定義

Stateトレイトは、異なる投稿状態で共有される動作を定義します。状態オブジェクトはDraftPendingReview、およびPublishedで、すべてがStateトレイトを実装します。今のところ、トレイトにはメソッドはありません。投稿が始まる状態であるDraft状態だけを定義して始めます。

新しいPostを作成するとき、そのstateフィールドをSome値に設定します。このSome値はBoxを保持しています[1]。このBoxDraft構造体の新しいインスタンスを指しています。これにより、新しいPostインスタンスを作成するたびに、それが下書きとして始まることが保証されます。Poststateフィールドはプライベートなので、他の状態でPostを作成する方法はありません! Post::new関数では、contentフィールドを新しい空のStringに設定します[2]。

投稿コンテンツのテキストを保存する

リスト 17-11 では、add_textという名前のメソッドを呼び出して、そこに&strを渡すことができるようにしたいと思いました。その&strは、ブログ投稿のテキストコンテントとして追加されます。これをメソッドとして実装します。contentフィールドをpubとして公開するのではなく、後でcontentフィールドのデータがどのように読み取られるかを制御するメソッドを実装できるようにします。add_textメソッドはかなり単純なので、リスト 17-13 の実装をimpl Postブロックに追加しましょう。

ファイル名:src/lib.rs

impl Post {
    --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

リスト 17-13: 投稿のcontentにテキストを追加するadd_textメソッドの実装

add_textメソッドは、add_textを呼び出しているPostインスタンスを変更しているので、selfへの可変参照を取ります。その後、contentStringに対してpush_strを呼び出し、保存されたcontentに追加するためにtext引数を渡します。この動作は投稿がどの状態にあるかに依存しないので、状態パターンの一部ではありません。add_textメソッドはstateフィールドとまったくやり取りせず、サポートしたい動作の一部です。

下書き投稿のコンテンツが空であることを保証する

add_textを呼び出して投稿にいくつかのコンテンツを追加した後でも、投稿がまだ下書き状態であるため、contentメソッドが空の文字列スライスを返すようにしたいと思います。これはリスト 17-11 の[3]で示されています。今のところ、この要件を満たす最も簡単な方法でcontentメソッドを実装しましょう。常に空の文字列スライスを返すようにします。後で投稿の状態を変更して公開できるようにする機能を実装したら、これを変更します。これまでのところ、投稿は下書き状態のみになるため、投稿コンテントは常に空でなければなりません。リスト 17-14 にこのプレースホルダーの実装を示します。

ファイル名:src/lib.rs

impl Post {
    --snip--
    pub fn content(&self) -> &str {
        ""
    }
}

リスト 17-14: 常に空の文字列スライスを返すPostcontentメソッドのプレースホルダー実装を追加する

この追加されたcontentメソッドにより、リスト 17-11 の[3]までのすべてのコードが意図通りに動作します。

レビュー依頼により投稿の状態が変更される

次に、投稿のレビュー依頼機能を追加する必要があります。これにより、投稿の状態がDraftからPendingReviewに変更されるはずです。リスト 17-15 にこのコードを示します。

ファイル名:src/lib.rs

impl Post {
    --snip--
  1 pub fn request_review(&mut self) {
      2 if let Some(s) = self.state.take() {
          3 self.state = Some(s.request_review())
        }
    }
}

trait State {
  4 fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      5 Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      6 self
    }
}

リスト 17-15: PostStateトレイトにrequest_reviewメソッドを実装する

Postには、selfへの可変参照を取るrequest_reviewという名前のパブリックメソッドを与えます[1]。その後、Postの現在の状態に対して内部のrequest_reviewメソッドを呼び出します[3]。この 2 番目のrequest_reviewメソッドは、現在の状態を消費して新しい状態を返します。

request_reviewメソッドをStateトレイトに追加します[4]。このトレイトを実装するすべての型は、今後request_reviewメソッドを実装する必要があります。メソッドの最初のパラメータとしてself&self、または&mut selfではなく、self: Box<Self>があることに注意してください。この構文は、メソッドがBoxに保持された型で呼び出された場合にのみ有効であることを意味します。この構文はBox<Self>の所有権を取得し、古い状態を無効にします。そのため、Postの状態値を新しい状態に変換できます。

古い状態を消費するために、request_reviewメソッドは状態値の所有権を取得する必要があります。ここでPoststateフィールドにあるOptionが役に立ちます。takeメソッドを呼び出して、stateフィールドからSome値を取り出し、その代わりにNoneを残します。なぜなら、Rust は構造体に未初期化のフィールドを持たせないからです[2]。これにより、state値をPostから移動させて、借用する代わりに所有権を取得できます。その後、投稿のstate値をこの操作の結果に設定します。

state値の所有権を取得するために、self.state = self.state.request_review();のようなコードで直接設定する代わりに、一時的にstateNoneに設定する必要があります。これにより、Postが古いstate値を新しい状態に変換した後に使用できないようになります。

Draftrequest_reviewメソッドは、新しいPendingReview構造体の新しいボックス化されたインスタンスを返します[5]。これは、投稿がレビューを待っている状態を表します。PendingReview構造体もrequest_reviewメソッドを実装していますが、変換は行いません。むしろ、それ自体を返します[6]。なぜなら、PendingReview状態にある既存の投稿にレビュー依頼を行った場合、それはPendingReview状態のままになるからです。

今では、状態パターンの利点がわかり始めました。Postrequest_reviewメソッドは、そのstate値に関係なく同じです。各状態は独自のルールを担っています。

Postcontentメソッドはそのままにして、空の文字列スライスを返します。今では、PendingReview状態とDraft状態の両方でPostを持つことができますが、PendingReview状態でも同じ動作が望まれます。リスト 17-11 は今では[5]までの行まで正常に動作します!

content の動作を変更するための approve の追加

approveメソッドはrequest_reviewメソッドと似ています。それは、現在の状態が承認された場合にその状態が持つべき値にstateを設定します。リスト 17-16 に示すようになります。

ファイル名:src/lib.rs

impl Post {
    --snip--
    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    --snip--
    fn approve(self: Box<Self>) -> Box<dyn State> {
      1 self
    }
}

struct PendingReview {}

impl State for PendingReview {
    --snip--
    fn approve(self: Box<Self>) -> Box<dyn State> {
      2 Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

リスト 17-16: PostStateトレイトにapproveメソッドを実装する

approveメソッドをStateトレイトに追加し、Stateを実装する新しい構造体であるPublished状態を追加します。

PendingReviewrequest_reviewと同じように動作します。Draftapproveメソッドを呼び出した場合、approveselfを返すため、何の影響もありません[1]。PendingReviewapproveを呼び出した場合、Published構造体の新しいボックス化されたインスタンスを返します[2]。Published構造体はStateトレイトを実装しており、request_reviewメソッドとapproveメソッドの両方で、それ自体を返します。なぜなら、それらの場合には投稿がPublished状態のままになるからです。

次に、Postcontentメソッドを更新する必要があります。contentから返される値がPostの現在の状態に依存するようにしたいので、Postがそのstateに定義されたcontentメソッドに委譲するようにします。リスト 17-17 に示すようになります。

ファイル名:src/lib.rs

impl Post {
    --snip--
    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    --snip--
}

リスト 17-17: Postcontentメソッドを更新して、Statecontentメソッドに委譲する

これらのルールをすべてStateを実装する構造体の中に保持することが目的であるため、stateの値に対してcontentメソッドを呼び出し、投稿インスタンス(つまり、self)を引数として渡します。そして、state値のcontentメソッドを使用して返される値を返します。

Optionに対してas_refメソッドを呼び出します。なぜなら、Optionの中の値の参照が必要であり、値の所有権は必要ないからです。stateOption<Box<dyn State>>なので、as_refを呼び出すと、Option<&Box<dyn State>>が返されます。as_refを呼び出さなければ、エラーが発生します。なぜなら、関数パラメータの借用された&selfからstateを移動させることはできないからです。

その後、unwrapメソッドを呼び出します。これは、Postのメソッドが完了したときにstateが常にSome値を含むことを保証するため、決してパニックにならないことがわかっています。これは、コンパイラがそれを理解できなくても、None値が決してあり得ないことを知っている「コンパイラよりも多くの情報を持つケース」の 1 つです。

この時点で、&Box<dyn State>に対してcontentを呼び出すと、&Boxに対して deref 強制が効果します。そのため、contentメソッドは最終的にStateトレイトを実装する型に対して呼び出されます。つまり、contentStateトレイト定義に追加する必要があります。そして、どの状態にあるかに応じて返すコンテンツのロジックを置く場所になります。リスト 17-18 に示すようになります。

ファイル名:src/lib.rs

trait State {
    --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
      1 ""
    }
}

--snip--
struct Published {}

impl State for Published {
    --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
      2 &post.content
    }
}

リスト 17-18: Stateトレイトにcontentメソッドを追加する

contentメソッドのデフォルト実装を追加して、空の文字列スライスを返すようにします[1]。つまり、DraftPendingReview構造体にcontentを実装する必要はありません。Published構造体はcontentメソッドをオーバーライドして、post.contentの値を返します[2]。

第 10 章で説明したように、このメソッドには寿命注釈が必要です。postの参照を引数として取り、そのpostの一部の参照を返しているので、返される参照の寿命はpost引数の寿命に関連しています。

これで完了です。リスト 17-11 のすべてが正常に動作します!ブログ投稿のワークフローのルールを持つ状態パターンを実装しました。ルールに関連するロジックは状態オブジェクトの中にあり、Post全体に散らばっているわけではありません。

なぜ enum を使わないのか?

投稿の状態を変数として持つenumを使わなかった理由が疑問に思われるかもしれません。それは確かに解決策の 1 つです。試してみて、最終結果を比較して、どちらが好きかを判断してみてください! enumを使う欠点の 1 つは、enumの値をチェックするすべての場所にmatch式やそれに似たものが必要になることです。これは、このトレイトオブジェクトの解決策よりも繰り返しが多くなる可能性があります。

状態パターンのトレードオフ

Rust は、投稿が各状態で持つべきさまざまな種類の動作をカプセル化するために、オブジェクト指向の状態パターンを実装できることを示しました。Postのメソッドは、さまざまな動作について何も知りません。コードの組織化の仕方により、公開済みの投稿がどのように振る舞うかを知るには、たった 1 か所を見ればよいことになります。つまり、Published構造体におけるStateトレイトの実装です。

状態パターンを使用しない代替実装を作成する場合、代わりにPostのメソッドや、投稿の状態をチェックしてそこで動作を変更するmainコードの中でmatch式を使用するかもしれません。これは、投稿が公開状態にある場合のすべての影響を理解するために、いくつかの場所を見なければならないことを意味します!追加する状態が増えるほど、この問題はさらに深刻になります。それぞれのmatch式には別のアームが必要になるからです。

状態パターンを使用すると、PostのメソッドやPostを使用する場所ではmatch式が必要なくなります。新しい状態を追加する場合、新しい構造体を追加して、その 1 つの構造体にトレイトメソッドを実装するだけです。

状態パターンを使用した実装は、機能を追加するために拡張しやすいです。状態パターンを使用するコードの保守の簡単さを見るには、次のいくつかの提案を試してみてください。

  • rejectメソッドを追加して、投稿の状態をPendingReviewからDraftに戻します。
  • 状態をPublishedに変更する前に、approveを 2 回呼び出す必要があります。
  • 投稿がDraft状態の場合にのみ、ユーザーがテキストコンテントを追加できるようにします。ヒント:状態オブジェクトにコンテントに関する変更を担わせるが、Postを変更する責任は負わせないでください。

状態パターンの欠点の 1 つは、状態が状態間の遷移を実装しているため、一部の状態が互いに結合していることです。PendingReviewPublishedの間にScheduledなどの別の状態を追加する場合、PendingReviewのコードを変更して、代わりにScheduledに遷移する必要があります。新しい状態の追加に伴ってPendingReviewが変更されなければ、作業量は少なくなりますが、それは別のデザインパターンに切り替えることを意味します。

もう 1 つの欠点は、一部のロジックを重複させていることです。重複を排除するために、Stateトレイトのrequest_reviewapproveメソッドに対して、selfを返すデフォルト実装を試してみるかもしれません。しかし、これは機能しません。Stateをトレイトオブジェクトとして使用する場合、トレイトは具体的なselfがどのようなものかを正確に知らないため、返却型はコンパイル時にわかりません。

他の重複には、Postrequest_reviewapproveメソッドの似たような実装が含まれます。両方のメソッドは、Optionstateフィールドの値に対する同じメソッドの実装に委譲し、stateフィールドの新しい値を結果に設定します。Postにこのパターンに従う多くのメソッドがある場合、繰り返しを排除するためにマクロを定義することを検討するかもしれません(「マクロ」を参照)。

オブジェクト指向言語で定義されている通りに状態パターンを実装することで、Rust の強みを最大限に活用していません。blogクレートに対して行うことができるいくつかの変更を見てみましょう。それにより、無効な状態と遷移をコンパイル時エラーにすることができます。

型としての状態と動作のエンコード

状態パターンを再考して、異なる種類のトレードオフを得る方法を紹介します。外部コードが状態と遷移をまったく知らないように状態と遷移を完全にカプセル化する代わりに、状態を異なる型にエンコードします。その結果、Rust の型チェックシステムは、コンパイラエラーを発行することで、公開済みの投稿のみが許可される場所で下書き投稿を使用しようとする試みを防ぎます。

リスト 17-11 のmainの最初の部分を考えてみましょう。

ファイル名:src/main.rs

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());
}

まだ、Post::newを使用して下書き状態の新しい投稿を作成し、投稿のコンテントにテキストを追加する機能を有効にしています。しかし、下書き投稿に空の文字列を返すcontentメソッドを持つ代わりに、下書き投稿には全くcontentメソッドがないようにします。そうすることで、下書き投稿のコンテントを取得しようとすると、メソッドが存在しないことを示すコンパイラエラーが表示されます。その結果、本番環境で下書き投稿のコンテントを偶然に表示することは不可能になります。なぜなら、そのコードはコンパイルされないからです。リスト 17-19 にPost構造体とDraftPost構造体の定義、およびそれぞれのメソッドを示します。

ファイル名:src/lib.rs

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
  1 pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

  2 pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
  3 pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

リスト 17-19: contentメソッドを持つPostcontentメソッドを持たないDraftPost

Post構造体とDraftPost構造体の両方には、ブログ投稿のテキストを格納するプライベートなcontentフィールドがあります。構造体にはもはやstateフィールドがなくなりました。なぜなら、状態のエンコードを構造体の型に移しているからです。Post構造体は公開済みの投稿を表し、contentメソッドがあり、それがcontentを返します[2]。

Post::new関数はまだありますが、Postのインスタンスを返す代わりに、DraftPostのインスタンスを返します[1]。contentはプライベートであり、Postを返す関数がないため、現在はPostのインスタンスを作成することはできません。

DraftPost構造体にはadd_textメソッドがあるため、以前と同じようにcontentにテキストを追加できます[3]。ただし、DraftPostにはcontentメソッドが定義されていないことに注意してください!これで、プログラムはすべての投稿が下書き投稿から始まり、下書き投稿のコンテントが表示できないことを保証します。これらの制約を回避しようとするすべての試みは、コンパイラエラーを引き起こします。

異なる型への変換としての遷移の実装

では、公開済みの投稿をどのようにして得るのでしょうか?下書き投稿は、公開される前にレビューと承認を受ける必要があるというルールを強制したいと思います。レビュー待ち状態の投稿もまだコンテントを表示してはいけません。これらの制約を実装するには、もう 1 つの構造体であるPendingReviewPostを追加し、DraftPostrequest_reviewメソッドを定義してPendingReviewPostを返し、PendingReviewPostapproveメソッドを定義してPostを返します。リスト 17-20 に示すようになります。

ファイル名:src/lib.rs

impl DraftPost {
    --snip--
    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}

リスト 17-20: DraftPostrequest_reviewを呼び出すことで作成されるPendingReviewPostと、PendingReviewPostを公開済みのPostに変換するapproveメソッド

request_reviewメソッドとapproveメソッドはselfの所有権を取得します。そのため、DraftPostPendingReviewPostのインスタンスを消費し、それぞれPendingReviewPostと公開済みのPostに変換します。このようにすることで、request_reviewを呼び出した後に、余分なDraftPostインスタンスが残らなくなります。PendingReviewPost構造体にはcontentメソッドが定義されておらず、DraftPostと同様に、そのコンテントを読もうとするとコンパイラエラーが発生します。定義されたcontentメソッドを持つ公開済みのPostインスタンスを取得する唯一の方法は、PendingReviewPostapproveメソッドを呼び出すことであり、PendingReviewPostを取得する唯一の方法は、DraftPostrequest_reviewメソッドを呼び出すことです。これで、ブログ投稿のワークフローを型システムにエンコードしました。

しかし、mainにもいくつかの小さな変更を加える必要があります。request_reviewメソッドとapproveメソッドは新しいインスタンスを返すため、呼び出された構造体を変更するわけではありません。そのため、返されたインスタンスを保存するために、より多くのlet post =のシャドーイング代入を追加する必要があります。また、下書きとレビュー待ちの投稿のコンテントが空文字列であるというアサーションも必要ありません。必要ないのです。それらの状態の投稿のコンテントを使用しようとするコードは、もはやコンパイルできなくなっています。mainの更新されたコードをリスト 17-21 に示します。

ファイル名:src/main.rs

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

リスト 17-21: ブログ投稿のワークフローの新しい実装を使用するためのmainの変更

mainに再割り当てするために必要な変更は、この実装がもはや完全にオブジェクト指向の状態パターンに従っていないことを意味します。状態間の変換はもはやPostの実装内に完全にカプセル化されていません。ただし、型システムとコンパイル時に行われる型チェックのおかげで、現在は無効な状態が不可能になっています!これにより、未公開の投稿のコンテントを表示するなどの特定のバグが本番環境に到達する前に見つかることが保証されます。

リスト 17-21 の後のblogクレートに対して、このセクションの最初で提案されたタスクを試してみて、このバージョンのコードの設計についてどう思うかを見てみてください。この設計では、一部のタスクは既に完了している場合があります。

Rust はオブジェクト指向のデザインパターンを実装できることがわかりましたが、Rust には状態を型システムにエンコードするなど、他のパターンもあります。これらのパターンには異なるトレードオフがあります。オブジェクト指向のパターンに非常に慣れているかもしれませんが、Rust の機能を生かして問題を再考することで、コンパイル時にいくつかのバグを防ぐなどの利点が得られます。所有権のような特定の機能のために、オブジェクト指向言語にはないため、オブジェクト指向のパターンが常に Rust における最善の解決策であるとは限りません。

まとめ

おめでとうございます!あなたはオブジェクト指向のデザインパターンを実装する実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行うことができます。