オブジェクト指向 ― 出自の違う話を分けて読む
「オブジェクト指向」は、現実世界をプログラムに写す設計法として説明されることもあれば、部品を再利用する技術や、継承・カプセル化・ポリモーフィズムという言語機能の組み合わせとして説明されることもあります。出自の違う話が一つの言葉にまとめられているため、全体がつかみにくくなっています。この記事では、1960年代のSimulaから現在のSwift・Kotlinまでを歴史の順にたどり、それぞれの言語や環境が何を解決しようとしたのかを確かめます。付録には、MacAppとTCLの命名規則をまとめました。
目次
- はじめに
- 1. 前提 ― 構造化プログラミングとモジュールの先に
- 2. シミュレーションから生まれたクラス ― Simula 67
- 3. メッセージとしてのオブジェクト ― Smalltalk
- 4. ソフトウェア部品の構想 ― McIlroyとCoxのSoftware-IC
- 5. アプリケーションフレームワーク ― Lisa ToolkitとClascal、MacApp
- 6. TCL ― コマンドの連鎖
- 7. NeXTSTEP・OpenStep・Cocoa ― 継承より委譲
- 8. Objective-CとC++ ― 遅延束縛と、コストのかからない抽象化
- 9. プロトコルと分散オブジェクト ― NeXTSTEPとMach
- 10. バイナリの部品 ― COM・SOM・OpenDoc・JavaBeans
- 11. Eiffelが考える世界 ― 契約による設計
- 12. 継承の見直し ― 合成とKotlin
- 13. Swiftのプロトコル指向
- 14. 混ざっている話を分ける
- まとめ
- 付録 MacAppとTCLの命名規則
- 注釈
- 参考資料
はじめに
オブジェクト指向の入門書の多くは、「犬クラスと猫クラスが動物クラスを継承する」といった例から始まります。ところが実際の開発で出会うオブジェクト指向は、フレームワークが用意したクラスを継承してメソッドを上書きすることだったり、デリゲートを設定することだったり、プロトコルに準拠させることだったりします。
この記事では、オブジェクト指向として語られる話を、次の5つの問いに分けて考えます。
- 対象をどうモデル化するか(シミュレーション、分析・設計)
- 作ったものをどう再利用するか(部品、クラスライブラリ)
- アプリケーションの骨格を誰が持つか(フレームワーク)
- プロセスやバイナリの境界を越えてどう組み合わせるか(分散オブジェクト、コンポーネント)
- 型と振る舞いの関係をどう保証するか(抽象データ型、契約、プロトコル)
言語や環境はそれぞれ、このうちのどれかに重点を置いて設計されています。以下では歴史の順にたどり、14節でこの5つの問いに沿って整理し直します。
1. 前提 ― 構造化プログラミングとモジュールの先に
構造化プログラミングの記事では、手続きの内部の制御構造を整える議論から、Parnasの情報隠蔽とModula-2のモジュールに至る流れを見ました。1970年代には、もう一つの流れとして抽象データ型(abstract data type)の研究が進みます。Barbara Liskov らが1974年に発表した言語CLUは、データの内部表現を隠し、操作だけを公開する「クラスタ」を言語の単位にしました(注1)。
抽象データ型の考え方は、型を「値の集合と、その値に対して許される操作の組」として定めるものです。スタックであれば、push・pop・isEmpty という操作とその性質がスタックの定義であり、配列で実装するか連結リストで実装するかは外から見えません。
オブジェクト指向は、この流れと重なりながら、別の場所から始まっています。出発点はプログラムの正しさや変更のしやすさではなく、シミュレーションでした。
2. シミュレーションから生まれたクラス ― Simula 67
ノルウェー計算センターの Ole-Johan Dahl と Kristen Nygaard は、1960年代前半に離散事象シミュレーションのための言語Simula Iを作りました。銀行の窓口や港の荷役のように、多数の主体がそれぞれの手順で動き、待ち行列で相互に影響し合う系を記述するための言語です。1967年には、これを汎用の言語に改めたSimula 67を発表します(注2)。
Simula 67で導入されたのが、クラス、オブジェクト、サブクラス(Simulaでは接頭辞クラスと呼ぶ)、仮想手続きです。今日のオブジェクト指向言語の基本的な仕組みは、ほぼこの時点で出そろっています。
Simulation Begin
Process Class Customer;
Begin
Hold(5.0); ! 窓口で処理を受ける時間だけ進める;
Activate New Customer Delay 3.0;
End;
Activate New Customer;
Hold(100.0);
End;
Customer は標準のクラス Process を接頭辞に持つクラスで、オブジェクトごとに自分の手順を持ち、Hold で模擬時間を進めながら動きます。客の一人ひとりが、データと手順を持った独立した存在としてプログラムの中に現れます。Simulaのオブジェクトはコルーチンとして実装されていて、実行を中断し、後で再開できました。
シミュレーションでは、プログラムの構造を現実の系の構造に合わせることがそのまま目的になります。窓口があれば窓口のクラスを、客がいれば客のクラスを作ります。「現実世界のものをオブジェクトとしてモデル化する」という説明は、この出自を反映したものです。
一方で、Simulaのクラスは、Hoareが1965年に提案したレコードクラス(record class)の考えも取り込んでいます(注3)。データの型を定義し、その型に属する手続きをまとめる、という見方です。モデル化の道具としてのクラスと、型を定義する道具としてのクラスという2つの顔は、最初の言語の時点で同居していました。
3. メッセージとしてのオブジェクト ― Smalltalk
「オブジェクト指向(object-oriented)」という言葉を使い始めたのは Alan Kay だとされています。Kayは1970年代にゼロックスのパロアルト研究所で、Smalltalkの開発を率いました(注4)。
KayはSimulaから強い影響を受けつつ、オブジェクトを、互いにメッセージを送り合う小さな計算機のようなものと考えました。オブジェクトの内部状態は外から直接触れず、メッセージを受け取ったオブジェクトが、それにどう応じるかを自分で決めます。
3 + 4. "数 3 に、引数 4 を付けてメッセージ + を送る"
#(3 1 2) asSortedCollection. "配列にメッセージ asSortedCollection を送る"
x > 0
ifTrue: ['positive']
ifFalse: ['not positive']. "真偽値オブジェクトにメッセージを送って分岐する"
Smalltalkでは、算術演算も条件分岐もメッセージ送信として書きます。ifTrue:ifFalse: は構文ではなく、真偽値のオブジェクトが受け取るメッセージです。true のオブジェクトは1つ目のブロックを、false のオブジェクトは2つ目のブロックを実行します。
どのメソッドが実行されるかは、メッセージを受け取ったオブジェクトが実行時に決めます。Kayは後年、自分にとってのオブジェクト指向とは、メッセージのやり取り、状態の局所化と保護・隠蔽、そしてあらゆるものの極端な遅延束縛だけを意味する、と書いています(注5)。継承は、この定義に含まれていません。
Smalltalkは言語であると同時に、ウインドウ・マウス・ブラウザを備えた開発環境でもあり、GUIを持つアプリケーションの作り方と一緒に示されました。
4. ソフトウェア部品の構想 ― McIlroyとCoxのSoftware-IC
構造化プログラミングの記事で触れた1968年のNATOソフトウェア工学会議で、Douglas McIlroy は "Mass Produced Software Components" という発表をしています(注6)。ハードウェアの部品のように、ソフトウェアも規格化された部品をカタログから選んで組み立てられるようにすべきだ、という提案です。ソフトウェア危機への処方として、構造化プログラミングとは別の方向から出てきた考え方でした。
この構想をオブジェクト指向と結び付けたのが Brad Cox です。Coxは1980年代前半にCにSmalltalk風のメッセージ送信を加えた言語Objective-Cを作り、1986年の著書でソフトウェアIC(Software-IC)という考えを示しました(注7)。集積回路が内部の回路を隠し、ピンの仕様だけで組み合わせられるように、オブジェクトもメッセージのインタフェースだけで組み合わせられる部品として流通させよう、というものです。
Coxが重視したのは、部品を作る人と使う人が別であることでした。使う側は部品のソースコードを持っていなくても、メッセージを送るだけで部品を使えなければなりません。この要求は、3節の遅延束縛と相性がよく、Objective-Cの設計にもそのまま表れています(8節)。
部品化の話は、「オブジェクト指向で再利用性が上がる」という説明の出どころです。ただし、どの粒度の部品がどう流通すれば再利用が成り立つのかについては、この後の歴史の中で何度も考え直されることになります(9節・10節)。
5. アプリケーションフレームワーク ― Lisa ToolkitとClascal、MacApp
1980年代前半、GUIを持つパーソナルコンピュータが登場すると、オブジェクト指向はアプリケーションの作り方の問題として持ち込まれます。ウインドウを開き、メニューを出し、マウスの操作に応じ、書類を保存して印刷する。どのアプリケーションにも共通するこうした処理を、アプリケーションごとに書き直すのは大きな負担でした。
Apple Lisaの開発では、Larry Tesler らが1983年に、Lisa Pascalにオブジェクト指向の拡張を加えた言語Clascalを作りました(注8)。Clascalで書かれたLisa Toolkitは、Lisaのアプリケーションに共通する骨格をクラスとして提供するものでした。ClascalはNiklaus Wirthの協力を得てObject Pascalとして整理され、Lisa ToolkitはMacintosh向けのMacAppに引き継がれます。MacAppの最初の版は1985年に出ています(注9)。
MacAppのようなアプリケーションフレームワークでは、プログラムの主導権が逆になります。イベントループを回し、どのオブジェクトのどのメソッドを呼ぶかを決めるのはフレームワークの側で、アプリケーションを書く人は、フレームワークが用意したクラスを継承し、必要なメソッドだけを上書きします。Ralph Johnson と Brian Foote は1988年の論文で、この関係を制御の反転(inversion of control)と呼び、クラスライブラリとフレームワークを区別しました(注10)。
MacAppは、アプリケーション・書類・ビュー・コマンドといった役割をクラスとして用意していました。取り消し(undo)を実現するために、ユーザーの操作をコマンドオブジェクトとして表す方式もその一つです。MacAppの流儀に倣って、Object Pascalで書くと次のようになります。
type
TDeleteCommand = object(TCommand)
fDocument: TTextDocument;
fStart, fLength: integer;
fSaved: Handle; { 削除した文字列を取り消しのために保存 }
procedure DoIt; override;
procedure UndoIt; override;
procedure RedoIt; override;
end;
procedure TDeleteCommand.DoIt;
begin
fSaved := fDocument.CopyText(fStart, fLength);
fDocument.DeleteText(fStart, fLength)
end;
procedure TDeleteCommand.UndoIt;
begin
fDocument.InsertText(fStart, fSaved)
end;
procedure TDeleteCommand.RedoIt;
begin
DoIt
end;
アプリケーションを書く人は、削除という操作の実行・取り消し・やり直しの3つを書くだけです。どのタイミングで UndoIt が呼ばれるか、取り消しメニューの表示をどう切り替えるかは、フレームワークが受け持ちます。
一方で、MacAppはアプリケーションのすべてをオブジェクトとして書くことを求めてはいませんでした。1985年2月付けのMacAppの最初の解説資料は、アプリケーションに現れるオブジェクトを2種類に分けています(注11)。ウインドウや書類のようにほとんどのアプリケーションに現れる「標準オブジェクト」と、表計算の表やグラフのように一部のアプリケーションにだけ現れる「個別のオブジェクト」です。標準オブジェクトはMacAppが定義した型(TApplication・TDocument・TView・TWindow・TCommand など)を継承して書く必要がありますが、個別のオブジェクトはその形で実装しなくてよい、と書かれています。
つまり、フレームワークとの接点になる部分だけをObject Pascalのオブジェクトとして書き、表計算の計算処理のようなアプリケーション固有の中身は、従来のPascalの手続きとレコードで書いてもかまいません。オブジェクト指向で作られたクラスライブラリを使うことと、自分のコードをオブジェクト指向で設計することは、別の判断として扱われていました。
ここでの継承は、フレームワークの決めた枠にアプリケーション固有の差分を差し込むための仕組みで、2節のシミュレーションで現実の系を写すために使う継承とは役割が違います。
6. TCL ― コマンドの連鎖
MacAppは強力でしたが、規模が大きく、学ぶのに時間のかかるフレームワークでした。より手軽な選択肢として広く使われたのが、THINK Cに付属したTHINK Class Library(TCL)です。1989年のTHINK C 4.0で、単一継承などのオブジェクト指向の拡張とともに加わり、後にC++による実装に移行しました(注12)。
TCLの解説で最初に説明されるのが、コマンドの連鎖(chain of command)という考え方です。マウスのクリックはウインドウの中の位置で行き先が決まるので、ウインドウからペイン(画面の区画)へと見た目の入れ子に沿って配られます。一方、メニューの選択やキー入力には位置がありません。TCLでは、こうしたコマンドを、グローバル変数 gGopher が指すオブジェクトにまず送ります。gopherは通常、作業中のウインドウでユーザーが最後にクリックしたペインです。
CApp(アプリケーション) ← 誰も処理しなかったコマンドが最後に届く
↑
CDoc(書類・ウインドウの管理者)
↑
CPane(ユーザーが選択中のペイン) ← gGopher が指す。コマンドはここから始まる
gopherがそのコマンドを処理できなければ、コマンドは上位の監督者(supervisor)に渡され、最後はアプリケーションのオブジェクトに届きます。それでも処理されなければ捨てられます(注13)。テキストのペインが選択されていれば「コピー」はそのペインが処理し、何も選択されていなければ書類やアプリケーションが処理する、という振る舞いを、条件分岐を書かずに実現できます。
同じ仕組みはMacAppにもありました。5節で触れた1985年の解説資料には、gTarget というグローバル変数が選択中のビューを指し、メニューのコマンドやキー入力はまずそのオブジェクトの DoMenuCommand・DoKeyCommand に渡される、と書かれています。処理できなければ、ビューからフレーム、ウインドウ、書類、アプリケーションへと所有の階層をさかのぼって渡されます。TCLのgopherは、MacAppのターゲットにあたります。
ここで継承と連鎖は別の軸になっています。CPane や CDocument は共通の祖先クラス CBureaucrat を継承することで、コマンドを受け取って上に回すという振る舞いを得ます。一方、どのオブジェクトがどのオブジェクトにコマンドを回すかは、実行時のオブジェクト同士の参照(supervisor)で決まります。
7. NeXTSTEP・OpenStep・Cocoa ― 継承より委譲
1988年に発表されたNeXTのコンピュータでは、OSの上にObjective-Cで書かれたアプリケーションフレームワークが載っていました。後にNeXTSTEPと呼ばれる環境です(注14)。NeXTSTEPのApplication Kitは、MacAppやTCLと同じくアプリケーションの骨格を提供するフレームワークですが、拡張の仕方に違いがあります。
MacAppでは、振る舞いを変えたければフレームワークのクラスを継承して上書きするのが基本でした。NeXTSTEPでは、フレームワークのクラスはそのまま使い、別のオブジェクトを結び付けて振る舞いを変える方法が多く用意されました。
- デリゲート:ウインドウやアプリケーションのオブジェクトが、判断の一部を別のオブジェクトに問い合わせる
- ターゲット・アクション:ボタンなどの部品が、操作されたときに送るメッセージ(アクション)と送り先(ターゲット)を持つ
- レスポンダチェーン:送り先を決めないアクションやキー入力を、選択中のビューから順に上位へ回して処理させる
レスポンダチェーンは、MacAppのターゲットやTCLのgopherから始まる連鎖と同じ考え方の仕組みです。デリゲートとターゲット・アクションは、Interface Builderで部品とオブジェクトを結び付ける操作と組み合わせて使われ、サブクラスを作らずにアプリケーションを組み立てられる範囲を広げました。
デリゲートの形を、現在のObjective-Cで書くと次のようになります。
@class Downloader;
@protocol DownloaderDelegate <NSObject>
- (void)downloaderDidFinish:(Downloader *)downloader;
@optional
- (void)downloader:(Downloader *)downloader didReceiveBytes:(NSUInteger)count;
@end
@interface Downloader : NSObject
@property (nonatomic, weak) id<DownloaderDelegate> delegate;
- (void)start;
@end
@implementation Downloader
- (void)start {
for (NSUInteger i = 0; i < 3; i++) {
// 任意のメソッドは、デリゲートが実装しているときだけ呼ぶ
if ([self.delegate respondsToSelector:@selector(downloader:didReceiveBytes:)]) {
[self.delegate downloader:self didReceiveBytes:1024];
}
}
[self.delegate downloaderDidFinish:self];
}
@end
Downloader を使う側は、Downloader を継承せず、デリゲートのプロトコルに準拠したオブジェクトを用意して delegate に設定します。任意のメソッドを実装していなくても、respondsToSelector: で実行時に確かめてから送るので問題ありません。実装されているメソッドを実行時に問い合わせられることが、この設計の前提になっています。
NeXTSTEPのAPIは、1994年にOpenStepという仕様として公開され、Foundation KitとApplication Kitの構成が整理されました。NeXTがAppleに買収された後、OpenStepはMac OS XのCocoaになり、iOSのUIKitにも同じ設計が引き継がれています(注15)。
8. Objective-CとC++ ― 遅延束縛と、コストのかからない抽象化
Objective-CとC++は、どちらもCにオブジェクト指向の機能を加えた言語ですが、手本にしたものと優先したものが違います。
Objective-C
Objective-Cは、Cにはほとんど手を加えず、Smalltalkのメッセージ送信をその上に載せた言語です。[receiver message] の形のメッセージ送信は、実行時にレシーバのクラスからメソッドを探して呼び出します。
NSArray *items = @[@"text", @42, [NSDate dateWithTimeIntervalSince1970:0]];
for (id item in items) {
// どのクラスのオブジェクトかは実行時まで決まらない
NSLog(@"%@ -> %@", [item class], [item description]);
}
id something = @"hello";
if ([something respondsToSelector:@selector(uppercaseString)]) {
NSLog(@"%@", [something uppercaseString]); // HELLO
}
NSString *nothing = nil;
NSLog(@"%lu", (unsigned long)[nothing length]); // nil へのメッセージは 0 を返す
手元の環境で実行すると、1つ目のループのクラス名は __NSCFConstantString・NSConstantIntegerNumber・__NSTaggedDate と表示されました。NSString や NSNumber の実体が何というクラスなのかは、使う側は知らなくてよく、description というメッセージに応じてくれれば足ります。nil へのメッセージ送信がエラーにならず、何もせずに0を返すのも、Smalltalk由来のメッセージ中心の考え方を反映した仕様です。
7節のデリゲートで、任意のメソッドを respondsToSelector: で確かめてから呼べたのも、この性質によるものです。
C++
C++は、Bjarne Stroustrup がベル研究所で1979年に始めた「C with Classes」を出発点とする言語です(注16)。手本にしたのはSmalltalkではなくSimulaで、Simulaのクラスによる構造化の利点を、Cと同等の実行効率で得ることを目標にしました。
C++の設計で繰り返し語られるのが、使わない機能にはコストを払わない、という原則です。メンバ関数は標準では静的に結び付けられ、virtual を付けた関数だけが実行時に呼び出し先を選びます。テンプレートは、型を引数にした抽象化をコンパイル時に展開します。
// 抽象データ型:内部表現を private に隠し、操作だけを公開する
template <typename T>
class Stack {
public:
void push(const T& x) { items_.push_back(x); }
T pop() {
if (items_.empty()) throw std::underflow_error("empty stack");
T x = items_.back();
items_.pop_back();
return x;
}
bool empty() const { return items_.empty(); }
private:
std::vector<T> items_;
};
// 実行時に振る舞いを切り替えたいところだけ virtual にする
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return M_PI * r_ * r_; }
private:
double r_;
};
Stack は、構造化プログラミングの記事のModula-2の Stacks モジュールと同じく、内部表現を隠して操作だけを公開する抽象データ型です。違いは、型引数 T によって要素の型を問わずに書けることと、コンストラクタ・デストラクタによって資源の確保と解放を型に結び付けられることです。継承も仮想関数も使っていませんが、C++ではこれもクラスの主要な使い方です。
Stroustrupは、手続き型、モジュール、データ抽象(抽象データ型による設計)、オブジェクト指向(継承と仮想関数による設計)をそれぞれ別のプログラミングの様式として区別し、C++はオブジェクト指向だけでなくそのすべてを支援する言語だと説明しています(注17)。後にはこれを「マルチパラダイム」と呼ぶようになりました。
実際、C++のコードはオブジェクト指向で書かれているとは限りません。Cと同じ書き方の手続き型のコードもそのままC++のコードですし、1990年代に Alexander Stepanov らが設計し、1998年の標準規格に取り込まれた標準テンプレートライブラリ(STL)は、継承も仮想関数もほとんど使わず、テンプレートでコンテナとアルゴリズムを組み合わせるジェネリックプログラミングの様式で書かれています。
2つの「オブジェクト」
William Cook は2009年の論文で、抽象データ型とオブジェクトは、データを隠すという点では似ていても、別の仕組みだと整理しました(注18)。抽象データ型は型の内側から同じ型の値の表現を見ることができ、Stack 同士の比較なら相手の内部も参照できます。オブジェクトは、相手が同じクラスかどうかに関係なく、メッセージ(インタフェース)を通してしか相手に触れません。
Objective-CとSmalltalkはオブジェクトの側に、C++のクラスの多くの使い方は抽象データ型の側に寄っています。
9. プロトコルと分散オブジェクト ― NeXTSTEPとMach
Machとメッセージ
NeXTSTEPのOSの中心には、カーネギーメロン大学で Richard Rashid らが開発したMachカーネルがありました。NeXTはMach 2.5にBSD UNIXを組み合わせて使い、Machの開発者の一人である Avie Tevanian がNeXTでソフトウェア開発を率いました(注19)。
Machでは、タスク(プロセス)同士がポートと呼ばれる保護されたメッセージキューにメッセージを送って通信します。カーネルへの要求も、ポートへのメッセージとして扱う設計でした。Objective-Cのメッセージ送信とは層の違う仕組みですが、次に見る分散オブジェクトでは、このポートが通信路として使われます。
分散オブジェクト
NeXTSTEPは、Objective-Cのメッセージ送信をプロセスの境界を越えて使えるようにする分散オブジェクト(Distributed Objects)を備えていました。NeXTが1993年に出したObjective-Cの解説書は、その仕組みを次のように説明しています(注20)。
- 相手のオブジェクトとの接続を確立すると、自分のアドレス空間に相手の代理(プロキシ)ができる
- プロキシは相手のコピーではなく、受け取ったメッセージを相手に運ぶだけの軽い代役で、相手のクラスを知る必要がない
- 送り手と受け手は別々にスケジュールされるので、届いたメッセージは受け手の都合のよいときに取り出せるよう、キューに置かれる
プロキシが成り立つのは、Objective-Cのメッセージ送信が実行時に解決され、自分で処理できないメッセージを別のオブジェクトへ転送する仕組み(フォワーディング)があるからです。現在のFoundationの NSProxy で、同じ構造を確かめられます。
@protocol Calculator
- (int)add:(int)a to:(int)b;
- (oneway void)reset; // 結果を待たずに戻る(非同期)
@end
// 受け取ったメッセージを、自分では実行せずに別のオブジェクトへ回す代理
@interface ForwardingProxy : NSProxy
- (instancetype)initWithTarget:(id)target;
@end
@implementation ForwardingProxy {
id _target;
}
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [_target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
NSLog(@"forward: %@", NSStringFromSelector(invocation.selector));
[invocation invokeWithTarget:_target]; // 分散オブジェクトでは、ここでプロセス間通信を行う
}
@end
ForwardingProxy は add:to: も reset も実装していません。メッセージを受け取ると、ランタイムがそれを NSInvocation という1つのオブジェクトにまとめて forwardInvocation: に渡します。この例では同じプロセスの中の別のオブジェクトに渡していますが、分散オブジェクトのプロキシは、ここでメッセージを直列化して相手のプロセスに送ります。Cocoaの分散オブジェクトでは、通信路として、Machのポートを包む NSMachPort(同じマシンの中)と、BSDのソケットを使う NSSocketPort(ネットワーク越し)が用意されていました(注21)。分散オブジェクトは現在では古い技術の扱いで、Appleはプロセス間通信にXPCを使うよう勧めています。
プロトコル
クラスを知らない相手にメッセージを送るには、相手が応じられるメッセージの一覧だけを宣言する手段が必要です。NeXTはこのためにObjective-Cにプロトコル(@protocol)を加えました。先の解説書は、クラス名を明かさない「匿名のオブジェクト」の用途として、部品を他の開発者に提供する場合と、別のアプリケーションにあるオブジェクトにメッセージを送る場合の2つを挙げています。遠隔のオブジェクトを公開するアプリケーションは、プロトコルを公開すれば足り、それ以外の内部を明かす必要はありません。
プロトコルには2種類ありました。
- 非形式プロトコル(informal protocol):ルートクラスのカテゴリとしてメソッドを並べたもので、言語上の検査はない。7節のデリゲートのメソッドの多くは、もともとこの形で宣言されていた
- 形式プロトコル(formal protocol):
@protocolで宣言し、クラスが準拠を表明する。コンパイラが検査でき、実行時にconformsTo:(現在のconformsToProtocol:)で準拠を確かめられる
上のコードの oneway は、形式プロトコルの中でだけ使える修飾子です。ほかに、引数の受け渡しの方向を示す in・out・inout と、プロキシではなくコピーを送らせる bycopy がありました。どれも、メッセージがプロセスの境界を越えるときの扱いを指定するものです。
プロトコルは、実装を引き継がずに仕様だけを多重に継承する仕組みです。クラスは単一継承のまま、プロトコルはいくつでも採用できます。
Javaのinterface
1995年に公開されたJavaは、クラスの単一継承と、interface の多重実装という組み合わせを採りました。Javaの初期の開発者の一人である Patrick Naughton は、Objective-CとNeXTSTEPから強い影響を受けたと述べたとされ、Javaの interface はObjective-Cのプロトコルを名前を変えて取り入れたものだ、という話が広く伝わっています(注22)。
interface Calculator {
int add(int a, int b);
}
Javaでは、インタフェースは静的な型としてコンパイル時に検査され、Objective-Cのように実行時に respondsToSelector: で確かめる使い方はしません。一方で、分散オブジェクトの考え方はJavaにも現れます。JDK 1.1のRMI(Remote Method Invocation)では、遠隔から呼び出せるメソッドを java.rmi.Remote を継承したインタフェースとして宣言し、利用側はそのインタフェースを実装したスタブ(代理)を通して呼び出します。JDK 1.3で加わった java.lang.reflect.Proxy は、指定したインタフェースを実装する代理オブジェクトを実行時に作り、呼び出しを1つのハンドラにまとめて渡します。
10. バイナリの部品 ― COM・SOM・OpenDoc・JavaBeans
4節のソフトウェア部品の構想は、1990年代に「コンポーネント」として再び取り組まれます。焦点になったのは、別々にコンパイルされ、別々に配布されたバイナリをどう組み合わせるかでした。
C++のクラスは、そのままではこの用途に向きません。クラスにメンバ変数や仮想関数を1つ加えると、オブジェクトのメモリ上の配置や仮想関数表の並びが変わり、そのクラスを使う側を再コンパイルしないと正しく動かなくなります。コンパイラが違えば、配置の規則自体が違うこともあります。SOMの資料などでは、これを脆弱なバイナリインタフェース問題(fragile binary interface problem)と呼んでいます。
COM
COMは、1992年のOLE 2の基盤として公開された、言語に依存しないバイナリの規格です(注23)。部品はインタフェースを通してだけ使い、実装とは完全に分離します。すべてのインタフェースは IUnknown を継承し、QueryInterface で同じオブジェクトの別のインタフェースを問い合わせ、AddRef と Release で参照カウントを管理します。仕組みを簡略化してC++で書くと次のようになります。
// COM の IUnknown を簡略化したもの(実際は GUID で問い合わせ、呼び出し規約も規定される)
struct IUnknown {
virtual int QueryInterface(const char* iid, void** out) = 0;
virtual unsigned long AddRef() = 0;
virtual unsigned long Release() = 0;
};
struct IShape : IUnknown {
virtual double Area() = 0;
};
struct IPrintable : IUnknown {
virtual void Print() = 0;
};
class Square final : public IShape, public IPrintable {
public:
explicit Square(double side) : side_(side) {}
int QueryInterface(const char* iid, void** out) override {
if (std::strcmp(iid, "IShape") == 0) *out = static_cast<IShape*>(this);
else if (std::strcmp(iid, "IPrintable") == 0) *out = static_cast<IPrintable*>(this);
else { *out = nullptr; return -1; }
AddRef();
return 0;
}
unsigned long AddRef() override { return ++refs_; }
unsigned long Release() override {
unsigned long n = --refs_;
if (n == 0) delete this;
return n;
}
double Area() override { return side_ * side_; }
void Print() override { std::printf("Square(%g)\n", side_); }
private:
double side_;
unsigned long refs_ = 1;
};
インタフェースは純粋仮想関数だけを持つ構造体で、その仮想関数表の並びがそのままバイナリの規格になります。利用する側が知っているのはインタフェースだけで、Square の内部の配置が変わっても影響を受けません。COMはインタフェースの継承は持ちますが、実装の継承は持たず、既存の部品を再利用するときは、内側に別の部品を持って呼び出しを回す包含(containment)か、内側の部品のインタフェースをそのまま見せる集約(aggregation)を使います。8節のCookの区別に当てはめると、COMの部品は、インタフェースを通してしか相手に触れないオブジェクトの側に属します。ネットワーク越しに呼び出すDCOMも、後に加わりました。
SOMとOpenDoc
IBMのSOM(System Object Model)は、同じ問題に別の答えを出しました。SOMは実装の継承も扱い、メソッドの表を実行時に組み立てることで、ライブラリにメソッドを加えても利用する側を再ビルドせずに済むようにしました。ある言語で定義したクラスを別の言語から使うこともできます。CORBAに基づく分散版のDSOMも用意されていました(注24)。
AppleとIBMなどが進めたOpenDocは、SOMを基盤に採用した複合文書の仕組みです。1つの文書の中に、別々の開発元が作った部品(パーツ)を並べ、それぞれをその場で編集します。COMを基盤にしたOLEの対抗として位置づけられましたが、部品の数で差を付けられ、1997年3月に開発が打ち切られました。
9節で引いたNeXTの1993年の解説書は、遠隔メッセージの用途として、データの種類ごとに別の編集ツールを用意し、それぞれ別のプロセスとして1つの文書の編集に協力させる、という例を挙げています。OpenDocとOLEの複合文書は、これに近いことを、バイナリの部品を単位にして行う仕組みでした。
JavaBeans
1996年にJDK 1.1とともに発表されたJavaBeansは、部品の規格を、命名規則と実行時のイントロスペクションで定めました(注25)。引数なしの公開コンストラクタを持ち、プロパティを get・set(真偽値なら is)で始まるメソッドで公開し、直列化できるクラスが、Beanとして扱われます。
public static class Person implements Serializable {
private String name = "";
private int age;
private boolean active;
public Person() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
// メソッド名からプロパティを推定する
BeanInfo info = Introspector.getBeanInfo(Person.class, Object.class);
for (PropertyDescriptor p : info.getPropertyDescriptors()) {
System.out.println(p.getName() + " : " + p.getPropertyType().getSimpleName());
}
実行すると active : boolean・age : int・name : String の3行が表示されます。Person はどのインタフェースも宣言していませんが、メソッドの名前から、ツールがプロパティの一覧を読み取れます。ビジュアルな開発ツールは、この仕組みで部品を並べ、プロパティを設定するパネルを自動で作りました。
COMとSOMがバイナリの配置とインタフェースで部品の境界を決めたのに対し、JavaBeansは、Javaの仮想マシンとリフレクションを前提に、名前の付け方を部品の規格にしました(付録も参照)。
11. Eiffelが考える世界 ― 契約による設計
Bertrand Meyer が1985年に設計を始めたEiffelは、オブジェクト指向をソフトウェアの正しさと再利用の問題として捉え直した言語です(注26)。Meyerは1988年の著書『Object-Oriented Software Construction』で、クラスを抽象データ型の実装と位置づけ、その仕様をクラスの中に書き込む方法を示しました。
Eiffelでは、ルーチン(メソッド)に事前条件(require)と事後条件(ensure)を、クラスに不変条件(invariant)を書けます。
class ACCOUNT
create
make
feature
balance: INTEGER
make
do
balance := 0
end
deposit (amount: INTEGER)
require
positive_amount: amount > 0
do
balance := balance + amount
ensure
increased: balance = old balance + amount
end
withdraw (amount: INTEGER)
require
positive_amount: amount > 0
sufficient_funds: amount <= balance
do
balance := balance - amount
ensure
decreased: balance = old balance - amount
end
invariant
non_negative_balance: balance >= 0
end
事前条件は呼び出す側が守る義務、事後条件は呼び出される側が守る義務です。Meyerはこれを、発注者と受注者の契約になぞらえて「契約による設計(Design by Contract)」と呼びました。withdraw の事前条件が満たされない呼び出しは呼び出し側の誤りであり、withdraw の中で残高不足を調べて何かを返す処理は不要です。
これは、構造化プログラミングの記事で見たHoare論理の事前条件・事後条件と、ループ不変条件の考え方をクラスの単位に広げたものです。クラス不変条件は、どの公開ルーチンの実行前後でも成り立つ性質で、ループ不変条件と同じく帰納法の形で確かめられます。
契約は継承とも結び付いています。Eiffelでは、サブクラスでルーチンを再定義するとき、事前条件は弱めることしかできず(require else)、事後条件は強めることしかできません(ensure then)。親クラスの契約を前提に書かれたコードが、サブクラスのオブジェクトを受け取っても正しく動くようにするための規則です。
同じ問題を、Barbara Liskov は1987年の講演で型の側から論じ、後に Jeannette Wing と「振る舞いによる部分型」として定式化しました(注27)。リスコフの置換原則と呼ばれるものです。Meyerは継承の用途を細かく分類していて、実装を再利用するための継承も認めています。そのうえで、どの用途でも、再定義の際に親クラスの契約を引き継ぐことを言語の規則にしています。
Meyerは同じ著書で、モジュールは拡張に対して開いていて、修正に対しては閉じているべきだという開放閉鎖原則も示しています。多重継承、総称型(ジェネリクス)、コマンドとクエリの分離など、Eiffelで整理された考え方の多くは、後の言語や設計原則に取り込まれました。
12. 継承の見直し ― 合成とKotlin
1990年代に入ると、継承の使いすぎによる問題が知られるようになります。10節ではバイナリの配置から見た脆弱な基底クラス問題(fragile base class problem)に触れましたが、同じ問題はソースコードの水準でも起こります。サブクラスが親クラスの実装の細部に依存していると、親クラスを修正しただけで、サブクラスの振る舞いが壊れることがあります。
1994年の書籍『Design Patterns』(いわゆるGoF本)は、「クラスの継承よりもオブジェクトの合成を優先する」ことを原則の一つに掲げました(注28)。7節のNeXTSTEPのデリゲートは、この本より前から、フレームワークの設計に合成を取り入れていました。
Kotlinは、この反省を言語の既定値に反映しています。
- クラスもメソッドも、
openを付けない限り継承・上書きできない -
interfaceは実装付きのメソッドを持てる -
byを使うと、インタフェースの実装を別のオブジェクトに委譲できる -
data classで、値を表すクラスのequals・hashCode・toStringを自動生成できる
interface Repository {
fun find(id: Int): String?
fun save(id: Int, value: String)
}
class MemoryRepository : Repository {
private val items = mutableMapOf<Int, String>()
override fun find(id: Int): String? = items[id]
override fun save(id: Int, value: String) { items[id] = value }
}
// Repository の実装は inner に任せ、save だけ振る舞いを足す
class LoggingRepository(private val inner: Repository) : Repository by inner {
override fun save(id: Int, value: String) {
println("save $id")
inner.save(id, value)
}
}
LoggingRepository は MemoryRepository を継承していません。Repository by inner によって、上書きしなかった find の呼び出しはコンパイラが生成したコードで inner に転送されます。親クラスの実装に依存しないので、MemoryRepository の内部を変えても LoggingRepository は影響を受けません。
継承を既定で閉じるという判断は、Joshua Bloch の『Effective Java』の「継承のために設計して文書化せよ、さもなくば継承を禁止せよ」という項目と同じ方向のものです(注29)。継承は、フレームワークの設計者が意図して開いた箇所でだけ使うもの、という位置づけになっています。
13. Swiftのプロトコル指向
Swiftは、Objective-Cのクラスとの互換性を保ちながら、構造体と列挙型を値型として前面に出した言語です。2015年のWWDCでは「Protocol-Oriented Programming in Swift」というセッションが行われ、クラスの継承から設計を始めるのではなく、プロトコルから始めることが提案されました(注30)。「プロトコル」という名前と考え方は、9節で見たObjective-Cのプロトコルを引き継いだものです。
protocol Shape {
var area: Double { get }
}
extension Shape {
// プロトコル拡張:準拠するすべての型に実装付きで追加される
func isLarger(than other: some Shape) -> Bool {
area > other.area
}
}
struct Circle: Shape {
var radius: Double
var area: Double { .pi * radius * radius }
}
struct Square: Shape {
var side: Double
var area: Double { side * side }
}
let c = Circle(radius: 1)
let s = Square(side: 2)
print(s.isLarger(than: c)) // true
var t = s
t.side = 1
print(s.side, t.side) // 2.0 1.0(構造体は値としてコピーされる)
Circle と Square は継承関係を持たない構造体です。var t = s の代入では値がコピーされるので、t を変えても s は変わりません。共通の処理は、isLarger(than:) のようにプロトコル拡張に一度書けば、準拠するすべての型で使えます。既存の型にも、後から準拠を加えられます。
protocol Describable {
var summary: String { get }
}
extension Int: Describable {
var summary: String { "整数 \(self)" }
}
extension Circle: Describable {
var summary: String { "半径 \(radius) の円" }
}
let things: [any Describable] = [42, c]
for thing in things {
print(thing.summary)
}
標準ライブラリの Int を変更せずに、自分で定義した Describable に準拠させています。クラスの継承では、親クラスはクラスを定義する時点で1つに決めなければなりませんが、プロトコルへの準拠は後から、必要な分だけ追加できます。
some Shape のようにプロトコルを型の制約として使えばコンパイル時に呼び出し先が決まり、any Describable のように存在型として使えば実行時に決まります。8節のC++では、テンプレートと仮想関数という別々の仕組みで行っていた使い分けです。
Swiftはクラスも持っていて、UIKitやAppKitのようなObjective-C由来のフレームワークを使うときは、継承とデリゲートが今も主役です。プロトコル指向は、アプリケーション固有のモデルやロジックを書くときの選択肢として提案されたもので、クラスの置き換えを求めたものではありません。
14. 混ざっている話を分ける
ここまでの話を、はじめにで挙げた5つの問いに沿って並べると次のようになります。
| 問い | 代表的な言語・環境 | クラス・継承の役割 | 主な関心 |
|---|---|---|---|
| 対象をどうモデル化するか | Simula 67 | 現実の系の構成要素を写す | 系の構造とプログラムの構造を一致させる |
| 作ったものをどう再利用するか | Objective-C(Software-IC) | メッセージで組み合わせる部品 | 作り手と使い手の分離、実行時の組み合わせ |
| アプリケーションの骨格を誰が持つか | Lisa Toolkit、MacApp、TCL、NeXTSTEP/Cocoa | フレームワークの枠に差分を差し込む。あるいはデリゲートで結び付ける | 制御の反転、共通処理の集約 |
| 境界を越えてどう組み合わせるか | NeXTSTEPの分散オブジェクト、COM、SOM/OpenDoc、Java(RMI、JavaBeans) | プロトコル・インタフェースで部品の境界を決める。実装の継承は境界の外に出さない | バイナリ互換、プロセス・マシンをまたぐ呼び出し、ツールによる組み立て |
| 型と振る舞いの関係をどう保証するか | CLU、C++、Eiffel、Kotlin、Swift | 抽象データ型、契約を引き継ぐ部分型、プロトコル | 正しさ、変更の影響範囲、コンパイル時の検査 |
Smalltalkは、メッセージという仕組みを通じて、この表のほとんどの行に関わっています。
オブジェクト指向についてよく聞く説明も、この表のどこかに当てはまります。
- 「現実世界をそのまま写す」という説明は、モデル化の問いに対するものです。業務システムの分析では役に立ちますが、フレームワークのクラス階層は現実の分類を写したものではありません
- 「継承して上書きする」という作業は、主にフレームワークの問いに属します。フレームワークの設計者が開いた箇所に差分を書く作業で、自分のアプリケーションのモデルを継承で分類することとは目的が違います
- 「インタフェースに対してプログラムする」という助言は、主に境界を越える問いから来ています。部品の作り手と使い手が別々にコンパイル・配布する状況で、実装の継承を境界の外に出さないための規則です
- 「継承は is-a 関係のときだけ使う」という規則は、型の保証の問いから来ています。Eiffelの契約やリスコフの置換原則を短く言い表したものです
- MacAppは標準オブジェクト以外を手続き型で書くことを認めていて、C++はオブジェクト指向以外の様式も支援しています。オブジェクト指向の言語やライブラリを使っていても、すべてをオブジェクト指向で設計する必要はありません
まとめ
- Simula 67は、シミュレーションのためにクラス・サブクラス・仮想手続きを導入した。「現実世界をモデル化する」という説明はこの出自による
- Smalltalkは、メッセージ・状態の隠蔽・遅延束縛を中心に置いた。この考えはObjective-Cを経て、NeXTSTEPのデリゲートや分散オブジェクトに受け継がれた
- MacApp・TCL・NeXTSTEP/Cocoaのようなアプリケーションフレームワークでは、骨格をフレームワークが持ち、アプリケーションは差分を書く。継承は、その差分を差し込む手段として使われた
- Objective-Cのプロトコル、COM・SOM・JavaBeansは、別々に作られた部品を組み合わせるために、実装を隠してインタフェースや命名規則だけで部品の境界を決めた
- C++・Eiffel・Kotlin・Swiftは、型と振る舞いの関係を言語で保証する方向に進んだ。Eiffelの契約、Kotlinの既定で閉じたクラスと委譲、Swiftのプロトコル指向がその例である
付録 MacAppとTCLの命名規則
MacAppとTCLのコードでは、識別子の先頭の1〜数文字で、その名前が何を指すかを表していました。言語が強制する規則ではなく、フレームワークの資料とサンプルコードを通じて広まりました。以下は、5節で触れたMacAppの1985年の解説資料と、6節で触れたTCLの解説書(1995年)に現れる名前から整理したものです。
MacApp(Object Pascal)
| 接頭辞 | 対象 | 例 |
|---|---|---|
T |
オブジェクト型(クラス) |
TApplication、TDocument、TView、TWindow、TCommand
|
f |
フィールド(インスタンス変数) |
fPieceArray、fBlankRow、fObjList
|
I + 型名 |
初期化メソッド |
IView、IListView、IPuzzle
|
c |
コマンド番号の定数 |
cCut、cTyping、cStretch
|
g |
グローバル変数 | gTarget |
a |
引数 | aCmd |
Do |
フレームワークから呼ばれる処理 |
DoMenuCommand、DoKeyCommand、DoSetupMenus、DoIt、UndoIt、RedoIt
|
1985年の資料は、MacAppの型がすべて T で始まるのは、Macintosh Toolboxで宣言されたグローバルな名前との衝突を避けるためで、アプリケーションが宣言する型は T で始めなくてもよい、と説明しています。それでも多くのアプリケーションがこの規則に合わせました。Pascalは大文字と小文字を区別しないので、型名に T が付いていれば、window: TWindow のように型と同じ語を変数名に使えます。
資料に載っている15パズルの書類の型は、次のように宣言されています(読みやすさのために体裁を整え、コメントを日本語にしています)。
TPuzzle = object(TDocument)
fPieceArray: array[0..3, 0..3] of integer;
fBlankCol, fBlankRow: integer;
procedure IPuzzle; { パズルを初期化する }
procedure Scramble; { 駒をかき混ぜる }
function MovePiece(oldLoc: Coords; var newLoc: Coords): boolean;
end;
Object Pascalのオブジェクトは New で領域を確保した後、I で始まる初期化メソッドを呼んで使い始めます。言語にコンストラクタがなかったため、初期化を名前の規則で表していました。メソッドの中ではフィールドを self. なしで参照できるので、f はフィールドと引数・局所変数を見分ける目印になります。
TCL(THINK C/Symantec C++)
| 接頭辞 | 対象 | 例 |
|---|---|---|
C |
クラス |
CApplication、CDocument、CPane、CBureaucrat、CDirector
|
its |
インスタンス変数 |
itsSupervisor、itsWindow、itsMainPane
|
g |
グローバル変数 |
gApplication、gGopher、gBartender、gDesktop
|
cmd |
コマンド番号の定数 |
cmdNew、cmdOpen、cmdPaste、cmdAbout
|
I + クラス名 |
初期化メソッド |
ICApp、Ix_CApp、ICMain
|
a / the
|
引数・局所変数 |
aSupervisor、theCommand、theDocument
|
Do |
コマンドやイベントを処理するメソッド |
DoCommand、DoClick、DoKeyDown
|
x_ |
Visual Architectが生成する基底クラス |
x_CApp、x_CMain
|
型の接頭辞はMacAppの T に対して C、インスタンス変数はMacAppの f に対して its です。itsWindow・itsSupervisor は、英語として「その(オブジェクトの)ウインドウ」と読めます。gBartender はメニューバーを管理するオブジェクト、gGopher は6節のコマンドの連鎖の起点です。
x_ は、ユーザーインタフェースの設計ツールVisual Architectが生成するクラスに付く接頭辞です。Visual Architectは、x_CApp のような基底クラスと、それを継承した CApp のようなサブクラスを対で生成します。画面の設計を変えるたびに基底クラスは生成し直されますが、サブクラスは最初に一度生成されるだけで、開発者が書き加えたコードはこちらに残ります。生成されるコードと人が書くコードを、継承で分けておく使い方です。
コマンドの処理は、DoCommand で switch を書き、処理しないコマンドを基底クラスの DoCommand に回す形になります。解説書の生成コードに沿って書くと次のようになります。
void CApp::DoCommand(long theCommand)
{
switch (theCommand) {
case cmdAbout:
// バージョン情報のダイアログを表示する
break;
default:
// 処理しないコマンドは、継承した DoCommand に任せる
x_CApp::DoCommand(theCommand);
break;
}
}
基底クラスをたどった先の CBureaucrat の DoCommand が、コマンドをsupervisorのオブジェクトに回します。まずクラスの継承をさかのぼり、その後に6節のオブジェクトの連鎖に移る、という順序です。
その後の命名規則
NeXTSTEP 3までのApplication Kitには、Window・View・Application のように接頭辞のないクラスと、NXBundle・NXProxy のように NX を付けたクラスが混在していました。OpenStepでは、クラスにも関数にも NS を付ける形に統一され、Cocoaに引き継がれました。アプリケーションの側も、自分のクラスに2〜3文字の接頭辞を付けるのが慣習になりました。インスタンス変数の接頭辞は使わないか、_ を付ける程度で、代わりにメソッド名を英語の文として読めるように付ける規則が重視されました(downloader:didReceiveBytes: や、デリゲートの will・did・should など)。
Cocoaのキー値コーディングは、name というキーに対して name・setName: といった名前のメソッドを実行時に探します。10節のJavaBeansの get・set・is と同じく、名前の規則がそのまま実行時の仕組みの一部になっています。MacAppやTCLの命名規則は人が読むための約束でしたが、キー値コーディングやJavaBeansでは、ツールやランタイムが名前を読み取って動作を決めます。
注釈
- Barbara Liskov・Stephen Zilles, "Programming with Abstract Data Types"(ACM SIGPLAN Notices, 1974年4月)。抽象データ型の考えを示し、これを言語で支援する方法を論じた。CLUの設計は同時期に進められ、Liskov らによる "Abstraction Mechanisms in CLU"(Communications of the ACM, 1977年8月)で詳しく報告されている。
- Kristen Nygaard・Ole-Johan Dahl, "The Development of the SIMULA Languages"(ACM SIGPLAN Notices, 1978年8月、HOPL)。Simula IからSimula 67に至る経緯を、設計者自身が述べている。Simula 67は1967年5月のIFIPワーキングカンファレンスで発表された。
- C. A. R. Hoare, "Record Handling"(ALGOL Bulletin, 1965年)。レコードの型と、型付きの参照を言語に導入する提案。Dahl・Nygaardは、この提案をSimula 67のクラスとサブクラスの設計に取り入れたと述べている。
- Alan C. Kay, "The Early History of Smalltalk"(ACM SIGPLAN Notices, 1993年3月、HOPL-II)。Smalltalk-72から-80に至る経緯と、Simula、Sketchpad、LISPなどから受けた影響をKay自身が述べている。
- Alan Kay が2003年に Stefan Ram へ送った電子メール。"OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things." という一文が、Ramのウェブページで公開されている。
- M. D. McIlroy, "Mass Produced Software Components"。1968年のNATOソフトウェア工学会議で発表され、Peter Naur・Brian Randell 編の会議報告書(1969年)に収録されている。
- Brad J. Cox, "Object-Oriented Programming: An Evolutionary Approach"(Addison-Wesley, 1986年)。Objective-Cの解説とともに、Software-ICの考えを示した。Coxは後に "Planning the Software Industrial Revolution"(IEEE Software, 1990年11月)でこの構想を発展させている。
- Clascal - Wikipedia、および Computer History Museum のLisaに関する記事による。Larry Tesler は、Lisa向けのSmalltalkが遅く、構文もなじみにくいことから、Pascalを拡張する道を選んだと述べている。
- MacApp - Wikipedia による。MacAppはLisa Toolkitの流れを汲み、Object Pascalで書かれた。1991年のバージョン3.0でC++に移行した。Object Pascalの拡張は Tesler が率いるチームが Niklaus Wirth と相談しながら進めた。
- Ralph E. Johnson・Brian Foote, "Designing Reusable Classes"(Journal of Object-Oriented Programming, 1988年6/7月)。フレームワークを、抽象クラスの集合とそれらの協調の設計として定義し、制御の反転をフレームワークの特徴として挙げた。
- Apple Computer, "An Introduction to MacApp"(Version 0.1, 1985年2月14日)。MacAppの最初の解説資料で、アプリケーションのオブジェクトを標準オブジェクト(standard objects)と個別のオブジェクト(particular objects)に分け、後者はオブジェクト型として実装しなくてよいと述べている。Object PascalはWirthの協力を得て設計されたこと、Objective-Cやアセンブラからも使えるようにする予定であることも書かれている。
- THINK C - Wikipedia による。TCLはTHINK C 4.0から付属し、Macintoshのアプリケーション開発で、MacAppに代わる事実上の標準のクラスライブラリとして使われた。オブジェクト指向の機能は、THINK Cのバージョン6以降で完全なC++の実装に拡張された。
- Richard O. Parker, "Mastering the THINK Class Library: Using Symantec C++ and Visual Architect"(Addison-Wesley, 1995年)の第1章による。コマンドは32ビットの値(コマンドトークン)としてgopherに送られ、処理されなければ連鎖の上位に渡される。
- NeXTSTEP - Wikipedia による。NeXTはObjective-CをStepstone社からライセンスして採用した。Interface Builderは Jean-Marie Hullot が開発した。
- OpenStep - Wikipedia、Cocoa (API) - Wikipedia による。OpenStepの仕様は1994年にNeXTとサン・マイクロシステムズが公開した。
- Bjarne Stroustrup, "The Design and Evolution of C++"(Addison-Wesley, 1994年)。C with ClassesからC++に至る設計判断を、Stroustrup自身が述べている。Simulaの経験が出発点であることもここに書かれている。
- Bjarne Stroustrup, "What is 'Object-Oriented Programming'?"(ECOOP '87、改訂版はIEEE Software, 1988年5月)。手続き型、モジュール、データ抽象、オブジェクト指向をそれぞれ別のプログラミングの様式として区別し、言語がそれを「支援する」ことと「可能にする」ことを分けて論じた。
- William R. Cook, "On Understanding Data Abstraction, Revisited"(OOPSLA 2009)。抽象データ型とオブジェクトを、データ抽象の2つの異なる形として比較した。
- Mach (kernel) - Wikipedia による。Machはカーネギーメロン大学で1985年から1994年まで開発され、Richard Rashid と Avie Tevanian が中心になった。NeXTSTEPにはMach 2.5が採用された。現在のmacOS・iOSのカーネルXNUも、Machを大きく改変したものを含んでいる。
- NeXT Computer, "Object-Oriented Programming and the Objective C Language"(NeXTSTEP Release 3, 1993年)第3章。非形式プロトコルと形式プロトコル、匿名のオブジェクト、遠隔メッセージと分散オブジェクト、
oneway・in・out・inout・bycopyの5つの型修飾子を説明している。分散オブジェクトのクラスNXProxy・NXConnectionの詳細は NeXTSTEP General Reference にあるとされている。 - Apple, "Distributed Objects Programming Topics" の "Ports" の節。
NSMachPortはMachのポートを包むクラスで同じマシンの中の通信に、NSSocketPortはBSDのソケットを使い、マシンをまたぐ通信にも使える。この文書は廃止扱いで、冒頭でプロセス間通信にはXPCを使うよう案内している。 - Sean Luke(George Mason University)のページ "Java Was Strongly Influenced by Objective-C" が、Naughton のものとされるUsenetへの投稿を紹介している。2005年のApple cocoa-devメーリングリストにも、Javaのinterfaceは元NeXTの開発者が設計に関わったObjective-Cのプロトコルに倣ったものだ、という趣旨の投稿がある。いずれも二次的な資料で、Javaの設計者自身の文書での確認はできていない。
- Component Object Model - Wikipedia、および Don Box, "Essential COM"(Addison-Wesley, 1998年)による。COMは1992年のOLE 2で導入された。包含と集約による再利用、
IUnknownの3つのメソッドの役割はBoxの書籍に詳しい。 - IBM System Object Model - Wikipedia、OpenDoc - Wikipedia による。SOMは実行時のリンカがメソッドの表を組み立て直すことで、利用側を再ビルドせずにライブラリを更新できるようにした。OpenDocはSOM/DSOMを基盤に採用し、1997年3月に開発が打ち切られた。
- JavaBeans - Wikipedia、および Sun Microsystems, "JavaBeans API Specification"(Version 1.01, 1997年)による。JavaBeansはJDK 1.1の一部として1996年に発表された。
- Bertrand Meyer, "Object-Oriented Software Construction"(Prentice Hall, 1988年、第2版1997年)。契約による設計、開放閉鎖原則、コマンドとクエリの分離などを示した。Eiffelの言語仕様は "Eiffel: The Language"(Prentice Hall, 1992年)にまとめられている。
- Barbara Liskov, "Data Abstraction and Hierarchy"(OOPSLA '87 基調講演、ACM SIGPLAN Notices, 1988年5月)、および Barbara Liskov・Jeannette Wing, "A Behavioral Notion of Subtyping"(ACM Transactions on Programming Languages and Systems, 1994年11月)。
- Erich Gamma・Richard Helm・Ralph Johnson・John Vlissides, "Design Patterns: Elements of Reusable Object-Oriented Software"(Addison-Wesley, 1994年)。第1章で "Favor object composition over class inheritance." という原則を示している。
- Joshua Bloch, "Effective Java"(Addison-Wesley)。第2版(2008年)の項目17、第3版(2018年)の項目19。Kotlinの公式ドキュメントも、クラスを既定でfinalにした理由としてこの項目を挙げている。
- Dave Abrahams, "Protocol-Oriented Programming in Swift"(WWDC 2015, Session 408)。Swift 2で導入されたプロトコル拡張を使い、クラス階層の代わりにプロトコルと値型で設計する方法を示した。
参考資料
- Object-oriented programming - Wikipedia
- Simula - Wikipedia
- Smalltalk - Wikipedia
- Alan Kay, The Early History of Smalltalk
- Dr. Alan Kay on the Meaning of "Object-Oriented Programming"
- Abstract data type - Wikipedia
- CLU (programming language) - Wikipedia
- Component-based software engineering - Wikipedia
- Clascal - Wikipedia
- Object Pascal - Wikipedia
- MacApp - Wikipedia
- An Introduction to MacApp, Version 0.1 (1985), bitsavers.org mirror
- THINK C - Wikipedia
- Computer History Museum, The Lisa: Apple's Most Influential Failure
- Inversion of control - Wikipedia
- NeXTSTEP - Wikipedia
- OpenStep - Wikipedia
- Cocoa (API) - Wikipedia
- Objective-C - Wikipedia
- C++ - Wikipedia
- Mach (kernel) - Wikipedia
- NeXT, Object-Oriented Programming and the Objective C Language (Release 3, 1993), bitsavers.org mirror
- Portable Distributed Objects - Wikipedia
- Apple, Distributed Objects Programming Topics: Ports
- Java Was Strongly Influenced by Objective-C (Sean Luke, George Mason University)
- Java remote method invocation - Wikipedia
- Component Object Model - Wikipedia
- IBM System Object Model - Wikipedia
- OpenDoc - Wikipedia
- JavaBeans - Wikipedia
- Apple, Key-Value Coding Programming Guide
- Bjarne Stroustrup, What is "Object-Oriented Programming"? (1991 revised version)
- Standard Template Library - Wikipedia
- Eiffel (programming language) - Wikipedia
- Design by contract - Wikipedia
- Liskov substitution principle - Wikipedia
- Fragile base class - Wikipedia
- Composition over inheritance - Wikipedia
- Kotlin: Inheritance
- Kotlin: Delegation
- The Swift Programming Language: Protocols
- The Swift Programming Language: Opaque and Boxed Protocol Types