詳細 ECMA-262-3 第7章1節 OOP: 概説
おはようございます。「 Dmitry 先生、言いたい放題」のコーナーへようこそ。大形尚弘です。
今回は最長、そして ECMAScript をどっかに忘れた Dmitry 先生が、オブジェクト指向というものを一般より一段抽象化したレベルから語ります。
誰あろう私がそうなのですが、オブジェクト指向や、その他の何であれプログラミングパラダイムを学ぶとき、大抵そのパラダイムを代表する実装や言語をベースに学習します。すると、パラダイム自体への理解が、その実装の制約に縛られることが多々あると思います。
私は主に ActionScript2/3 でオブジェクト指向を学習しましたので、 JavaScript は「オブジェクト指向言語では無い」と思ってしまっていたことがあります。これはこの章をお読みいただければ分かるとおり、全くの間違いです。正確に表現するならば、オブジェクト指向のための「理論的糖衣」が AS3 と JavaScript で異なっている、というだけで、多くの概念は JavaScript においてもまた違った形で実装可能なのです。
とまあ、そんなことを、ごゆっくり、お時間のあるときに、腰を落ち着けて、ご理解いただければ幸いです。
詳細 ECMA-262-3 シリーズ
第7章1節 OOP: 概説
目次
はじめに
この章では、 ECMAScript におけるオブジェクト指向プログラミングの主要な側面について検討していきます。「もう一つのもの( yet another )」になるべくしたものではありません、このトピックは、既に多くの記事で取り扱われています。ここでは、オブジェクト指向にまつわるさまざまな項目について、その内部から、理論的な面により注意を向けていきます。特に、オブジェクト生成のアルゴリズムについて検討し、ごく基本的な関係である継承を始めとしたオブジェクト間の関係が構築される様子について理解を深め、こうした検討の中で用いられる概念に対し正確な定義を与えていきます(そうして、 JavaScript における OOP についてしばしば取り上げられる、用語上、理論上の懸念や困惑を晴らすことを望みます)。
一般的な規定、パラダイム、体系
ECMAScript における OOP の技術的な要素を分析していく前に、 OOP の一般的な特徴を挙げ、それらの概論について中心となる考え方を明らかにする必要があるでしょう。
ECMAScript はさまざまなプログラミングパラダイムをサポートする言語です。構造化、オブジェクト指向、関数型、命令型、そしてある場合にはアスペクト指向をもサポートしています。しかし、この章ではこの中で特に OOP について検討しますので、この特性に関して、 ECMAScript に定義を与えましょう。
ECMAScript は、プロトタイプベースの実装による、オブジェクト指向プログラミング言語です。
プロトタイプベースモデルの OOP は、静的クラスベースのパラダイムとは多くの点で異なります。それらの違いを、詳しく見ていくことにしましょう。
クラスベース及びプロトタイプベースモデルの特徴
さきほどの文中で、重要な点について触れました。静的クラスベースという言葉です。ここでは「静的」という言葉によって、静的なオブジェクトとクラス、そして原則として強い型付けを意識しています(最後の点については必須ではありませんが)。
しばしば多くの記事やフォーラム中の発言において、 JavaScript は「クラス対プロトタイプ」という視点において「もう一つの」「異なった」ものと呼ばれます。しかし、この差が認められるのはいくつかの実装(動的クラスベースの Python や Ruby )におけるものであって、本質的なものではありません(いくつかの条件を受け入れれば、理論的な特徴において差異はあるにせよ、 JavaScript はそれほど「異質な」ものではないのです)。私があえて「静的」という点を強調するのは、「静的 + クラス vs 動的 + プロトタイプ」という対置こそ、本質的なものであるからです。まさに、静的及びクラスの組み合わせ(例えば C++ や Java )とそれに関係するプロパティ・メソッド解決のメカニズムこそが、プロトタイプベースの実装との正確な差異を明らかにしてくれるのです。
とはいえ、一つ一つ見ていくことにしましょう。これらのパラダイムの一般的な理論と、主要な考え方について検討しましょう。
静的クラスベースモデル
クラスベースモデルには、クラスと、そのクラスという分類に属するインスタンスという考え方があります。あるクラスのインスタンスは、オブジェクトまたは exemplar (典型) と呼ばれることもあります。
クラスとオブジェクト
クラスは、一般化されたインスタンスの特性(オブジェクトに関する情報)の形式的で抽象的な集合を表すものです。
この場合の集合とは数学のそれに近い言葉ですが、それを型または分類と呼ぶこともできます。
例です(以下は擬似コードです)(訳注:擬似コードは背景色を変えてあります)
C = Class {a, b, c} // クラス C 、 a ・ b ・ c という特性を持つ
インスタンスの特性とは、プロパティ(オブジェクトの説明)及びメソッド(オブジェクトの活動)です。
それらの特性自身もオブジェクトとして扱うことが可能です。プロパティが書き換え可能なのか( writable )、設定可能なのか( configurable )、動作可能なのか( getter/setter )などといったものが挙げられます。
つまり、オブジェクトとは状態(クラスに記述された全てのプロパティの具体的な値)を保管するものであり、クラスは完全で不変な構造(あるプロパティの有無)と完全で不変なふるまい(あるメソッドの有無)を定義するものです。
C = Class {a, b, c, method1, method2} c1 = {a: 10, b: 20, c: 30} // クラス C のオブジェクト c1 c2 = {a: 50, b: 60, c: 70} // 同じくクラス C の、また別の状態を持ったオブジェクト c2
階層的な継承
コードの再利用性を高めるために、クラスは他のクラスを拡張( extend )し、必要な追加を行うことができます。このメカニズムは、(階層的な)継承と呼ばれます。
D = Class extends C = {d, e} // {a, b, c, d, e} d1 = {a: 10, b: 20, c: 30, d: 40, e: 50}
インスタンスからメソッドを呼び出す際のメソッドの解決は、そのメソッドの存在に関し、完全で不変で連続的なクラスへの検査によって行われます。ネイティブの(そのインスタンスが直接に属する)クラスにそのメソッドが見つからなかった場合は、そのクラスの親、さらにそのクラスの親の親へと、完全な階層的チェーンの中で探索が継続していきます。その継承チェーンの基となるクラスまでメソッドが解決されないままである場合、結論は、そのオブジェクトは(それ自身の特性の集合、及び階層的なチェーンの中に)求められたふるまいを持たず、望まれた結果を得ることは不可能であるということになります。
d1.method1() // D.method1 (無) -> C.method1 (有) d1.method5() // D.method5 (無) -> C.method5 (無) -> 結果無し
継承においてクラスの子孫へと複製されず、階層を形成するメソッドに対し、プロパティは常に複製されます。このふるまいは、その祖先がクラス C
であるクラス D
の例で見ることができます。プロパティ a
、 b
、 c
は複製され、 D
の構造は {a, b, c, d, e}
となります。しかし、 {method1, method2}
はコピーされず、継承されます。従って、この面におけるメモリ使用量は階層の深さに比例することになります。ここにおける基本的な欠点は、階層の深いレベルにおいてオブジェクトにとって必要の無いプロパティがあったとしても、ともかくそのオブジェクトはその不要なプロパティを持ってしまう、ということです。
クラスベースモデルの主要な考え方
以上の通り、クラスベースモデルの主要な考え方は次の通りとなります。
- オブジェクトを生成するためには、まず初めに、必ずそのクラスを定義しなければならない。
- 従って、オブジェクトはそれ自身の分類、「像と肖」(構造とふるまい)において生成される。
- メソッドの解決は、継承の完全で間に無関係なものを挟まない不変のチェーンにおいて行われる。
- クラスの子孫(そしてそこから生成されたオブジェクト)は、継承チェーンのプロパティ全てを持つ(たとえそのプロパティのいくつかが具体的に継承されたクラスにおいて必要なかったとしても)。
- 一度生成されてしまうと、クラスは(その静的なモデルに基づき)そのインスタンスの特性(プロパティ、メソッド)の集合を変更することはできない。
- インスタンスは(再度その静的なモデルに基づき)、追加の自分自身のための(固有な)ふるまいや、そのクラスの構造とふるまいとは異なる追加のプロパティを持つことはできない。
それでは次に、プロトタイプに基づいた別の OOP モデルがどのように提供されるのか、見てみることにしましょう。
プロトタイプベースモデル
プロトタイプベースモデルにおいて基本となる考え方は、動的で可変なオブジェクトです。
可変性(完全な交換可能性。値だけで無く全ての特性に関して)こそが、言語の動的性に直接に関係します。
このようなオブジェクトは独自に全ての特性(プロパティ、メソッド)を保管し、クラスを必要としません。
object = {a: 10, b: 20, c: 30, method: fn}; object.a; // 10 object.c; // 30 object.method();
さらに、その動的性に基づき、オブジェクトはその特性を簡単に変更(追加、削除、修正)することができます。
object.method5 = function () {...}; // 新しいメソッドを追加する object.d = 40; // 新しいプロパティ "d" を追加する delete object.c; // プロパティ "c" を削除する object.a = 100; // プロパティ "a" を修正する // 結果として object: {a: 100, b: 20, d: 40, method: fn, method5: fn};
すなわち、代入時にそのオブジェクト中に代入しようとする対象の特性が存在しなければ、その特性は新たに生成され渡された値によって初期化されます。存在した場合は、上書きされます。
この場合のコードの再利用性は、クラスを拡張することではなく(ここではそもそも、不変的な特性の集合としてのクラスに一切触れていません。クラスは一切存在しないのです)、いわゆるプロトタイプを参照することによって行われます。
プロトタイプは、他のオブジェクトの原本として使われたり、あるいはあるオブジェクトが必要とする特性をそれ自身に持っていなかった際に特性の委譲先として参照する、補助的なオブジェクトです。
委譲ベースモデル
どんなオブジェクトも、他のオブジェクトのプロトタイプとして使用されることができ、そしてまたその可変性に基づき、オブジェクトはそのプロトタイプすらランタイムに動的に変更することができます。
注意していただきたいのは、ここでは私たちは一般的な理論について検討しているのであり、具体的な実装には触れていません。具体的な実装、特に ECMAScript について検討する際には、それらの実装の多くの特徴について見ていくことになるでしょう。
例です(擬似コード)
x = {a: 10, b: 20}; y = {a: 40, c: 50}; y.Prototype = x; // x は y のプロトタイプ y.a; // 40 、 y 自身の特性 y.c; // 50 、これも y 自身の特性 y.b; // 20 - これはプロトタイプから得られたもの: y.b (無) -> y.Prototype.b (有): 20 delete y.a; // 自身の特性 "a" を削除する y.a; // 10 - プロトタイプから得られたもの z = {a: 100, e: 50} y.Prototype = z; // y のプロトタイプを z に変更した y.a; // 100 - プロトタイプから得られたもの y.e; // 50 - これもまたプロトタイプから得られたもの z.q = 200 // プロトタイプに新たなプロパティを追加する y.q // その変更結果は y からも利用可能
この例は、プロトタイプがある特性に関するヘルパーオブジェクトとして利用される際の、プロトタイプに関する重要な特徴とメカニズムを示しています。ある特性が対象のオブジェクトに存在しない場合は、他のオブジェクトがそれを代理できるのです。
このメカニズムは委譲と呼ばれ、またはそのプロトタイプ的モデルに基づき、委譲プロトタイピング(または委譲ベースのプロトタイピング)と呼ばれます。
この場合の特性への参照は、オブジェクトにメッセージを送ると表現され得ます。対象のオブジェクトが自身でそのメッセージに応答することができない場合、オブジェクトはそのプロトタイプに処理を委譲します(メッセージに応えることを試みるように依頼します)。
同様にこの場合のコード再利用性は、委譲ベースの継承、またはプロトタイプベースの継承と呼ばれます。
どんなオブジェクトもプロトタイプになることができるということは、プロトタイプもまた、それ自身のプロトタイプを持つことができるということを意味します。この連結されたプロトタイプの組み合わせが、いわゆるプロトタイプチェーンを形成します。このチェーンは静的クラスモデルにおける階層性に似ていますが、ここではその可変性によって、簡単に再配置し、階層や構造を変更することができるのです。
x = {a: 10} y = {b: 20} y.Prototype = x z = {c: 30} z.Prototype = y z.a // 10 // z.a はプロトタイプチェーン中に見つかる // z.a (無) -> // z.Prototype.a (無) -> // z.Prototype.Prototype.a (有): 10
もし対象のオブジェクト及びそのプロトタイプチェーンが送られたメッセージに応答できなかった場合、そのオブジェクトは対応するシステムシグナルを発生します。それにより、送出を継続することが可能か、他のチェーンに委譲することができるかを判断します。
このシステムシグナルは、動的クラスベースシステムを含め多くの実装で利用可能です。 SmallTalk では #doesNotUnderstand 、 Ruby では method_missing 、 Python では __getattr__ 、 PHP では __call 、 ECMAScript 実装の一つでは __noSuchMethod__ などです。
例です( SpiderMonkey ECMAScirpt 実装)。
var object = { // メッセージに応答できないことに関する // システムシグナルを捕獲 __noSuchMethod__: function (name, args) { alert([name, args]); if (name == 'test') { return '.test() method is handled'; } return delegate[name].apply(this, args); } }; var delegate = { square: function (a) { return a * a; } }; alert(object.square(10)); // 100 alert(object.test()); // .test() メソッドがうまく処理された
つまり、静的クラスベースの実装に対し、メッセージに応答不可能な場合の結論はこうです。その時点における対象のオブジェクトは、必要とされる特性を持っていない。しかし、代替のプロトタイプチェーンを分析したり、あるいはオブジェクトがいくつかの変更の後にその特性を持ちえる場合に、望まれた結果を得ることは依然として可能であるということなのです。
ECMAScript に関して言えば、まさにこの実装、委譲ベースのプロトタイピングが用いられています。しかし、後に見ていくように、仕様及び実装によってそれぞれ独自の特徴があります。
連結モデル
公正を期すために、別の( ECMAScript では使われていませんが)ケースについても定義から少々触れておかないとなりません。プロトタイプが、他のオブジェクトがそこから特性を複製する、原本としてふるまうというモデルです。
ここでのコードの再利用性は、委譲では無く、オブジェクトの生成時におけるプロトタイプの完全な複製(クローン)によるものとなります。
この種のプロトタイピングは、連結型プロトタイピングと呼ばれます。
プロトタイプの特性の全てを自身にコピーした後、オブジェクトは以降そのプロパティとメソッドを完全に変更することができ、これはプロトタイプもまた同様です(そしてこのプロトタイプへの変更は、既に存在するオブジェクトには影響しません。委譲ベースモデルにおいてプロトタイプの特性を変更した場合とは異なります)。このようなアプローチの利点は送出と委譲にかかる時間の削減であり、欠点はより高いメモリ使用量です。
ダックタイピング
静的クラスベースモデルと比較した場合の、動的で、弱い型付け、オブジェクトの可変性に話を戻すと、オブジェクトがある活動を取り得るかどうかの検査を通過するということは、オブジェクトがどんな型(クラス)に属しているかではなく、そのメッセージに応答することができるか(検査の後実際にオブジェクトが必要とされる活動ができるか)に関係します。
例
// 静的クラスベースモデルでは if (object instanceof SomeClass) { // ある活動が許可される } // 動的な実装では // その瞬間のオブジェクトの型は必須では無い // なぜなら可変性によって、型や特性は変換可能だからである // 繰り返しになるが、本質的なことは // オブジェクトが "test" メッセージに // 応答できるかどうかということである if (isFunction(object.test)) // ECMAScript if object.respond_to?(:test) // Ruby if hasattr(object, 'test'): // Python
プログラミング用語ではこれはダックタイピングと呼ばれるものです。つまりオブジェクトが、階層中のオブジェクトの位置やある具体的な型に属することではなく、検査のその瞬間のある特性の集合によって識別されるのです。
プロトタイプベースモデルの主要な考え方
このアプローチの主な特徴についてまとめてみましょう。
- 考え方の中心となるのは、オブジェクトである。
- オブジェクトは完全に動的で、可変である(そして理論的にはある型から他の型への変異も可能である)。
- オブジェクトはその構造とふるまいを記述する厳格なクラスを持たない。オブジェクトはクラスを必要としない。
- しかし、クラスは持たないが、オブジェクトは受け取ったメッセージに自身で応えることができなかった場合に委譲できる、プロトタイプを持っている。
- オブジェクトのプロトタイプは、ランタイムにいつでも変更することができる。
- 委譲ベースモデルでは、プロトタイプの特性への変更は、このプロトタイプに結びつく全てのオブジェクトに影響する。
- 連結型プロトタイプモデルでは、プロトタイプは原本であり、他のオブジェクトはそこから複製された後、完全に独立である。プロトタイプの特性への変更は、そこから複製されたオブジェクトには影響しない。
- もしメッセージに応答できなかった場合も、呼び出し元に対し他の対策(例えばメッセージの送信先を変更するなど)を採るようシグナルを送出することができる。
- オブジェクトの識別は、その階層や具体的な型への所属ではなく、その瞬間の特性の集合によって行われる。
まだ一方、もう一つ、考慮しなければならないモデルがあります。
動的クラスベースモデル
このモデルは、最初に触れた例、つまり「クラス vs プロトタイプ」の対置は本質的では無い(特にプロトタイプチェーンが不変である場合、もっと正確な区別では、クラスにおける静的性を検討する必要がある)ということを、例示するために検討していきます。例えば、 Python や Ruby (あるいはその他の同様の言語)を取り上げてみます。これらの言語は、共に動的クラスベースのパラダイムを採用しています。しかし、ある面では、プロトタイプベースの実装の特徴も、いくつかその中に見て取ることができます。
下記の例では、まさに委譲ベースのプロトタイピングのように観察することができます。クラス(プロトタイプ)を拡張し、その変更がこのクラスに結びつく全てのオブジェクトに影響します。さらにまた、動的に、ランタイムに、オブジェクトのクラスを変更すること(委譲のための新しいオブジェクトを与えることで)などが可能なのです。
# Python class A(object): def __init__(self, a): self.a = a def square(self): return self.a * self.a a = A(10) # インスタンスを生成 print(a.a) # 10 A.b = 20 # クラスの新しいプロパティ print(a.b) # 20 - インスタンス "a" からの「委譲」を通じて利用可能 a.b = 30 # 自身のプロパティを生成 print(a.b) # 30 del a.b # 自身のプロパティを削除 print(a.b) # 20 - 再度、クラス(プロトタイプ)から取られる # プロトタイプベースモデルと同様 # オブジェクトの「プロトタイプ」を # ランタイムに変更することができる class B(object): # 「空っぽの」クラス B pass b = B() # クラス B のインスタンス b.__class__ = A # クラス(プロトタイプ)を動的に変更する b.a = 10 # 新しいプロパティを生成 print(b.square()) # 100 - クラス A のメソッドを利用可能 # クラスへの明確な参照を削除することもできる del A del B # しかしオブジェクトは引き続き # 暗黙の参照を持っており、メソッドは依然利用可能 print(b.square()) # 100 # しかし、クラスをビルトインのものに # 変更することはできない(現在のバージョンでは) # これは実装の特徴である b.__class__ = dict # error
Ruby でも状況は似ています。完全に動的なクラスが使われ(ちなみに、現在のバージョンの Python では、 Ruby や ECMAScript と異なり、ビルトインのクラス・プロトタイプを拡張することはできません)、完全にオブジェクトやクラスの特性を変更できます(クラスにメソッドやプロパティを追加すると、これらの変更は既に存在するオブジェクトに影響します)。しかしながら、例えば Ruby では、オブジェクトのクラスを動的に変更することはできません。
とはいえ、このシリーズは Python や Ruby のためのものではありませんので、この比較はここまでとし、 ECMAScript そのものに議論に進んでいきたいと思います。
しかしその前に、さらに別の、ある OOP 実装では利用可能であるような「シンタックス及び理論のシュガー」について見てみなくてはなりません。これに関する疑問はしばしば JavaScript についての記事に現れてくるからです。
そしてこの項では特に、「 JavaScript は、クラスでは無くプロトタイプを持つ異なる言語だ」といったような言葉の誤りを示してきました。異なるクラスベース実装間でさえ、その実装において全てが完全に異なっているわけではないということを理解する必要があります。例えもし「 JavaScript は違う」と語らざるを得ないとしても、(「クラス」の考え方だけでなく)関係する全ての特徴について検討する必要があると思います。
さまざまな OOP 実装のその他の特徴
この項では、さまざまな OOP 実装で見られるその他の特徴やコード再利用の種類について、 ECMAScript での OOP 実装と比較しながら、簡単に見ていきます。この理由は、 JavaScript に関する記事の中で、本来さまざまな実装が可能であるにもかかわらず、 OOP の考え方がほんのいくつかの習慣的な実装に限られてしまっているためです。ただ一つ(主として)の要件は、技術的、そして理論的に証明され得るということだけのはずです。ある(習慣的な) OOP 実装による「シンタックスシュガー」に類似性を見いだせなければ、 JavaScript は短慮にも「純粋な OOP 言語では無い」と呼ばれてしまうことになります。これは誤った考えです。
ポリモーフィズム(多態性)
ECMAScript のオブジェクトは、いくつかの意味で多態的です。
まず例として、一つの関数を、まるでもともとそのオブジェクトの特性であったかのように、異なるオブジェクトに適用することができます(なぜなら this 値は実行コンテキストへの進入時に決定されるためです)。
function test() { alert([this.a, this.b]); } test.call({a: 10, b: 20}); // 10, 20 test.call({a: 100, b: 200}); // 100, 200 var a = 1; var b = 2; test(); // 1, 2
しかしながら、これには例外もあります。例えば Date.prototype.getTime() メソッドは、仕様によれば this 値は常に date オブジェクトで無ければなりません。そうでは無い場合は例外が発生します。
alert(Date.prototype.getTime.call(new Date())); // time alert(Date.prototype.getTime.call(new String(''))); // TypeError
次に、関数が全てのデータ型に対して等しく定義される、いわゆるパラメトリック多態は、多態的な関数型の引数を受け入れます(例えば配列の .sort メソッドの引数は、多態的なソート関数です)。ちなみに、二つ前の例もまた、パラメトリック多態の一種であると見なすことができます。
そしてまた、プロトタイプにおいてメソッドを空のまま定義し、生成される全てのオブジェクトにおいてそのメソッドを再定義(実装)することもできます(つまり、「一つのインタフェース(シグネチャ)に対したくさんの実装」を実現できます)。
多態性とはまた、前述したダックタイピングと関係づけられます。つまりオブジェクトの型と階層における位置は重要では無く、必要な特性を持っていさえすれば簡単に受け入れられるのです(つまり繰り返しになりますが、外観的なインタフェースのみが重要で、実装は異なって構わないということです)。
カプセル化
この概念は、しばしば受け取り方において混乱や誤りが見受けられるものの一つです。ここでは、いくつかの OOP 実装における便利な「シュガー(糖衣性)」の一つ、よく知られる修飾子である、オブジェクトの特性に対するアクセスレベル(またはアクセス修飾子)、すなわち private 、 protected 、そして public といったものついて検討します。
私がここで強調しておきたいのは、カプセル化の本来の目的は、抽象性を高めることであり、「あなたのクラスのフィールドに直接何かを書き込もうとする悪意のあるハッカー」から偏執的にフィールドを隠そうとすることでは無い、ということです。
隠す目的のために隠すというのは、大きな(そして蔓延した)誤解です。
アクセスレベル( private 、 protected 、 public )は、より抽象的に記述し、より抽象的なシステムを構築するためのプログラマの利便性のために、いくつかの OOP 実装で導入されてきました(そして十分に便利な「シュガー(糖衣性)」を持っています)。
この特徴は、いくつかの実装(既に触れた Python や Ruby )に見ることができます。( Python の場合)まずある一方では __private や _protected なプロパティがあり(先頭のアンダースコアによる命名の運用によって特定されます)、これらは外側からアクセスすることはできません。もう一方では、 Python はこのようなフィールドの別名を持っており( _ClassName__field_name )、この名前によって、 外側からアクセスすることができます。
class A(object): def __init__(self): self.public = 10 self.__private = 20 def get_private(self): return self.__private # 外側 a = A() # A のインスタンス print(a.public) # OK 、 30 print(a.get_private()) # OK、 20 print(a.__private) # 失敗、 A の記述の内部からのみ利用可能 # しかし Python はこうしたプロパティの別名を持つ # _ClassName__property_name # この名前によって、これらのプロパティは # クラスの外側においても利用可能 print(a._A__private) # OK 、 20
一方 Ruby においては、 private や protected な特性を定義する仕組みがあります。そして同時に、特別なメソッド( instance_variable_get と instance_variable_set 、 send など)がカプセル化されたデータへのアクセスを可能にします。
class A def initialize @a = 10 end def public_method private_method(20) end private def private_method(b) return @a + b end end a = A.new # 新しいインスタンス a.public_method # OK 、 30 a.a # 失敗、 @a はゲッタ "a" を持たないプライベートなインスタンス変数 # 失敗 # "private_method" はプライベートで、 # クラス A の定義中からのみ利用可能 a.private_method # エラー # しかし特別なメタメソッドを使えば # それらカプセル化されたデータにアクセス可能 a.send(:private_method, 20) # OK 、 30 a.instance_variable_get(:@a) # OK 、 10
この主な動機は、プログラマ自身が、カプセル化された(私が注意して「隠蔽された」という言葉を避けていることにご注意ください)データにアクセスしたいのだということです。もしこれらのデータが不正に変更されたり、エラーが発生したら、その全ての責任は全くプログラマ自身にあります。これは単なる「タイポ」や「誰かが不用意にフィールドを変更してしまった」といったものとは性質が違います。しかし、もしこのようなケースが頻繁に起こるとすれば、これは悪いプログラミング習慣、悪いプログラミングスタイルと言わざるを得ないでしょう。一般的には、オブジェクトとは public な API を通じて「話し合う」ことが最善であるからです。
何度も繰り返しますが、カプセル化の基本的な目的は、補助的なヘルパーデータのユーザからの抽象化であって、「ハッカーからオブジェクトを守る」ことではありません。ソフトウェアのセキュリティや安全のためには、「 private 」修飾子などではなく、よりもっと深刻な対策が必要です。
補助的なヘルパー(ローカル)オブジェクトをカプセル化することで私たちは、未来にわたるパブリックインタフェースのふるまいの変更を最小のコストで実現し、これらの変更の箇所を予測し、局所化するのです。そしてこれこそが、カプセル化の本当の目的なのです。
setter メソッドの重要な目的もまた、難しい計算を抽象化することにあります。例えば、 element.innerHTML setter は、 innerHTML のための setter 関数の内部で難しい計算と検査が行われている一方、「それではこの要素の html は次の通りです」というように抽象化して表現できてしまいます。ここでは 抽象化 についてお話ししましたが、カプセル化が、その向上のために役立っているのです。
カプセル化の考え方というものは、特に OOP に限ったものではありません。例えば、さまざまな計算をカプセル化し、抽象性を利用できるようにする関数というものも考えられます(例えばユーザにとって、 Math.round がどのように実装されているかということはさほど重要ではありません。ユーザは単にこれを呼び出しさせすれば良いのです)。これがカプセル化というものであり、「 private 、 protected 、 public 」といった言葉は特に必要としないのです(訳注:それらはカプセル化という考え方に対する「アイデアシュガー」だということだと思います)。
現行のバージョンの ECMAScript では、 private 、 protected 、 public といった修飾子は定義されていません。
しかし実践においては、「 JS におけるカプセル化の模倣」と呼べるようなことが可能です。大抵はこの目的のために、取り囲むコンテキスト(概してコンストラクタ関数そのもの)が利用されます。不幸にも、しばしばこうした「模倣」を実装するにあたって、プログラマは全く抽象的でない要素に対し、(盲目的に)「 getter / setter 」を定義することができてしまいます(再び、これはカプセル化の誤った取り扱い方です)。
function A() { var _a; // "private" a this.getA = function _getA() { return _a; }; this.setA = function _setA(a) { _a = a; }; } var a = new A(); a.setA(10); alert(a._a); // undefined 、「プライベート」 alert(a.getA()); // 10
ここでお気づきの通り、生成されたオブジェクト一つ一つに対し、 "getA/setA" のメソッドのペアが生成され、生成されるオブジェクトの数に直接に比例するメモリ使用量の問題につながります(メソッドがプロトタイプにて定義できていればそういうことは無いのですが)。とはいえ、このケースでは、理論的には結合オブジェクト(邦訳)による最適化が可能ではあります。
また、さまざまな記事中においてこうしたメソッドを「特権メソッド」と呼ぶことがありますが、正確には、 ECMA-262-3 仕様には「特権メソッド」という考え方は存在していません。
しかし、コンストラクタ関数の中でオブジェクトのメソッドを生成すること自体は、いたって通常のことですし、それは言語の思想にも現れています。オブジェクトは完全に可変であり、それぞれ独自の特性を持つことができるのです(コンストラクタ中で、条件によってあるオブジェクトには追加のメソッドを持たせ、あるメソッドにはそれを持たせないということが可能です)。
さらに JavaScript に関して言えば、このような「隠蔽された」、「プライベートな」変数はさほど隠蔽されている訳ではありません(カプセル化が依然誤解されていて、 setter メソッドを使わずあるフィールドに直接値を書き込もうとする「悪意のあるハッカー」から守るものだと捉えられているとするなら、という意味においてです)。いくつかの実装においては、呼び出し元コンテキストを eval 関数に渡すことによって、必要なスコープチェーン(およびその中の全ての変数オブジェクト)にアクセスすることができます(バージョン1.7までの SpiderMonkey でテストしています)。
eval('_a = 100', a.getA); // あるいは a.setA 。どちらにせよ "_a" が Scope 中に存在する a.getA(); // 100
さらにまた、アクティベーションオブジェクトへの直接参照が可能な実装(例えば Rhino )においては、アクティベーションオブジェクトの対応するプロパティにアクセスすることで、内部変数の値を変更することが可能です。
// Rhino var foo = (function () { var x = 10; // 「プライベート」 return function () { print(x); }; })(); foo(); // 10 foo.__parent__.x = 20; foo(); // 20
時折、組織的な手法として(これもまたカプセル化の一種として)、 JavaScript 中の「 private 」及び「 protected 」データをアンダースコアから始めるような運用を採る場合もあります(ただしこれは Python とは違い、ただの命名のための慣習でしかありません)。
var _myPrivateData = 'testString';
取り囲む実行コンテキストの効果に関して言えば、特に本当に補助的な、オブジェクトの直接関係しないような(ヘルパー)データのカプセル化のために、外部 API からそれらを抽象化する目的で、これは実に頻繁に利用されます。
(function () { // コンテキストを初期化 })();
多重継承
多重継承は、コードの再利用を向上する上で便利な「シュガー(糖衣)」です(一つのクラスを継承するなら、どうして10個のクラスを一度に継承しないのでしょう?)。とはいえ、多重継承には数多くの問題があり、そのために実装系ではあまり一般的ではありません。
ECMAScript は多重継承をサポートしていません(つまり、ただ一つのオブジェクトのみが、直接のプロトタイプになり得るということです)。しかし、 ECMAScript の祖先である Self プログラミング言語ではそれが可能です。ただし実装によっては、例えば SpiderMonkey では、 __noSuchMethod__ を利用して、送出を管理し、代替のプロトタイプチェーンに委譲することができます。
mixin
mixin もまた、コード再利用に関する便利な手法です。 mixin は、多重継承の代替手法として提案されてきました。どんなオブジェクトにもミックスできる独立の要素があり、それによってオブジェクトの機能を拡張するのです(従ってオブジェクトはいくつもの mixin をミックスできます)。 ECMA-262-3 仕様では「 mixin 」 の考え方は定義されていませんが、 mixin そのものの定義について考えてみれば、 ECMAScript は動的で可変のオブジェクトを持ちますから、あるオブジェクトを他のオブジェクトにミックスすることを妨げるものは何もありません。単純に、その特性を拡張できるのです。
古典的な例です。
// 拡張のためのヘルパー Object.extend = function (destination, source) { for (property in source) if (source.hasOwnProperty(property)) { destination[property] = source[property]; } return destination; }; var X = {a: 10, b: 20}; var Y = {c: 30, d: 40}; Object.extend(X, Y); // Y を X にミックス alert([X.a, X.b, X.c, X.d]); 10, 20, 30, 40
ご注意いただきたいのは、私はこれらの定義(「 mixin 」「ミックスする」)を括弧付き又は斜体で示しました。これは、 ECMA-262-3 では、このような考え方が定義されているわけではなく、「ミックスする」というよりはオブジェクトを新しい特性で拡張していると捉えられるものだからです(一方、例えば Ruby では、 mixin の考え方は公式に採用されていて、あるモジュールのプロパティを単に全て別のオブジェクトに複製するのではなく、 mixin は盛り込まれるモジュールに対して参照を生成します。つまり、実際のところ委譲のための追加のオブジェクト(「プロトタイプ」)を作ることなのです)。
trait
trait は mixin に似ていますが、多くの機能を持っています(例えば基本的なものをあげると、定義によれば、 mixin では可能な、名前の衝突する状態を持つことができません)。 ECMAScript に関して言えば、 trait は mixin と同様の原理によって模倣することができます。標準仕様は「 trait 」の考え方を採用はしていません(訳注:実際に JavaScript で trait を実現する traits.js というライブラリが存在します)。
インタフェース
いくつかの OOP 実装で利用可能なインタフェースも、 mixin や trait と同様です。しかしそれらとは異なり、インタフェースはクラスに対し、あるシグネチャのメソッドのふるまいを完全に実装することを要求します。
インタフェースは、全くもって抽象クラスとして扱うことができますが、抽象クラス(一部のメソッドを実装し、残りのパートをシグネチャとしてのみ定義できる)と異なる点は、一段階の継承において、クラスがいくつものインタフェースを実装することができる点です。この点において、インタフェースは mixin 同様、多重継承の代替として用いられることができます。
ECMA-262-3 標準では「インタフェース」も「抽象クラス」の考え方も定義されてはいません。しかし模倣として、「空の」メソッドを持ったオブジェクトによって、他のオブジェクトを拡張することができます(あるいは、メソッド中で例外を投げ、このメソッドが実装されるべきであるということを警告することができます)。
オブジェクトコンポジション
オブジェクトコンポジションもまた、動的なコード再利用のためのテクニックの一つです。オブジェクトコンポジションが継承と異なるのは、高い柔軟性と動的に可変な委譲の構造を実装できることです。これはつまるところ、委譲ベースプロトタイピングの基礎です。動的に可変なプロトタイプの代わりに、オブジェクトが委譲のためのオブジェクトを集約し(集約される結果として、コンポジションを生成します)、以降オブジェクトにあるメッセージが送出される時に、オブジェクトはこのメッセージを集約した委譲先に委譲します。委譲先は一つに限りませんし、さらに動的な性質によってランタイムにそれらを変更することすら可能です。
既に触れた __noSuchMethod__ もこの例の一つなのですが、明確に委譲を使用する例を別に挙げてみましょう。
例です。
var _delegate = { foo: function () { alert('_delegate.foo'); } }; var agregate = { delegate: _delegate, foo: function () { return this.delegate.foo.call(this); } }; agregate.foo(); // foo を委譲する agregate.delegate = { foo: function () { alert('foo from new delegate'); } }; agregate.foo(); // 新しい委譲先の foo
このようなオブジェクトの関係を、「 has-a 」と呼び、継承による「 is-a 」関係が「子孫である」のに対し、「包括している」ということになります。
(継承と比較して柔軟性があることに対し)明確なコンポジションの欠点は、中間(媒介)コードの量が大きくなりがちだということです。
AOP
アスペクト指向プログラミングの特徴の一つとして、関数デコレータが挙げられるでしょう。 ECMA-262-3 仕様では「関数デコレータ」の考え方について明確な定義はありませんが(対して Python では公式な用語です)、関数型の引数を持つことから、関数はデコレートされることができ、(いわゆるアドバイスを採用することによって)あるアスペクトに関して実行されることができるのです。
簡単なデコレータの例を見てみましょう。
function checkDecorator(originalFunction) { return function () { if (fooBar != 'test') { alert('wrong parameter'); return false; } return originalFunction(); }; } function test() { alert('test function'); } var testWithCheck = checkDecorator(test); var fooBar = false; test(); // 'test function' testWithCheck(); // 'wrong parameter' fooBar = 'test'; test(); // 'test function' testWithCheck(); // 'test function'
結論
ここまでで、 OOP に関する概説を終えたいと思います(この資料がみなさまにとって有益であることを望みます)。そして、『第7章2節 OOP: ECMAScript での実装』へと続きます。
参考文献
- Using Prototypical Objects to Implement Shared Behavior in Object Oriented Systems (by Henry Lieberman)
- Prototype-based programming(日本語)
- Class(日本語)
- Object-oriented programming(日本語)
- Abstraction(日本語)
- Encapsulation
- Polymorphism(日本語)
- Inheritance(日本語)
- Multiple inheritance
- Mixin(日本語)
- Trait
- Interface(日本語)
- Abstract class(日本語)
- Object composition(日本語)
- Aspect-oriented programming(日本語)
- Dynamic programming language(日本語)
英語版翻訳: Dmitry A. Soshnikov 、 Juriy "kangax" Zaytsev の助力を借りて[英語版].
英語版公開日時: 2010-03-04
オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
オリジナルロシア語版公開日時: 2009-09-12
本シリーズはすべて英語版からの訳出です。