はじめに
この記事では、ECMAScript でのオブジェクト指向プログラミングのさまざまな側面を検討します (ただし、このトピックは以前に多くの記事で議論されています)。これらの問題を理論的な観点からさらに見ていきます。 特に、オブジェクト作成アルゴリズム、オブジェクトの関係 (基本的な関係 - 継承を含む) について検討します。これは議論にも使用できます (これにより、JavaScript の OOP に関するこれまでの概念的な曖昧さが払拭されることを願っています)。
英語原文:http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general- Theory/
紹介、パラダイム、アイデア
ECMAScript で OOP の技術分析を行う前に、OOP の基本的な特性をいくつか理解し、導入部の主な概念を明確にする必要があります。
ECMAScript は、構造化、オブジェクト指向、関数型、命令型などのさまざまなプログラミング手法をサポートしています。場合によっては、アスペクト指向プログラミングもサポートしていますが、この記事ではオブジェクト指向プログラミングについて説明します。 ECMAScript での指向プログラミング 定義:
ECMAScript は、プロトタイプ実装に基づいたオブジェクト指向プログラミング言語です。
プロトタイプベースの OOP と静的クラスベースのアプローチの間には多くの違いがあります。 それらの違いを詳しく見てみましょう。
クラス属性とプロトタイプに基づく
前の文で重要な点が指摘されていることに注意してください - 完全に静的クラスに基づいています。 「静的」という言葉は、(必須ではありませんが) 厳密に型指定された静的オブジェクトと静的クラスを意味します。
この状況に関して、フォーラム上の多くの文書は、これが JavaScript での「クラスとプロトタイプ」の比較に反対する主な理由であることを強調していますが、実装は (動的クラスに基づくなど) Python と Ruby では異なります。焦点にはそれほど反対していません(いくつかの条件が書かれていますが、考え方には一定の違いがありますが、JavaScriptはそれほど代替的になっていません)、しかし、彼らの対立の焦点は静的クラスと動的プロトタイプです)、正確に言うと、メカニズムです静的クラス (例: C、JAVA) とその下位クラス、およびメソッド定義を使用すると、それとプロトタイプベースの実装との正確な違いを確認できます。
しかし、それらを 1 つずつリストしてみましょう。 これらのパラダイムの一般原則と主な概念を考えてみましょう。
静的クラスに基づく
クラスベースのモデルには、クラスとインスタンスの概念があります。 クラスのインスタンスには、オブジェクトまたはインスタンスという名前が付けられることもよくあります。
クラスとオブジェクト
クラスはインスタンス (つまり、オブジェクト) の抽象化を表します。この点では数学に少し似ていますが、これをタイプまたは分類と呼びます。
例 (ここと以下の例は疑似コードです):
階層継承
コードの再利用を改善するために、追加情報を追加して、クラスを別のクラスに拡張できます。 このメカニズムは (階層) 継承と呼ばれます。
クラスのインスタンスでメソッドを呼び出すときは、通常、ネイティブ クラスでメソッドを検索します。見つからない場合は、直接の親クラスに移動して検索します。検索する親クラス (たとえば、厳密な継承チェーン内) で、継承の先頭が見つかってもまだ見つからない場合、結果は次のようになります。オブジェクトには同様の動作がなく、結果を取得する方法がありません。
クラスに基づく主要な概念
したがって、次の重要な概念があります:1. オブジェクトを作成する前に、まずクラスを定義する必要があります。
2. したがって、オブジェクトは、独自の「図像と類似性」(構造と動作) に抽象化されたクラスから作成されます
3. メソッドは、厳密、直接、不変の継承チェーンを通じて処理されます
4. サブクラスには、継承チェーン内のすべての属性が含まれます (一部の属性がサブクラスで必要でない場合も含む)。
5. クラス インスタンスを作成します。クラスは (静的モデルのため) インスタンスの特性 (プロパティまたはメソッド) を変更できません。
6. インスタンスは (厳密な静的モデルのため)、インスタンスに対応するクラスで宣言されているもの以外の追加の動作や属性を持つことはできません。
JavaScript で OOP モデルを置き換える方法を見てみましょう。これは、プロトタイプ OOP に基づいて私たちが提案するものです。
ここでの基本概念は、動的に変更可能なオブジェクトです。変換 (値だけでなく属性も含む完全な変換) は動的言語に直接関係します。次のようなオブジェクトは、クラスを必要とせずに、すべてのプロパティ (プロパティ、メソッド) を独立して保存できます。
プロトタイプは、他のオブジェクトのプリミティブ コピーとして使用されるオブジェクトです。または、一部のオブジェクトが独自の必要なプロパティを持たない場合、プロトタイプはこれらのオブジェクトのデリゲートとして使用され、補助オブジェクトとして機能します。 。
委任ベース
オブジェクトは実行時にプロトタイプを動的に簡単に変更できるため、任意のオブジェクトを別のオブジェクトのプロトタイプ オブジェクトとして使用できます。
現在、特定の実装ではなく概要を検討していることに注意してください。ECMAScript での特定の実装について説明すると、それぞれの独自の特性がいくつかわかります。例 (疑似コード):
この例は、独自の属性を要求するのと同じように、プロトタイプの補助オブジェクト属性としての重要な機能とメカニズムを示しています。これらの属性は、デリゲート属性です。このメカニズムはデリゲートと呼ばれ、これに基づくプロトタイプ モデルはデリゲート プロトタイプ (またはデリゲート ベースのプロトタイプ) です。ここでの参照メカニズムは、オブジェクトへのメッセージの送信と呼ばれます。オブジェクトが応答を取得しない場合、オブジェクトはプロトタイプに委任されて、メッセージへの応答を試行します。
この場合のコードの再利用は、デリゲートベースの継承またはプロトタイプベースの継承と呼ばれます。任意のオブジェクトをプロトタイプとして使用できるため、プロトタイプは独自のプロトタイプを持つこともできます。 これらのプロトタイプは相互にリンクされて、いわゆるプロトタイプ チェーンを形成します。 チェーンも静的クラスと同様に階層的ですが、簡単に再配置して階層と構造を変更できます。
オブジェクトとそのプロトタイプ チェーンがメッセージ送信に応答できない場合、オブジェクトは対応するシステム信号をアクティブにすることができ、おそらくプロトタイプ チェーン上の他のデリゲートによって処理されます。
このシステム シグナルは、括弧で囲まれた動的クラスに基づくシステムを含む多くの実装で利用できます: Smalltalk の #doesNotUnderstand、Ruby の method_missing、Python の __getattr__、PHP の __call、ECMAScript の __noSuchMethod__ 実装など。
例 (SpiderMonkey の ECMAScript 実装):
言い換えると、静的クラスに基づく実装がメッセージに応答できない場合、現在のオブジェクトには必要な特性がないと結論付けられますが、プロトタイプ チェーンから取得しようとすると、それでも可能性があります。結果を取得するか、一連の変更後にオブジェクトがこの特性を保持します。
ECMAScript に関する具体的な実装は、デリゲートベースのプロトタイプを使用することです。 ただし、仕様と実装からわかるように、それらには独自の特性もあります。
連結モデル
正直に言うと、(ECMASCript で使用されなくなるとすぐに) 別の状況、つまりプロトタイプが他のオブジェクトからネイティブ オブジェクトを置き換える状況について何か言う必要があります。この場合のコードの再利用は、委任ではなく、オブジェクト作成段階でのオブジェクトの真のコピー (クローン) です。この種のプロトタイプは連結プロトタイプと呼ばれます。オブジェクトのすべてのプロトタイプ プロパティをコピーすると、そのプロパティとメソッドがさらに完全に変更される可能性があり、プロトタイプ自体も変更される可能性があります (デリゲート ベースのモデルでは、この変更は既存のオブジェクトの動作を変更しませんが、そのプロトタイプ プロパティを変更します)。 この方法の利点は、スケジュールと委任の時間を短縮できることですが、欠点はメモリ使用量が多いことです。
アヒルタイプ
弱い型を動的に変更するオブジェクトを返す。静的クラスに基づくモデルと比較して、これらのことができるかどうかのテストは、オブジェクトの型 (クラス) とは関係ありませんが、メッセージに応答できるかどうかをテストします。する能力が必須かどうかを確認した上で)。
例:
いわゆるDockタイプです。 つまり、チェック時に、オブジェクトの階層内での位置や特定のタイプに属しているかどうかではなく、オブジェクト自体の特性によってオブジェクトを識別できます。
プロトタイプに基づく主要なコンセプト
このアプローチの主な特徴を見てみましょう:
1. 基本的な概念はオブジェクトです
2. オブジェクトは完全に動的で可変です (理論的には、ある型から別の型に変換できます)
3. オブジェクトには、独自の構造と動作を記述する厳密なクラスがありません。オブジェクトにはクラスが必要ありません
。
4. オブジェクトにはクラスはありませんが、プロトタイプを持つことができます。メッセージに応答できない場合は、プロトタイプ
に委任できます。
5. オブジェクトのプロトタイプは実行中にいつでも変更できます。
6. デリゲートベースのモデルでは、プロトタイプの特性を変更すると、プロトタイプに関連するすべてのオブジェクトに影響します。
7. 連結プロトタイプモデルでは、プロトタイプは他のオブジェクトからクローンされたオリジナルのコピーであり、さらに完全に独立したコピーのオリジナルになります。プロトタイプの特性の変換は、それからクローンされたオブジェクトには影響しません
。
8. メッセージに応答できない場合、発信者は追加の措置を講じることができます (スケジュールの変更など)
9. オブジェクトの失敗は、オブジェクトのレベルや属するクラスによって決定されるのではなく、現在の特性によって決定されます
しかし、考慮すべき別のモデルもあります。
動的クラスに基づく
上記の例で示された「クラス VS プロトタイプ」の区別は、動的クラスに基づくこのモデルではそれほど重要ではないと考えています (特にプロトタイプ チェーンが不変の場合、より正確に区別するには、依然として必要です)静的クラスを考慮してください)。 例として、Python や Ruby (または他の同様の言語) を使用することもできます。 これらの言語はすべて、動的なクラスベースのパラダイムを使用しています。 ただし、いくつかの側面では、プロトタイプに基づいて実装された機能が確認できます。
次の例では、委任のみに基づいてクラス (プロトタイプ) を拡張できることがわかり、その結果、このクラスに関連するすべてのオブジェクトに影響を与えることもできます (新しいクラスを提供します)。デリゲートのオブジェクト) など。
Ruby での実装も同様です。完全に動的クラスも使用されます (ちなみに、Python の現在のバージョンでは、Ruby や ECMAScript とは異なり、クラス (プロトタイプ) の拡大は機能しません)。オブジェクトを完全に変更できます。ただし、オブジェクトのクラスを動的に変更することはできません。
ただし、この記事は Python と Ruby に特化したものではないため、これ以上は説明せず、ECMAScript 自体について引き続き説明します。
しかし、その前に、一部の OOP に含まれる「糖衣構文」についてもう一度見てみる必要があります。これは、JavaScript に関する以前の記事の多くでこれらの問題が取り上げられていることが多いためです。
このセクションで注意すべき唯一の間違った文は、「JavaScript はクラスではありません。クラスを置き換えることができるプロトタイプがあります。」です。 「JavaScript は異なる」と言っても、すべてのクラスベースの実装が完全に異なるわけではないことを認識することが重要です。(「クラス」の概念に加えて) 他にも関連する特性があることを考慮する必要もあります。 。
さまざまな OOP 実装のその他の機能
このセクションでは、ECMAScript での OOP 実装を含む、さまざまな OOP 実装でのコード再利用の他の機能と方法を簡単に紹介します。 その理由は、JavaScript での OOP の実装には習慣的な思考制限があるためです。唯一の主な要件は、それが技術的およびイデオロギー的に証明される必要があることです。他の OOP 実装で構文上のシュガー関数が発見されていないとは言えず、JavaScript は純粋な OOP 言語ではないと性急に考えましたが、これは間違いです。
ポリモーフィック
ECMAScript では、オブジェクトにはポリモーフィズムのいくつかの意味があります。
たとえば、ネイティブ オブジェクトのプロパティと同様に、関数をさまざまなオブジェクトに適用できます (値は実行コンテキストに入るときに決定されるため)。
関数を定義するときのいわゆるパラメーター多態性は、多態性パラメーター (配列とそのパラメーターの .sort ソート方法 - 多態性ソート関数など) を受け入れる点を除いて、すべてのデータ型と同等です。ちなみに、上記の例もパラメトリック多態性の一種と考えることができます。
プロトタイプ内のメソッドは空として定義でき、作成されたすべてのオブジェクトはこのメソッドを再定義 (実装) する必要があります (つまり、「1 つのインターフェイス (シグネチャ)、複数の実装」)。
ポリモーフィズムは、上で説明した Duck タイプに関連しています。つまり、階層内のオブジェクトのタイプと位置はそれほど重要ではありませんが、必要な特性がすべて揃っていれば、簡単に受け入れることができます (つまり、共通のインターフェイスが重要です) 、実装は多様になる可能性があります)。
カプセル化
カプセル化については誤解が多いです。このセクションでは、OOP 実装のいくつかの構文糖衣 (修飾子とも呼ばれます) について説明します。 この場合、OOP 実装で便利な「糖衣」について説明します。よく知られている修飾子: private、protected、public (オブジェクトのアクセスとも呼ばれます)レベルまたはアクセス修飾子)。
ここで、カプセル化の主な目的を思い出していただきたいと思います。カプセル化は抽象的な追加であり、クラスに直接何かを書き込む隠れた「悪意のあるハッカー」ではありません。
これは大きな間違いです。隠すために Hide を使用してください。
アクセス レベル (プライベート、プロテクト、パブリック) は、プログラミング (実際には非常に便利な構文糖) を容易にし、システムをより抽象的に記述および構築するために、多くのオブジェクト指向プログラムに実装されています。
これは一部の実装 (前述の Python や Ruby など) で見られます。一方で (Python では)、これらの __private_protected 属性 (アンダースコア規則に従って名前が付けられています) には外部からアクセスできません。 一方、Python は特別なルール (_ClassName__field_name) を使用して外部からアクセスできます。
Ruby では、プライベートな特性と保護された特性を定義する機能がある一方で、カプセル化されたデータを取得するための特別なメソッド (instance_variable_get、instance_variable_set、send など) もあります。
主な理由は、プログラマー自身がカプセル化された (特に「隠し」を使用していないことに注意してください) データを取得したいことです。 このデータが何らかの方法で誤って変更されたり、エラーが発生した場合、全責任はプログラマーにありますが、単なる「入力ミス」や「一部のフィールドの変更」だけではありません。 しかし、これが頻繁に発生する場合、それは非常に悪いプログラミング習慣およびスタイルです。通常、オブジェクトと「対話」するにはパブリック API を使用する価値があるからです。
繰り返しになりますが、カプセル化の基本的な目的は、補助データのユーザーを抽象化することであり、ハッカーによるデータの隠蔽を防ぐことではありません。 さらに深刻なことに、カプセル化では、ソフトウェアのセキュリティを実現するためにプライベートを使用してデータを変更することはありません。
補助オブジェクトをカプセル化します (部分的)。最小限のコスト、ローカリゼーション、および予測変更を使用して、パブリック インターフェイスの動作変更の実現可能性を提供します。これがカプセル化の目的です。
さらに、setter メソッドの重要な目的は、複雑な計算を抽象化することです。 たとえば、element.innerHTML セッター (抽象ステートメント) は、「この要素内の HTML は次のコンテンツです」と、innerHTML プロパティ内のセッター関数を計算して確認するのが困難になります。 この場合、問題は主に抽象化に関係しますが、カプセル化も発生します。
カプセル化の概念は OOP だけに関係するものではありません。 たとえば、さまざまな計算をカプセル化して抽象化するだけの単純な関数にすることもできます (たとえば、関数 Math.round(...) がどのように実装されているかをユーザーが知る必要はなく、ユーザーは単に呼び出すだけです)それ)。 これは一種のカプセル化です。「プライベート、保護、パブリック」とは言っていないことに注意してください。
ECMAScript 仕様の現在のバージョンでは、private、protected、および public 修飾子が定義されていません。
しかし、実際には「Mock JS Encapsulation」という名前のものを見ることができます。 一般に、このコンテキストは (原則として、コンストラクター自体) が使用されることを目的としています。 残念ながら、この「模倣」は頻繁に実装されており、プログラマーは擬似的に絶対に抽象ではないエンティティ設定「ゲッター/セッター メソッド」を生成できます (繰り返しますが、これは間違いです)。
つまり、オブジェクトが作成されるたびに getA/setA メソッドも作成され、これが (プロトタイプ定義と比較して) メモリが増加する理由でもあることを誰もが理解しています。 ただし、理論的には、最初のケースでもオブジェクトを最適化できます。
さらに、一部の JavaScript 記事では、「プライベート メソッド」の概念についてよく言及しています。 注: ECMA-262-3 標準では、「プライベート メソッド」の概念が定義されていません。
ただし、JS はイデオロギー的な言語であるため、場合によってはコンストラクター内で作成できます。オブジェクトは完全に変更可能であり、独自の特性を持っています (コンストラクター内の特定の条件下では、追加のメソッドを取得できるオブジェクトと取得できないオブジェクトがあります) )。
さらに、JavaScript でカプセル化が、悪意のあるハッカーが setter メソッドを使用する代わりに特定の値を自動的に書き込むのを防ぐものであると依然として誤解されている場合、いわゆる「隠し」および「プライベート」実際にはあまり「隠蔽」されておらず、一部の実装では、コンテキストを eval 関数に呼び出すことで、関連するスコープ チェーン (および対応するすべての変数オブジェクト) の値を取得できます (SpiderMonkey1.7 でテストできます)。
あるいは、実装ではアクティブなオブジェクト (Rhino など) に直接アクセスでき、オブジェクトの対応するプロパティにアクセスすることで内部変数の値を変更できます。
var _myPrivateData = 'testString';
これは実行コンテキストを括弧で囲むためによく使用されますが、実際の補助データの場合は、オブジェクトに直接関係せず、外部 API から抽象化するのに便利なだけです:
多重継承
多重継承は、コードの再利用を改善するための非常に便利な構文糖衣です (一度に 1 つのクラスを継承できるのに、なぜ一度に 10 クラスを継承できないのでしょうか?)。 ただし、多重継承にはいくつかの欠点があるため、実装では普及していません。
ECMAScript は多重継承をサポートしていません (つまり、直接プロトタイプとして使用できるオブジェクトは 1 つだけです)。ただし、その先祖のプログラミング言語にはそのような機能があります。 ただし、一部の実装 (SpiderMonkey など) では、プロトタイプ チェーンの代わりに __noSuchMethod__ を使用して、スケジューリングと委任を管理できます。
ミックスイン
ミックスインはコードを再利用する便利な方法です。ミックスインは多重継承の代替手段として提案されています。 これらの個々の要素は、任意のオブジェクトと混合して機能を拡張できます (したがって、オブジェクトを複数の Mixin と混合することもできます)。 ECMA-262-3 仕様では「Mixins」の概念は定義されていませんが、Mixins の定義によれば、ECMAScript には動的に変更可能なオブジェクトがあり、Mixins を使用して機能を単純に拡張することに障害はありません。
典型的な例:
ECMA-262-3 で言及されている引用符でこれらの定義 (「ミックスイン」、「ミックス」) を使用していることに注意してください。仕様にはそのような概念はなく、ミックスではなく、オブジェクトを拡張するために一般的に使用されます。新しい機能を搭載。 (Ruby のミックスインの概念は公式に定義されています。ミックスインは、モジュールのすべてのプロパティを別のモジュールに単純にコピーするのではなく、含まれるモジュールへの参照を作成します。実際には、デリゲートの追加オブジェクト (プロトタイプ) を作成します。)
特性
トレイトは概念としてはミックスインに似ていますが、多くの機能を備えています (定義上、ミックスインは適用できるため、名前の競合を引き起こす可能性があるため、状態を含めることはできません)。 ECMAScript によれば、トレイトとミックスインは同じ原則に従うため、仕様では「トレイト」の概念は定義されていません。
インターフェース
一部の OOP に実装されているインターフェイスは、ミックスインやトレイトに似ています。ただし、ミックスインやトレイトとは対照的に、インターフェイスは実装クラスにメソッド シグネチャの動作を強制的に実装します。インターフェースは完全に抽象クラスとみなすことができます。ただし、抽象クラス(抽象クラスのメソッドはメソッドの一部のみを実装でき、残りの部分はシグネチャとして定義されたまま)と比較すると、継承は単一の基本クラスしか継承できませんが、複数のインターフェイスを継承できます。インターフェイス (複数混合) は、多重継承の代替手段と見なすことができます。
ECMA-262-3 標準では、「インターフェイス」の概念も「抽象クラス」の概念も定義されていません。 ただし、模倣として、「空」メソッド (または、このメソッドを実装する必要があることを開発者に伝えるために空のメソッドにスローされた例外) を使用してオブジェクトを実装することは可能です。
オブジェクトの組み合わせ
オブジェクトの合成も動的コード再利用技術の 1 つです。 オブジェクトの構成は、動的に変更可能なデリゲートを実装するという点で、柔軟性の高い継承とは異なります。これも委託されたプロトタイプに基づいています。 動的に変更可能なプロトタイプに加えて、オブジェクトはデリゲート用にオブジェクトを集約し (結果として組み合わせ、つまり集約を作成)、さらにデリゲートに委任するオブジェクトにメッセージを送信できます。これは、動的性質により実行時に変更される可能性があるため、3 つ以上のデリゲートで実行できます。すでに述べた __noSuchMethod__ の例はこれを実行しますが、デリゲートを明示的に使用する方法も示しましょう:
例:
明示的な構成 (継承に比べて柔軟性) がないため、中間コードを追加しても問題ありません。
AOP の機能
アスペクト指向の関数として、関数デコレーターを使用できます。 ECMA-262-3 仕様では、(この用語が正式に定義されている Python とは対照的に) 「関数デコレータ」の概念が明確に定義されていません。 ただし、関数パラメータを持つ関数は、特定の側面で修飾およびアクティブ化できます (いわゆる提案を適用することで)。最も単純なデコレータの例:
結論
この記事では、OOP の導入について説明しました (この情報がお役に立てば幸いです)。次の章では、オブジェクト指向プログラミングのための ECMAScript の実装を続けます。