changed 5 years ago
Linked with GitHub

Rust勉強会 第18回

17章

2020/12/4 岡本拓海


本日のメニュー

  1. オブジェクト指向言語の特徴
  2. トレイトオブジェクトで異なる型の値を許容する
  3. オブジェクト指向デザインパターンを実装する

プレゼン25分くらい+議論5分位で18:30前後終了を目指します。


オブジェクト指向言語の特徴

オブジェクト指向プログラムとは?

GoFの定義によると

オブジェクト指向プログラムは、オブジェクトで構成される。オブジェクトは、 データとそのデータを処理するプロシージャを梱包している。このプロシージャは、 典型的にメソッドまたはオペレーションと呼ばれる。


Rustはオブジェクト指向?

  • データ
    • 構造体
    • enum
  • データを処理するプロージャ
    • implブロックで実装されるメソッド

Rustはオブジェクト指向言語!


継承が選択される理由

  • コードを再利用したい
  • 親の型と同じ個所で子供の型を使用したい

継承の悪いところ

  • コードを再利用したい
    • 設計によっては親や子供のコードが肥大化しがち
    • 継承の制限によってより設計の柔軟性が下がる(Javaとか)
  • 親の型と同じ個所で子供の型を使用したい

Rustのポリモーフィズム

ポリモーフィズム(polymorphism):多相性 多態性

Rustはサブクラスの代わりにジェネリクスを使用して様々な可能性のある型を抽象化し、トレイト境界を使用してそれらの型が提供するものに制約を課します。 これは時に、パラメータ境界多相性(bounded parametric polymorphism)と呼ばれます。


トレイトオブジェクトで異なる型の値を許容する

ライブラリの使用者が特定の場面で合法になる型のセットを拡張できるようにしたくなることがあります。

GUIライブラリの実装例を見ていきましょう


一般的なふるまいをトレイトに定義する

Drawトレイト

pub trait Draw { fn draw(&self); }

Drawトレイト実装するトレイトオブジェクト(Box<Draw>)

pub struct Screen { pub components: Vec<Box<Draw>>, }

Screenの実装を考えてみる

pub trait Draw { fn draw(&self); } pub struct Screen { pub components: Vec<Box<Draw>>, } impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } }

↓この方法はあまりよくない。Tの型が1つに制限されてしまう。。。

pub struct Screen<T: Draw> { pub components: Vec<T>, } impl<T> Screen<T> where T: Draw { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } }

トレイトを実装する

Button型の実装を考える

pub struct Button { pub width: u32, pub height: u32, pub label: String, } impl Draw for Button { fn draw(&self) { // code to actually draw a button // 実際にボタンを描画するコード } }

extern crate gui; use gui::Draw; struct SelectBox { width: u32, height: u32, options: Vec<String>, } impl Draw for SelectBox { fn draw(&self) { // code to actually draw a select box //セレクトボックスを実際に描画するコード } }

use gui::{Screen, Button}; fn main() { let screen = Screen { components: vec![ Box::new(SelectBox { width: 75, height: 10, options: vec![ // はい String::from("Yes"), // 多分 String::from("Maybe"), // いいえ String::from("No") ], }), Box::new(Button { width: 50, height: 10, // 了解 label: String::from("OK"), }), ], }; screen.run(); }

トレイトオブジェクトは、ダイナミックディスパッチを行う

単相化の結果吐かれるコードは、 スタティックディスパッチを行い、これは、コンパイル時にコンパイラがどのメソッドを呼び出しているかわかる時のことです。 これは、ダイナミックディスパッチとは対照的で、この時、コンパイラは、コンパイル時にどのメソッドを呼び出しているのかわかりません。 ダイナミックディスパッチの場合、コンパイラは、どのメソッドを呼び出すか実行時に弾き出すコードを生成します

― コンパイラがコードのインライン化するのを実効速度の向上がしにくくなる。

コードの柔軟性とのトレードオフなので、使用時は要検討


トレイトオブジェクトには、オブジェクト安全性が必要

pub trait Clone { fn clone(&self) -> Self; }
pub struct Screen { pub components: Vec<Box<Clone>>, }

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

以下の内容を実装してみましょう。(それっぽいかはさておき。。。)

  1. ブログ記事は、空の草稿から始まる。
  2. 草稿ができたら、査読が要求される。
  3. 記事が承認されたら、公開される。
  4. 公開されたブログ記事だけが表示する内容を返すので、未承認の記事は、誤って公開されない。

デモコード

extern crate blog; use blog::Post; fn main() { let mut post = Post::new(); // 今日はお昼にサラダを食べた post.add_text("I ate a salad for lunch today"); assert_eq!("", post.content()); post.request_review(); assert_eq!("", post.content()); post.approve(); assert_eq!("I ate a salad for lunch today", post.content()); }

Postを定義し、草稿状態で新しいインスタンスを生成する

pub struct Post { state: Option<Box<State>>, content: String, } impl Post { pub fn new() -> Post { Post { state: Some(Box::new(Draft {})), content: String::new(), } } } trait State {} struct Draft {} impl State for Draft {}

記事の内容のテキストを格納する

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

草稿の記事の内容は空であることを保証する

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

記事の査読を要求すると、状態が変化する

impl Post { // --snip-- pub fn request_review(&mut self) { if let Some(s) = self.state.take() { self.state = Some(s.request_review()) } } } trait State { fn request_review(self: Box<Self>) -> Box<State>; } struct Draft {} impl State for Draft { fn request_review(self: Box<Self>) -> Box<State> { Box::new(PendingReview {}) } } struct PendingReview {} impl State for PendingReview { fn request_review(self: Box<Self>) -> Box<State> { self }

contentの振る舞いを変化させるapproveメソッドを追加する

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<State>; fn approve(self: Box<Self>) -> Box<State>; } struct Draft {} impl State for Draft { // --snip-- fn approve(self: Box<Self>) -> Box<State> { self } } struct PendingReview {} impl State for PendingReview { // --snip-- fn approve(self: Box<Self>) -> Box<State> { Box::new(Published {}) } } struct Published {} impl State for Published { fn request_review(self: Box<Self>) -> Box<State> { self } fn approve(self: Box<Self>) -> Box<State> { self } }

contentの振る舞いを変化させるapproveメソッドを追加する

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

h2

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

ステートパターンの代償

  • 状態が状態間の遷移を実装しているので、状態の一部が密に結合した状態になってしまう
    • PendingReviewとPublishedの間に、Scheduledのような別の状態を追加したら、 代わりにPendingReviewのコードをScheduledに遷移するように変更しなければならない
  • ロジックの一部を重複させてしまう
    • 重複を除くためには、 Stateトレイトのrequest_reviewとapproveメソッドにselfを返すデフォルト実装する?
    • これ↑はオブジェクト安全性を侵害する

状態と振る舞いを型としてコード化する

fn main() { let mut post = Post::new(); post.add_text("I ate a salad for lunch today"); assert_eq!("", post.content()); }

状態と振る舞いを型としてコード化する

pub struct Post { content: String, } pub struct DraftPost { content: String, } impl Post { pub fn new() -> DraftPost { DraftPost { content: String::new(), } } pub fn content(&self) -> &str { &self.content } } impl DraftPost { pub fn add_text(&mut self, text: &str) { self.content.push_str(text); } }

遷移を異なる型への変形として実装する

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, } } }

ブログ記事ワークフローの新しい実装を使うmainの変更

extern crate blog; 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()); }

参考資料

https://doc.rust-jp.rs/book/second-edition/ch17-00-oop.html


ご清聴ありがとうございました

Select a repo