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

言語がオブジェクト指向と考えられるのになければならない機能について、プログラミングコミュニティ内での総意はありません。 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_variables)]
#fn main() {
pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}
#}

リスト17-1: 整数のリストとコレクションの要素の平均を管理するAveragedCollection構造体

構造体は、他のコードが使用できるようにpubで印づけされていますが、構造体のフィールドは非公開のままです。 値が追加されたりリストから削除される度に、平均も更新されることを保証したいので、今回の場合重要です。 addremoveaverageメソッドを構造体に実装することでこれをします。リスト17-2のようにですね:

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#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;
    }
}
#}

リスト17-2: AveragedCollectionaddremoveaverage公開メソッドの実装

addremoveaverageの公開メソッドがAveragedCollectionのインスタンスを変更する唯一の方法になります。 要素がaddメソッドを使用してlistに追加されたり、removeメソッドを使用して削除されたりすると、 各メソッドの実装がaverageフィールドの更新を扱う非公開のupdate_averageメソッドも呼び出します。

listaverageフィールドを非公開のままにしているので、外部コードが要素をlistフィールドに直接追加したり削除したりする方法はありません; そうでなければ、averageフィールドは、listが変更された時に同期されなくなる可能性があります。 averageメソッドはaverageフィールドの値を返し、外部コードにaverageを読ませるものの、 変更は許可しません。

構造体AveragedCollectionの実装詳細をカプセル化したので、データ構造などの側面を将来容易に変更することができます。 例を挙げれば、listフィールドにVec<i32>ではなくHashSet<i32>を使うこともできます。 addremoveaverage公開メソッドのシグニチャが同じである限り、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において、トレイトオブジェクトがどう多相性を可能にするかを見ましょう。