Javascriptに実際に適用可能なオブジェクト指向の原則はありますか?


79

Javascriptはプロトタイプベースのオブジェクト指向言語ですが、次のいずれかの方法により、さまざまな方法でクラスベースになります。

  • 自分でクラスとして使用する関数を書く
  • フレームワークで気の利いたクラスシステムを使用する(mootools Class.Classなど)
  • Coffeescriptから生成する

最初はJavascriptでクラスベースのコードを書く傾向があり、それに大きく依存していました。しかし最近、私はJavascriptフレームワークとNodeJSを使用していますが、これらはクラスの概念から離れ、次のようなコードの動的な性質により依存しています。

  • 非同期プログラミング、コールバック/イベントを使用するコードの作成と使用
  • RequireJSを使用したモジュールのロード(グローバルネームスペースにリークしないようにするため)
  • リスト内包表記(マップ、フィルターなど)などの関数型プログラミングの概念
  • とりわけ

これまでに収集したことは、私が読んだほとんどのオブジェクト指向の原則とパターン(SOLIDやGoFパターンなど)は、SmalltalkやC ++のようなクラスベースのオブジェクト指向言語向けに書かれたものです。しかし、Javascriptなどのプロトタイプベースの言語に適用可能なものはありますか?

Javascriptに固有の原則やパターンはありますか?コールバックの地獄悪の評価、またはその他のアンチパターンなどを避けるための原則

回答:


116

多くの編集を経て、この答えは長さの怪物になりました。事前におaび申し上げます。

まず第一に、それeval()は常に悪いわけではなく、たとえば、遅延評価で使用するとパフォーマンスにメリットがあります。レイジー評価は遅延ロードに似ていますが、基本的に、文字列の中にあなたのコードを保存した後、使用しevalたりnew Function、コードを評価します。いくつかのトリックを使用すると、悪よりもはるかに便利になりますが、使用しないと、悪いことにつながる可能性があります。:あなたは、このパターンを使用して、私のモジュールシステムで見ることができhttps://github.com/TheHydroImpulse/resolve.js。Resolve.jsはnew Function主に各モジュールで使用可能なCommonJS exportsmodule変数をモデル化するためにevalを使用new Functionし、匿名関数内にコードをラップしますが、evalと組み合わせて手動で各モジュールをラップすることになります。

詳細については、次の2つの記事でお読みください。後の記事では、最初の記事も参照しています。

ハーモニージェネレーター

ジェネレーターがようやくV8に到着し、Node.jsにフラグ(--harmonyまたは--harmony-generators)が追加されました。これらは、あなたが持っているコールバック地獄の量を大きく減らします。非同期コードの作成が本当に素晴らしいものになります。

ジェネレータを利用する最良の方法は、ある種の制御フローライブラリを使用することです。これにより、ジェネレーター内で譲歩する際にフローを続行できます。

要約/概要:

ジェネレーターに慣れていない場合、ジェネレーターと呼ばれる特別な機能の実行を一時停止する習慣があります。この方法は、キーワード使用したyield譲歩と呼ばれます。

例:

function* someGenerator() {
  yield []; // Pause the function and pass an empty array.
}

したがって、この関数を初めて呼び出すと、新しいジェネレーターインスタンスが返されます。これによりnext()、そのオブジェクトを呼び出してジェネレーターを開始または再開できます。

var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }

戻るnextまで電話をかけ続けdoneますtrue。これは、ジェネレーターの実行が完全に終了し、それ以上yieldステートメントがないことを意味します。

制御フロー:

ご覧のとおり、ジェネレーターの制御は自動ではありません。それぞれを手動で続行する必要があります。そのため、coのような制御フローライブラリが使用されます。

例:

var co = require('co');

co(function*() {
  yield query();
  yield query2();
  yield query3();
  render();
});

これにより、同期スタイルでNode(およびハーモニージェネレーターを利用するソースコードを入力として受け取り、完全に互換性のあるES5コードを分割するFacebookのRegeneratorを備えたブラウザー)にすべてを書き込むことができます。

ジェネレーターはまだかなり新しいため、Node.js> = v11.2が必要です。私がこれを書いているとき、v0.11.xはまだ不安定であり、したがって多くのネイティブモジュールが壊れており、ネイティブAPIが落ち着くv0.12までになります。


元の回答に追加するには:

私は最近、JavaScriptでより機能的なAPIを好んでいます。この規則では、必要に応じて舞台裏でOOPを使用しますが、すべてを簡素化します。

たとえば、ビューシステム(クライアントまたはサーバー)を取り上げます。

view('home.welcome');

読むことや従うことははるかに簡単です:

var views = {};
views['home.welcome'] = new View('home.welcome');

このview関数は、ローカルマップに同じビューが既に存在するかどうかを確認するだけです。ビューが存在しない場合は、新しいビューを作成し、新しいエントリをマップに追加します。

function view(name) {
  if (!name) // Throw an error

  if (view.views[name]) return view.views[name];

  return view.views[name] = new View({
    name: name
  });
}

// Local Map
view.views = {};

非常に基本的なものですか?パブリックインターフェイスが劇的に簡素化され、使いやすくなりました。チェーンアビリティも採用しています...

view('home.welcome')
   .child('menus')
   .child('auth')

私が開発しているフレームワーク(他の誰かと)または次のバージョン(0.5.0)を開発しているTowerは、ほとんどのインターフェイスを公開する際にこの機能的アプローチを使用します。

一部の人々は、「コールバック地獄」を回避する方法として繊維を利用しています。JavaScriptへのアプローチはまったく異なり、私は大ファンではありませんが、多くのフレームワーク/プラットフォームで使用されています。Node.jsをスレッド/接続ごとのプラットフォームとして扱うため、Meteorを含みます。

コールバック地獄を避けるために、抽象メソッドを使用したいです。面倒になるかもしれませんが、実際のアプリケーションコードは大幅に簡素化されます。TowerJSフレームワークの構築を支援することで、多くの問題が解決しましたが、明らかにある程度のコールバックが残っていることは明らかですが、ネストは深くありません。

// app/config/server/routes.js
App.Router = Tower.Router.extend({
  root: Tower.Route.extend({
    route: '/',
    enter: function(context, next) {
      context.postsController.page(1).all(function(error, posts) {
        context.bootstrapData = {posts: posts};
        next();
      });
    },
    action: function(context, next) {
      context.response.render('index', context);
      next();
    },
    postRoutes: App.PostRoutes
  })
});

現在開発中のルーティングシステムと「コントローラー」の例ですが、従来の「レールのような」ものとはかなり異なります。しかし、この例は非常に強力であり、コールバックの量を最小限に抑え、物事をかなり明確にします。

このアプローチの問題は、すべてが抽象化されていることです。そのまま実行されるものはなく、その背後に「フレームワーク」が必要です。しかし、これらの種類の機能とコーディングスタイルがフレームワーク内に実装されている場合、それは大きな勝利です。

JavaScriptのパターンについては、正直に依存しています。継承は、CoffeeScript、Ember、または任意の「クラス」フレームワーク/インフラストラクチャを使用する場合にのみ本当に役立ちます。「純粋な」JavaScript環境にいるとき、従来のプロトタイプインターフェイスを使用することは魅力的です。

function Controller() {
    this.resource = get('resource');
}

Controller.prototype.index = function(req, res, next) {
    next();
};

Ember.jsは、少なくとも私にとっては、オブジェクトを構築するための異なるアプローチを使用して開始しました。各プロトタイプメソッドを個別に構築する代わりに、モジュールのようなインターフェイスを使用します。

Ember.Controller.extend({
   index: function() {
      this.hello = 123;
   },
   constructor: function() {
      console.log(123);
   }
});

これらはすべて異なる「コーディング」スタイルですが、コードベースに追加します。

多型

ポリモーフィズムは、純粋なJavaScriptでは広く使用されていません。継承では、「クラス」のようなモデルをコピーするには多くの定型コードが必要です。

イベント/コンポーネントベースの設計

イベントベースおよびコンポーネントベースのモデルはIMOの勝者、または特にEventEmitterコンポーネントが組み込まれているNode.jsを使用する場合に最も簡単に使用できますが、そのようなエミッターの実装は簡単ですが、それは素晴らしい追加です。

event.on("update", function(){
    this.component.ship.velocity = 0;
    event.emit("change.ship.velocity");
});

ほんの一例ですが、使用するのに適したモデルです。特にゲーム/コンポーネント指向のプロジェクトで。

コンポーネント設計はそれ自体が独立した概念ですが、イベントシステムとの組み合わせで非常にうまく機能すると思います。ゲームは従来、コンポーネント指向設計で知られていますが、オブジェクト指向プログラミングではこれまでのところしかありません。

コンポーネントベースの設計には用途があります。建物のシステムの種類によって異なります。Webアプリでも動作すると確信していますが、ゲーム環境ではオブジェクトの数や個別のシステムのために非常にうまく動作しますが、他の例も確かに存在します。

パブ/サブパターン

イベントバインディングとpub / subは似ています。pub / subパターンは、統一言語のためNode.jsアプリケーションで本当に輝いていますが、どの言語でも機能します。リアルタイムアプリケーション、ゲームなどで非常にうまく機能します。

model.subscribe("message", function(event){
    console.log(event.params.message);
});

model.publish("message", {message: "Hello, World"});

観察者

Observerパターンをpub / subとして考えることを選択する人もいますが、これには主観的なものがありますが、違いがあります。

「オブザーバーは、オブジェクト(サブジェクトと呼ばれる)がそれに依存するオブジェクトのリスト(オブザーバー)を維持し、状態の変化を自動的に通知する設計パターンです。」- オブザーバーパターン

オブザーバーパターンは、一般的なpub / subシステムを超えるステップです。オブジェクトには、互いに厳密な関係または通信方法があります。オブジェクト「Subject」は、依存関係のリスト「Observers」を保持します。サブジェクトはオブザーバーを最新の状態に保ちます。

リアクティブプログラミング

リアクティブプログラミングは、特にJavaScriptにおいて、より小さく、より未知の概念です。この「リアクティブプログラミング」を使用するための簡単なAPIを公開するフレームワーク/ライブラリ(私が知っている)が1つあります。

リアクティブプログラミングに関するリソース:

基本的に、同期データのセット(変数、関数など)があります。

 var a = 1;
 var b = 2;
 var c = a + b;

 a = 2;

 console.log(c); // should output 4

リアクティブプログラミングは、特に命令型言語ではかなり隠されていると思います。特にNode.jsでは、驚くほど強力なプログラミングパラダイムです。Meteorは、フレームワークの基本となる独自のリアクティブエンジンを作成しました。Meteorの反応性は舞台裏でどのように機能しますか?内部での動作の概要です。

Meteor.autosubscribe(function() {
   console.log("Hello " + Session.get("name"));
});

これは正常に実行され、の値を表示しますがname、変更すると

Session.set( 'name'、 'Bob');

console.logを再出力して表示しHello Bobます。基本的な例ですが、この手法をリアルタイムのデータモデルとトランザクションに適用できます。このプロトコルの背後にある非常に強力なシステムを作成できます。

流星の...

リアクティブパターンとオブザーバーパターンは非常に似ています。主な違いは、オブザーバーパターンは一般にオブジェクト/クラス全体のデータフローを記述するのに対して、リアクティブプログラミングは特定のプロパティへのデータフローを記述することです。

Meteorはリアクティブプログラミングの優れた例です。JavaScriptにはネイティブの値変更イベントがないため、ランタイムは少し複雑です(ハーモニープロキシはそれを変更します)。他のクライアント側フレームワークであるEmber.jsおよびAngularJSも、リアクティブプログラミングを(ある程度拡張して)利用しています。

後の2つのフレームワークは、テンプレートで最も反応的なパターンを使用します(つまり、自動更新です)。Angular.jsは、単純なダーティチェック手法を使用します。これをリアクティブプログラミングとは呼びませんが、ダーティチェックはリアルタイムではないため、これは近いものです。Ember.jsは異なるアプローチを使用します。Emberの使用set()と、get()依存する値を即座に更新できる方法。runloopを使用すると、非常に効率的で、角度に理論的な制限がある場合により多くの依存値を使用できます。

約束

コールバックの修正ではありませんが、インデントを外し、ネストされた関数を最小限に抑えます。また、問題に素晴らしい構文を追加します。

fs.open("fs-promise.js", process.O_RDONLY).then(function(fd){
  return fs.read(fd, 4096);
}).then(function(args){
  util.puts(args[0]); // print the contents of the file
});

インラインでないようにコールバック関数を広げることもできますが、それは別の設計上の決定です。

別のアプローチは、イベントを適切にディスパッチする関数がある場所にイベントとプロミスを結合し、実際の機能関数(内部に実際のロジックを持つ関数)が特定のイベントにバインドすることです。その後、各コールバック位置内にディスパッチャーメソッドを渡しますが、パラメーター、ディスパッチする関数を知るなど、思い浮かぶいくつかのねじれを解決する必要があります...

単機能機能

膨大な数のコールバック地獄を抱える代わりに、単一の機能を単一のタスクに保持し、そのタスクを適切に実行します。場合によっては、自分より先に進んで各機能に機能を追加することもできますが、自問してみてください関数に名前を付けると、インデントがクリーンアップされ、その結果、コールバック地獄の問題がクリーンアップされます。

最終的には、小さな「フレームワーク」を開発または使用することをお勧めします。基本的にはアプリケーションのバックボーンであり、抽象化、イベントベースのシステムの決定、または「独立した」システム。私はコードが特にコールバック地獄で非常に乱雑であるが、コーディングを開始する前に考えが欠けていたいくつかのNode.jsプロジェクトで働いてきました。APIと構文の観点からさまざまな可能性を考えてください。

Ben Nadelが、JavaScriptに関する非常に優れたブログ投稿と、状況に応じて機能する可能性のある非常に厳格で高度なパターンを作成しました。私が強調するいくつかの良い投稿:

制御の反転

コールバック地獄と正確に関連しているわけではありませんが、特に単体テストでは、全体的なアーキテクチャに役立ちます。

Inversion-of-Controlの2つの主要なサブバージョンは、Dependency InjectionとService Locatorです。Dependency Injectionとは対照的に、Service LocatorはJavaScript内で最も簡単だと思います。どうして?主にJavaScriptは動的言語であり、静的型付けは存在しないためです。とりわけJavaとC#は、型を検出できるため、依存性注入で「既知」であり、インターフェースやクラスなどが組み込まれています。これにより、作業が非常に簡単になります。ただし、JavaScript内でこの機能を再作成することはできますが、同一で少しハックすることはありません。システム内でサービスロケーターを使用することをお勧めします。

あらゆる種類の制御の反転は、コードを劇的に分離して、いつでもモックまたは偽造できる個別のモジュールに分離します。レンダリングエンジンの2番目のバージョンを設計しましたか?素晴らしい、新しいインターフェースを古いインターフェースに置き換えてください。サービスロケーターは新しいHarmonyプロキシで特に興味深いものですが、Node.js内でのみ有効に使用でき、Service.get('render');and を使用するよりも優れたAPIを提供しますService.render。現在、そのようなシステムに取り組んでいます:https : //github.com/TheHydroImpulse/Ettore

静的型付けの欠如(静的型付けは、Java、C#、PHPでの依存性注入で効果的に使用される可能性のある理由です-静的型付けではありませんが、型ヒントがあります。)間違いなくそれを強みに変えてください。すべてが動的であるため、「偽の」静的システムを設計できます。サービスロケーターと組み合わせて、各コンポーネント/モジュール/クラス/インスタンスをタイプに関連付けることができます。

var Service, componentA;

function Manager() {
  this.instances = {};
}

Manager.prototype.get = function(name) {
  return this.instances[name];
};

Manager.prototype.set = function(name, value) {
  this.instances[name] = value;
};

Service = new Manager();
componentA = {
  type: "ship",
  value: new Ship()
};

Service.set('componentA', componentA);

// DI
function World(ship) {
  if (ship === Service.matchType('ship', ship))
    this.ship = new ship();
  else
    throw Error("Wrong type passed.");
}

// Use Case:
var worldInstance = new World(Service.get('componentA'));

単純化した例。現実の世界で効果的に使用するには、この概念をさらに進める必要がありますが、従来の依存関係の注入が本当に必要な場合は、システムを切り離すのに役立ちます。この概念に少し手を加える必要があるかもしれません。前の例についてはあまり考えていません。

モデルビューコントローラー

最も明らかなパターンであり、Webで最も使用されているパターン。数年前、JQueryが大流行し、JQueryプラグインが誕生しました。クライアント側に完全なフレームワークは必要なく、jqueryといくつかのプラグインを使用するだけです。

現在、クライアント側のJavaScriptフレームワークの大規模な戦争があります。そのほとんどはMVCパターンを使用しており、すべて異なる方法で使用しています。MVCは常に同じように実装されるわけではありません。

従来のプロトタイプインターフェイスを使用している場合は、MVCを使用する際に、手動の作業を行わない限り、構文糖または素敵なAPIを取得するのに苦労するかもしれません。Ember.jsは、「クラス」/オブジェクトシステムを作成することでこれを解決します。コントローラーは次のようになります。

 var Controller = Ember.Controller.extend({
      index: function() {
        // Do something....
      }
 });

ほとんどのクライアント側ライブラリは、ビューヘルパー(ビューになる)とテンプレート(ビューになる)を導入することでMVCパターンを拡張します。


新しいJavaScript機能:

これは、Node.jsを使用している場合にのみ効果がありますが、それでも非常に貴重です。Brendan EichによるNodeConfでのこの講演は、いくつかのクールな新機能をもたらします。提案された関数構文、特にTask.js jsライブラリ。

これにより、おそらく関数のネストに関する問題のほとんどが修正され、関数のオーバーヘッドがないため、パフォーマンスがわずかに向上します。

V8がこれをネイティブにサポートしているかどうかはあまりわかりませんが、最後にいくつかのフラグを有効にする必要があることを確認しましたが、これはSpiderMonkeyを使用するNode.jsのポートで動作します。

追加リソース:


2
素晴らしい記事。私は個人的にMVを使用していませんか?ライブラリ。より大規模で複雑なアプリのコードを整理するために必要なものはすべて揃っています。彼らはすべて、サーバーとクライアントの通信で実際に起こっていることについて、独自のさまざまながらくたを投げかけようとしているJavaとC#を思い起こさせます。DOMを取得しました。イベントの委任を受けました。OOPができました。自分のイベントをデータ変更tyvmにバインドできます。
エリックReppen

2
「膨大な数のコールバック地獄の代わりに、単一の機能を単一のタスクに維持し、そのタスクをうまく実行します。」-詩。
CuriousWebDeveloper 14

1
Javascriptは、2000年代初期から中期に非常に暗い時代を経て、Javascriptを使用して大規模なアプリケーションを作成する方法をほとんど理解していなかったとき。@ErikReppenが言っているように、JSアプリケーションがJavaまたはC#アプリケーションのように見える場合、それは間違っています。
バックパック

3

ダニエルズの回答に追加:

観測可能な値/コンポーネント

このアイデアはMVVMフレームワークKnockout.JSko.observable)から借用されており、値とオブジェクトは観察可能なサブジェクトになり、1つの値またはオブジェクトに変更が発生すると、すべてのオブザーバーが自動的に更新されます。これは基本的にJavascriptで実装されたオブザーバーパターンであり、ほとんどのpub / subフレームワークの実装方法ではなく、「キー」は任意のオブジェクトではなくサブジェクトそのものです。

使用方法は次のとおりです。

// the subjects
// plain old javascript object with observable values
var shipComponent = {
    velocity : observable(0)
};

// the observer, a player user interface
// implemented with revealing module pattern
var playerUi = (function(ship) {

  var module = {
    setVelocity: function (x) { 
      // ... sets the velocity on the player user interface
    },

    // only called once
    init: function() {

      // subscribe to changes on the velocity value
      // using the module's function as callback
      module.velocity.onChange(playerUi.setVelocity);
    }
  };

  return module;
})(shipComponent).init();

// the player ui will change when the velocity value is changed
shipComponent.velocity.set(10);

アイデアは、オブザーバーは通常、サブジェクトがどこにあり、サブスクライブする方法を知っているということです。pub / subの代わりのこの利点は、リファクタリングのステップとしてサブジェクトを簡単に削除できるため、コードを大幅に変更する必要がある場合に顕著です。これは、対象を削除すると、その対象に依存していたすべてのユーザーが失敗するためです。コードがすぐに失敗する場合、残りの参照を削除する場所がわかります。これは、完全に分離されたサブジェクト(pub / subパターンの文字列キーなど)とは対照的であり、特に動的キーが使用され、メンテナンスプログラマーがそれを認識しなかった場合(死んでいる場合)メンテナンスプログラミングのコードは迷惑な問題です)。

ゲームプログラミングでは、これにより、古い更新ループパターンなどのイベント/リアクティブプログラミングイディオムへの必要性が減ります。これは、何かが変更されるとすぐに、更新ループを待たずに対象がすべてのオブザーバーを自動的に更新するためです実行する。更新ループには(ゲームの経過時間と同期する必要があるもののために)用途がありますが、コンポーネント自体がこのパターンで自動更新できる場合、それを煩雑にしたくない場合があります。

observable関数の実際の実装は、実際には驚くほど簡単に記述して理解することができます(特に、javascriptおよびobserverパターンで配列を処理する方法を知っている場合)。

var observable = function(v) {
    var val = v, subscribers = [];

    // the observable object,
    // as revealing module
    var output = {

        // subscribes to event
        onChange : function(func) {
            // idiomatic JS to add object to the
            // subscribers array
            subscribers.push(func);

            return output: // enables chaining
        },

        // the method that changes the observable object
        // and emits the event
        set : function(v) {
            var i;
            val = v;
            for (i = 0, i < subscribers.length; i++) {
                // this is hardly fault tolerant but as long
                // as subscribers are functions it'll work
                subscribers[i](v);
            }

            return output;
        }

    };

    return output;
};

JsFiddleオブザーバブルオブジェクトの実装を作成ました。これによりコンポーネントの監視とサブスクライバーの削除が可能になります。JsFiddleを自由に試してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.