プロトタイプ・ベースのオブジェクト指向プログラミングを採り入れる

Web 用のプログラミング・インターフェースとして最も敷居が低い JavaScript は、至るところで使用されており、Web が次第に日常生活の一部に入りこんでくるにつれ、ますます重要なものになっています。JavaScript は誤解されがちな言語であり、一部の人には「おもちゃの言語」あるいは「赤ん坊用の Java 言語」とみなされています。JavaScript が持つ特徴のなかでも汚名を着せられているものの 1 つが、プロトタイプ・ベースのオブジェクト・システムです。JavaScript に欠点があることは否定できませんが、プロトタイプ・ベースのオブジェクト・システムが欠点の 1 つであるわけではありません。この記事では、JavaScript によるプロトタイプ・ベースのオブジェクト指向プログラミングの途方もない強力さ、単純さ、スマートさについて学びます。

Delon Newman, Freelance Developer, Freelance

Photo of Delon NewmanDelon Newman は 1997年以来、楽しみとしてプログラミングをしています。彼は C と C++ から始め、その後 HTML、Perl、JavaScript に移りました。彼は 1999年から IT 業界で働いており、ヘルプデスクのサポート技術者、グラフィック・デザイナー、Web デザイナー、システム管理者、プログラマー、アナリスト、ソフトウェア技術者として働いてきました。彼自身の会社で、またコンサルタントとして働く一方、彼はこれまで、Ruby、Python、Java、C#、PHP、Smalltalk、Lisp、Haskell、Erlang、Scala、Clojure など、多くの言語や環境を経験してきました。



2012年 7月 05日

オブジェクトの世界

皆さんがいつも通りの 1 日を過ごすとき ― 車で出勤し、デスクに向かって仕事をし、食事をとり、公園を散歩するとき ―、世界を支配する物理法則の詳細を知らなくても、その世界とかかわって普通に生きていくことができます。そして、自分が毎日扱うさまざまなシステムをそれぞれ 1 つの構成単位として、つまり 1 つの対象物 (オブジェクト) として扱うことができます。皆さんはそれらのオブジェクトの複雑さを当然のものとみなし、それらのオブジェクトの操作に専念します。

歴史

一般に、最初のオブジェクト指向言語と考えられているのは、モデリング用の言語である Simula です。それに続いて、Smalltalk、C++、Java 言語、C# が登場しました。その当時、ほとんどのオブジェクト指向言語ではクラスによってオブジェクトが定義されていました。その後、Smalltalk に似たシステムである Self プログラミング言語の開発者達は、クラスでオブジェクトを定義する手法に代わり、プロトタイプ・ベース・プログラミング (つまりプロトタイプ・ベースのオブジェクト指向プログラミング) と呼ばれる軽量のオブジェクト定義手法を考え出しました。

やがて、プロトタイプ・ベースのオブジェクト・システムを持つ JavaScript が開発されました。JavaScript がよく使われるようになったことでプロトタイプ・ベースのオブジェクトも主流なものになりました。このことについて多くの開発者は不快に思っていますが、よく調べてみると、プロトタイプ・ベースのシステムには多くのメリットがあるのです。

同じように動作するソフトウェア・システムを作成するための試みであるオブジェクト指向プログラミングは、ソフトウェア開発のための強力なモデリング・ツールとして非常に幅広く使用されています。オブジェクト指向プログラミングが広く使用されている理由は、世界に対する私達の見方を反映しているからです。つまり私達は世界を対象物 (オブジェクト) の集合と捉えており、そのオブジェクトは互いに相互作用することができ、さまざまな方法で操作できるものとして認識しています。オブジェクト指向プログラミングの強力さは以下の 2 つの中心原則によるものです。

カプセル化
開発者はデータ構造の内部動作を隠すことができ、モジュール式で適応型のソフトウェアを作成するための信頼性の高いプログラミング・インターフェースを公開することができます。カプセル化は情報を隠すための手段と考えることができます。
継承
オブジェクトが他のオブジェクトのカプセル化された動作を継承できるようにすることで、カプセル化の強力さを増幅します。継承は情報を共有するための手段と考えることができます。

ほとんどの開発者はこれらの原則をよく理解していますが、それは主流となるプログラミング言語はすべてオブジェクト指向プログラミングをサポートしている (そして多くの場合は強制している) からです。すべてのオブジェクト指向プログラミング言語は上記 2 つの中心原則をサポートしていますが、そのサポート形式は、長年にわたって存在してきた少なくとも 2 通りの根本的に異なるオブジェクト定義手法のなかの 1 つになります。

この記事では、プロトタイプ・ベースのオブジェクト指向プログラミングと JavaScript のオブジェクト・パターンを使用するメリットについて学びます。


プロトタイプとは: クラスとプロトタイプ

クラスはオブジェクトを抽象的に定義します。その抽象的定義により、クラス全体またはオブジェクトの集合に対して共通のデータ構造とメソッドを定義します。各オブジェクトは、そのオブジェクトのクラスのインスタンスとして定義されます。クラスも、そのクラスの定義に従い、また (オプションとして) ユーザー・パラメーターにより、クラス・オブジェクトを作成することができます。

典型的な例が、2 次元の点を定義する Point クラスと、その子クラスとして 3 次元の点を定義する Point3D です。これらのクラスを Java コードで表現したものがリスト 1 です。

リスト 1. Java の Point クラス
class Point {
    private int x;
    private int y;

    static Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int getX() {
        return this.x;
    }

    int getY() {
        return this.y;
    }

    void setX(int val) {
        this.x = val;
    }

    void setY(int val) {
        this.y = val;
    }
}

Point p1 = new Point(0, 0);
p1.getX() // => 0;
p1.getY() // => 0;

// The Point3D class 'extends' Point, inheriting its behavior
class Point3D extends Point {
    private int z;

    static Point3D(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    int getZ() {
        return Z;
    }

    void setZ(int val) {
        this.z = val;
    }
}

Point3D p2 = Point3D(0, 0, 0);
p2.getX() // => 0
p2.getY() // => 0
p2.getZ() // => 0

クラスによってオブジェクトを定義する方法とは対照的に、プロトタイプ・ベースのオブジェクト・システムでは、より直接的な方法でオブジェクトを作成します。例えば JavaScript の場合、オブジェクトは単純なプロパティー・リストにすぎません。各オブジェクトには、別の親オブジェクト、つまりプロトタイプ・オブジェクトへの特別な参照が含まれており、そのプロトタイプ・オブジェクトから動作を継承します。Point の例を JavaScript で真似ると、リスト 2 のようになります。

リスト 2. JavaScript の Point クラス
var point = {
    x : 0,
    y : 0
};

point.x // => 0
point.y // => 0

// creates a new object with point as its prototype, inheriting point's behavior
point3D = Object.create(point);
point3D.z = 0;

point3D.x // => 0
point3D.y // => 0
point3D.z // => 0

従来のオブジェクト・システムとプロトタイプ・ベースのオブジェクト・システムとの間には根本的な違いがあります。従来のオブジェクトは概念的グループの一部として抽象的に定義され、他のオブジェクト・クラス、つまり他のオブジェクト・グループから性質を継承します。対照的に、プロトタイプ・オブジェクトは特定のオブジェクトとして具体的に定義され、他の特定のオブジェクトから動作を継承します。

つまり、クラス・ベースのオブジェクト指向言語には、クラスとオブジェクトという少なくとも 2 つの基本的な構成体を必要とする二重性があります。この二重性のため、クラス・ベースのソフトウェアは規模が拡大するにつれてクラスの階層構造が複雑になる傾向があります。一般に、クラスが将来どのように使用されるかを完全に予測することはできないため、クラスの階層構造を常にリファクタリングして変更に対応できるようにする必要があります。

プロトタイプ・ベースの言語では上述の二重性が必要なく、オブジェクトを直接作成して、操作することができます。オブジェクトがクラスに結合されないため、より疎結合のオブジェクト・システムを作成することができ、モジュール性が維持され、リファクタリングの必要性を減らすことができます。

オブジェクトを直接定義することができると、非常に強力かつ単純にオブジェクトの作成や操作ができるようになります。例えば、リスト 2 では、var point = { x: 0, y: 0 }; という 1 行で単純に point オブジェクトを宣言することができます。この 1 行により、完全に動作して JavaScript の Object.prototype (toString メソッドなど) から動作を継承するオブジェクトを作成することができます。オブジェクトの動作を継承するためには、point をプロトタイプとして単純に別のオブジェクトを宣言します。対照的に、最も簡潔な従来のオブジェクト指向言語の場合でさえ、最初にクラスを定義し、そのクラスをインスタンス化しない限り操作可能なオブジェクトは得られません。オブジェクトを継承するためには、別のクラスを定義して最初のクラスを継承する必要があります。

プロトタイプ・パターンは概念的に単純です。私達は人間として、プロトタイプを基に考えることがよくあります。例えば Steve Yegge 氏は彼のブログ記事「The Universal Design Pattern」(「参考文献」を参照) の中で、アメリカン・フットボールの選手の例を挙げています。例えば Emmitt Smith 選手は、そのスピード、敏捷さ、奪取力から、NFL (National Football League) の新しい選手全員のプロトタイプとなっています。そして、非常に優れた新しいランニング・バックとして LT 選手が選ばれると、コメンテーター達は次のように言います。

「LT は Emmitt のような足をしています。」
「彼はまるで Emmitt のように相手をかわしていくことができます。」
「それでも彼は 1 マイルを 5 分フラットで走ります。」

コメンテーター達は、プロトタイプ・オブジェクト (Emmitt Smith) を基に新しいオブジェクト (LT) をモデリングしています。JavaScript の場合、そうしたモデルはリスト 3 のようになります。

リスト 3. JavaScript のモデル
var emmitt = {
    // ... properties go here
};

var lt = Object.create(emmitt);
// ... add other properties directly to lt

この例を従来のモデリングと比較すると、従来のモデリングでは FootballPlayer クラスを継承する RunningBack クラスを定義します。lt と emmitt は RunningBack のインスタンスです。これらのクラスは Java クラスではリスト 4 のようになります。

リスト 4. 3 つの Java クラス
class FootballPlayer {
    private string name;
    private string team;

    static void FootballPlayer() { }

    string getName() {
        return this.name;
    }

    string getTeam() {
        return this.team;
    }

    void setName(string val) {
        this.name = val;
    }

    void setTeam(string val) {
        this.team = val;
    }
}

class RunningBack extends FootballPlayer {
    private bool offensiveTeam = true;

    bool isOffesiveTeam() {
        return this.offensiveTeam;
    }
}

RunningBack emmitt = new RunningBack();
RunningBack lt   = new RunningBack();

この従来モデルは概念的なオーバーヘッドをかなり多く伴いますが、プロトタイプ・モデルの場合のようにクラスのインスタンス (emmittlt) を詳細に制御することはできません (公平を期すために言えば、FootballPlayer クラスは 100% 必要なわけではなく、次の例との比較のために記載してあります)。このオーバーヘッドが役立つ場合もありますが、多くの場合は重荷にすぎません。

従来型のモデリングはプロトタイプ・ベースのオブジェクト・システムによって極めて簡単にエミュレートすることができます (プロトタイプ・モデリングを従来のオブジェクト・システムによってエミュレートすることも確かに可能ですが、おそらく容易ではないでしょう)。例えば、プロトタイプとして footballPlayer を継承する別の runningBack オブジェクトを使用して footballPlayer オブジェクトを作成することができます。JavaScript の場合、これらのオブジェクトはリスト 5 のようになります。

リスト 5. JavaScript のモデリング
var footballPlayer = {
    name : "";
    team : "";
};

var runningBack = Object.create(footballPlayer);
runningBack.offensiveTeam = true;

footballPlayer を継承する別の lineBacker オブジェクトを作成することもできます (リスト 6)。

リスト 6. オブジェクトの継承
var lineBacker = Object.create(footballPlayer);
lineBacker.defensiveTeam = true;

リスト 7 を見るとわかるように、footballPlayer オブジェクトに動作を追加することにより、lineBacker オブジェクトと runningBack オブジェクトの両方に動作を追加することができます。

リスト 7. 動作を追加する
footballPlayer.run = function () { this.running = true };
lineBacker.run();
lineBacker.running; // => true

この例では footballPlayer をクラスとして扱っています。Emmitt 用と LT 用のオブジェクトを作成することもできます (リスト 8)。

リスト 8. オブジェクトを作成する
var emmitt = Object.create(runningBack);
emmitt.superbowlRings = 3;

var lt = Object.create(emmitt);
lt.mileRun = '5min';

lt オブジェクトは emmitt オブジェクトを継承しているので、emmitt オブジェクトをクラスとして扱うこともできます (リスト 9)。

リスト 9. 継承とクラス
emmitt.height = "6ft";
lt.height // => "6ft";

従来の静的なオブジェクトを特徴とする言語 (Java コードなど) で上記の例を試したとすると、Decorator パターンを使用する必要があるため、さらに概念的なオーバーヘッドが必要となり、それでも emmitt オブジェクトをインスタンスとして直接継承することはできません。対照的に、JavaScript のようなプロトタイプ・ベースの言語のプロパティー・パターンを使用すると、はるかに自由な方法でオブジェクトを修飾することができます。


JavaScript は Java 言語ではありません

JavaScript とその機能の一部 (プロトタイプ・オブジェクトなど) は不幸な歴史的大失敗やマーケティング上の判断の犠牲となってきました。例えば、Brendan Eich 氏 (JavaScript の父) はブログ記事の中で、なぜ新しい言語が必要であったのかを次のように語っています。「技術の上層部からの絶対的命令により、その新しい言語は「Java と似ている」言語でなければなりませんでした。そのため、Perl、Python、Tcl は Scheme とともに排除されました。」つまり JavaScript は Java コードと似ており、JavaScript という名前自体も Java 言語と関係しているため、いずれか一方になじみがない人や、どちらもなじみがない人は混乱してしまいます。JavaScript は表面的には Java 言語と似ているように見えますが、深いレベルでは Java とはまったく似ておらず、似ていると思った人達の期待を裏切ります。Brendan Eich 氏は次のように語っています。

Scheme 風の第一級関数と (風変りながら) Self 風のプロトタイプを主な構成要素として選択したことは、大満足というわけではありませんが、良かったと思っています。Java の影響、特に 2000年問題の日付のバグや、プリミティブとオブジェクトの区別 (例えば string と String など) は不幸なことでした。

期待外れへの対応は難しいものです。Java 言語のように静的なエンタープライズ・レベルの言語を期待しているところに、構文は Java コードに似ていながらも動作はむしろ Scheme や Self に似ている言語であるとわかると、驚くのも当然です。動的言語を好む人にとっては、これは歓迎すべき驚きですが、動的言語を好まない人や動的言語にまったくなじみがない人にとっては、JavaScript によるプログラミングは不愉快かもしれません。

また、JavaScript には元々いくつか欠点があり、グローバル変数の強制、スコープの問題、セミコロンの挿入、== の振る舞いの一貫性欠如などの問題があります。これらの問題から、JavaScript プログラマー達は信頼性の高いソフトウェアの開発を支援するための一連のパターンとベスト・プラクティスを作成しました。次のセクションでは、JavaScript のプロトタイプ・ベースのオブジェクト・システムを最大限に活用するために、使用すべきパターンと避けるべきパターンをいくつか紹介します。


JavaScript のオブジェクト・パターン

JavaScript を Java コードのように見せようと試みる中で、JavaScript 設計者達はコンストラクター関数を JavaScript に含めました。従来の言語にはコンストラクター関数が必要ですが、プロトタイプ・ベースの言語では通常は不要なオーバーヘッドにすぎません。コンストラクター関数を使用してオブジェクトを宣言する以下のパターンについて考えてみましょう (リスト 10)。

リスト 10. オブジェクトを宣言する
function Point(x, y) {
    this.x = x;
    this.y = y;
}

すると、Java コードの場合と同じように、new キーワードを使用してオブジェクトを作成することができます (リスト 11)。

リスト 11. オブジェクトを作成する
var p = new Point(3, 4);
p.x // => 3
p.y // => 4

JavaScript では関数もオブジェクトです。そのため、コンストラクター関数のプロトタイプにメソッドを追加することもできます (リスト 12)。

リスト 12. メソッドを追加する
Point.prototype.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y));
};

コンストラクター関数だけではなく、従来風の継承パターンも使用することができます (リスト 13)。

リスト 13. 従来風の継承パターン
function Point3D(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

Point3D.prototype = new Point(); // inherits from Point

Point3D.prototype.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};

確かに、これは JavaScript でオブジェクトを定義するための有効な方法です (そして場合によると最適な方法かもしれません) が、少し不細工に思えます。この方法はプロトタイプ・パターンを生かして純粋にプロトタイプ・スタイルでオブジェクトを定義する方法に比べ、コードに不必要な要素が追加されています。つまり一言で言えば、JavaScript ではオブジェクト・リテラルを使用してオブジェクトを定義することができるのです (リスト 14)。

リスト 14. オブジェクトを定義する
var point = {
    x: 1,
    y: 2,
    r: function () {
        return Math.sqrt((this.x * this.x) + (this.y * this.y));
    }
};

すると、リスト 15 のように Object.create を使用して継承することができます。

リスト 15. Object.create を使用して継承する
var point3D = Object.create(point);
point3D.z = 3;
point3D.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};

この方法でオブジェクトを作成する方が JavaScript では自然であり、JavaScript のプロトタイプ・オブジェクトの長所を生かしています。ただしプロトタイプ・パターンと従来風のパターンに共通する短所として、メンバーがプライベートではありません。これが問題にならない場合もありますが、問題になる場合もあります。リスト 16 はプライベート・メンバーを持つオブジェクトを作成するパターンを示しています。Douglas Crockford 氏は彼の著書、『JavaScript: The Good Parts ― 「良いパーツ」によるベストプラクティス』の中で、これを関数型継承パターンと呼んでいます。

リスト 16. 関数型継承パターン
var point = function(spec) {
    var that = {};

    that.getTimesSet = function() {
        return timesSet;
    };

    that.getX = function() {
        return spec.x;
    };

    that.setX = function(val) {
        spec.x = val;
    };

    that.getY = function() {
        return spec.y;
    };

    that.setY = function(val) {
        spec.y = val;
    };

    return that;
};

var point3D = function(spec) {
    var that = point(spec);

    that.getZ = function() {
        return spec.z;
    };

    that.setZ = function(val) {
        spec.z = val;
    };

    return that;
};

コンストラクターを使用してオブジェクトを生成し、そのオブジェクトの中でプライベート・メンバーを定義し、そのコンストラクターに spec を渡すことによってインスタンスを作成します (リスト 17)。

リスト 17. インスタンスを作成する
var p = point({ x: 3, y: 4 });
p.getX();  // => 3
p.setX(5);

var p2 = point3D({ x: 1, y: 4, z: 2 });
p.getZ();  // => 2
p.setZ(3);

まとめ

この記事では、プロトタイプ・ベースのオブジェクト指向プログラミングの表面的なことを説明したにすぎません。Self、Lua、Io、REBOL など、他にも多くの言語がプロトタイプ・パターンを実装しています。静的な型付けの言語を含め、どの言語でもプロトタイプ・パターンを実装することができます。また、単純さと柔軟性が必要なシステムを設計する場合には、それがどのようなシステムであってもプロトタイプ・パターンを使用すると有効です。

プロトタイプ・ベースのオブジェクト指向プログラミングは非常に強力かつ単純であり、その明確さとスマートさでオブジェクト指向プログラミングの目標を満たします。プロトタイプ・ベースでオブジェクト指向プログラミングを行えることは JavaScript の資産の 1 つであり、欠点ではありません。

参考文献

学ぶために

製品や技術を入手するために

  • Self プログラミング言語: Self をダウンロードしてください。このダウンロードには、プログラミング言語、Self 言語で定義された一連のオブジェクト、Self プログラム作成のために Self に組み込まれたプログラミング環境が含まれています。
  • 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=823512
ArticleTitle=プロトタイプ・ベースのオブジェクト指向プログラミングを採り入れる
publish-date=07052012