オブジェクト指向言語の特徴
言語がオブジェクト指向と考えられるのになければならない機能について、プログラミングコミュニティ内での総意はありません。 RustはOOPを含めた多くのプログラミングパラダイムに影響を受けています; 例えば、 第13章で関数型プログラミングに由来する機能を探究しました。議論はあるかもしれませんが、 OOP言語は特定の一般的な特徴を共有しています。具体的には、オブジェクトやカプセル化、 継承などです。それらの個々の特徴が意味するものとRustがサポートしているかを見ましょう。
オブジェクトは、データと振る舞いを含む
エーリヒ・ガンマ(Enoch Gamma)、リチャード・ヘルム(Richard Helm)、ラルフ・ジョンソン(Ralph Johnson)、
ジョン・ブリシディース(John Vlissides)(アディソン・ワズリー・プロ)により、
1994年に書かれたデザインパターン: 再利用可能なオブジェクト指向ソフトウェアの要素という本は、
俗に4人のギャングの本(訳注
: the Gang of Four book; GoFとよく略される)と呼ばれ、オブジェクト指向デザインパターンのカタログです。
そこでは、OOPは以下のように定義されています:
オブジェクト指向プログラムは、オブジェクトで構成される。オブジェクトは、 データとそのデータを処理するプロシージャを梱包している。このプロシージャは、 典型的にメソッドまたはオペレーションと呼ばれる。
この定義を使用すれば、Rustはオブジェクト指向です: 構造体とenumにはデータがありますし、
impl
ブロックが構造体とenumにメソッドを提供します。メソッドのある構造体とenumは、
オブジェクトとは呼ばれないものの、GoFのオブジェクト定義によると、同じ機能を提供します。
カプセル化は、実装詳細を隠蔽する
OOPとよく紐づけられる別の側面は、カプセル化の思想です。これは、オブジェクトの実装詳細は、 そのオブジェクトを使用するコードにはアクセスできないことを意味します。故に、 オブジェクトと相互作用する唯一の手段は、その公開APIを通してです; オブジェクトを使用するコードは、 オブジェクトの内部に到達して、データや振る舞いを直接変更できるべきではありません。 このために、プログラマはオブジェクトの内部をオブジェクトを使用するコードを変更する必要なく、 変更しリファクタリングできます。
カプセル化を制御する方法は、第7章で議論しました: pub
キーワードを使用して、
自分のコードのどのモジュールや型、関数、メソッドを公開するか決められ、
既定ではそれ以外のものは全て非公開になります。例えば、
i32
値のベクタを含むフィールドのあるAveragedCollection
という構造体を定義できます。
この構造体はさらに、ベクタの値の平均を含むフィールドを持てます。つまり、平均は誰かが必要とする度に、
オンデマンドで計算する必要はないということです。言い換えれば、AveragedCollection
は、
計算した平均をキャッシュしてくれるわけです。リスト17-1には、AveragedCollection
構造体の定義があります:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct AveragedCollection { list: Vec<i32>, average: f64, } }
構造体は、他のコードが使用できるようにpub
で印づけされていますが、構造体のフィールドは非公開のままです。
値が追加されたりリストから削除される度に、平均も更新されることを保証したいので、今回の場合重要です。
add
やremove
、average
メソッドを構造体に実装することでこれをします。リスト17-2のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct AveragedCollection { list: Vec<i32>, average: f64, } impl AveragedCollection { pub fn add(&mut self, value: i32) { self.list.push(value); self.update_average(); } pub fn remove(&mut self) -> Option<i32> { let result = self.list.pop(); match result { Some(value) => { self.update_average(); Some(value) }, None => None, } } pub fn average(&self) -> f64 { self.average } fn update_average(&mut self) { let total: i32 = self.list.iter().sum(); self.average = total as f64 / self.list.len() as f64; } } }
add
、remove
、average
の公開メソッドがAveragedCollection
のインスタンスを変更する唯一の方法になります。
要素がadd
メソッドを使用してlist
に追加されたり、remove
メソッドを使用して削除されたりすると、
各メソッドの実装がaverage
フィールドの更新を扱う非公開のupdate_average
メソッドも呼び出します。
list
とaverage
フィールドを非公開のままにしているので、外部コードが要素をlist
フィールドに直接追加したり削除したりする方法はありません;
そうでなければ、average
フィールドは、list
が変更された時に同期されなくなる可能性があります。
average
メソッドはaverage
フィールドの値を返し、外部コードにaverage
を読ませるものの、
変更は許可しません。
構造体AveragedCollection
の実装詳細をカプセル化したので、データ構造などの側面を将来容易に変更することができます。
例を挙げれば、list
フィールドにVec<i32>
ではなくHashSet<i32>
を使うこともできます。
add
、remove
、average
といった公開メソッドのシグニチャが同じである限り、AveragedCollection
を使用するコードは変更する必要がないでしょう。
代わりにlist
を公開にしたら、必ずしもこうはならないでしょう: HashSet<i32>
とVec<i32>
は、
要素の追加と削除に異なるメソッドを持っているので、外部コードが直接list
を変更しているなら、
外部コードも変更しなければならない可能性が高いでしょう。
カプセル化が、言語がオブジェクト指向と考えられるのに必要な側面ならば、Rustはその条件を満たしています。
コードの異なる部分でpub
を使用するかしないかという選択肢のおかげで、実装詳細をカプセル化することが可能になります。
型システム、およびコード共有としての継承
継承は、それによってオブジェクトが他のオブジェクトの定義から受け継ぐことができる機構であり、 それ故に、再定義する必要なく、親オブジェクトのデータと振る舞いを得ます。
言語がオブジェクト指向言語であるために継承がなければならないのならば、Rustは違います。 親構造体のフィールドとメソッドの実装を受け継ぐ構造体を定義する方法はありません。しかしながら、 継承がプログラミング道具箱にあることに慣れていれば、そもそも継承に手を伸ばす理由によって、 Rustで他の解決策を使用することができます。
継承を選択する理由は主に2つあります。1つ目は、コードの再利用です: ある型に特定の振る舞いを実装し、
継承により、その実装を他の型にも再利用できるわけです。デフォルトのトレイトメソッド実装を代わりに使用して、
Rustコードを共有でき、これは、リスト10-14でSummary
トレイトにsummarize
メソッドのデフォルト実装を追加した時に見かけました。
Summary
トレイトを実装する型は全て、追加のコードなくsummarize
メソッドが使用できます。
これは、親クラスにメソッドの実装があり、継承した子クラスにもそのメソッドの実装があることと似ています。
また、Summary
トレイトを実装する時に、summarize
メソッドのデフォルト実装を上書きすることもでき、
これは、親クラスから継承したメソッドの実装を子クラスが上書きすることに似ています。
継承を使用するもう1つの理由は、型システムに関連しています: 親の型と同じ箇所で子供の型を使用できるようにです。 これは、多相性(polymorphism)とも呼ばれ、複数のオブジェクトが特定の特徴を共有しているなら、 実行時にお互いに代用できることを意味します。
多相性
多くの人にとって、多相性は、継承の同義語です。ですが、実際には複数の型のデータを取り扱えるコードを指すより一般的な概念です。 継承について言えば、それらの型は一般的にはサブクラスです。
Rustは代わりにジェネリクスを使用して様々な可能性のある型を抽象化し、トレイト境界を使用してそれらの型が提供するものに制約を課します。 これは時に、パラメータ境界多相性(bounded parametric polymorphism)と呼ばれます。
継承は、近年、多くのプログラミング言語において、プログラムの設計解決策としては軽んじられています。 というのも、しばしば必要以上にコードを共有してしまう危険性があるからです。サブクラスは、 必ずしも親クラスの特徴を全て共有するべきではないのに、継承ではそうなってしまうのです。 これにより、プログラムの設計の柔軟性を失わせることもあります。また、道理に合わなかったり、メソッドがサブクラスには適用されないために、 エラーを発生させるようなサブクラスのメソッドの呼び出しを引き起こす可能性が出てくるのです。 さらに、サブクラスに1つのクラスからだけ継承させる言語もあり、さらにプログラムの設計の柔軟性が制限されます。
これらの理由により、継承ではなくトレイトオブジェクトを使用してRustは異なるアプローチを取っています。 Rustにおいて、トレイトオブジェクトがどう多相性を可能にするかを見ましょう。