web-dev-qa-db-ja.com

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

JavaScriptはプロトタイプベースのオブジェクト指向言語ですが、次のいずれかの方法でクラスベースになることができます。

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

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

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

これまでに収集したのは、私が読んだほとんどのOOの原則とパターン(SOLIDやGoFパターンなど)は、クラスベースのOO言語用に作成されたということですSmalltalkやC++と同じように。しかし、Javascriptなどのプロトタイプベースの言語に適用できるものはありますか?

JavaScriptだけに固有の原則やパターンはありますか?回避すべき原則callback hellevil eval、またはその他のアンチパターンなど.

81
Spoike

多くの編集を経て、この答えは長いモンスターになりました。あらかじめお詫び申し上げます。

まず最初に、eval()は必ずしも悪いとは限りません。たとえば、遅延評価で使用すると、パフォーマンスが向上する可能性があります。遅延評価は遅延読み込みと似ていますが、基本的にはコードを文字列内に格納し、evalまたは_new Function_を使用してコードを評価します。いくつかのトリックを使用すると、悪よりもはるかに便利になりますが、使用しないと、悪いことになる可能性があります。このパターンを使用する私のモジュールシステムを見ることができます: https://github.com/TheHydroImpulse/resolve.js 。 Resolve.jsは、_new Function_の代わりにevalを使用して、主に各モジュールで使用可能なCommonJS exportsおよびmodule変数をモデル化し、_new Function_は、コードを匿名関数内にラップします。しかし、私は各モジュールを関数でラップすることになりますが、evalと組み合わせて手動で行います。

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

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

これで、ジェネレーターがフラグ(_--harmony_または_--harmony-generators_)の下でV8に、つまりNode.jsに最終的に追加されました。これらはあなたが持っているコールバック地獄の量を大幅に減らします。それは非同期コードを書くことを本当に素晴らしいものにします。

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

要約/概要:

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

例:

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

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

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

nextdoneを返すまで、trueを呼び出し続けます。これは、ジェネレーターが実行を完全に終了し、yieldステートメントがなくなったことを意味します。

制御フロー:

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

例:

_var co = require('co');

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

これにより、すべてをNode(および FacebookのRegenerator を使用したブラウザ)で書き込む可能性が可能になります。コード)と同期スタイル。

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


私の元の答えに追加するには:

私は最近、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に対するまったく異なるアプローチであり、私はJavaScriptの大ファンではありませんが、多くのフレームワーク/プラットフォームが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
  })
});
_

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

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

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は似ています。言語が統一されているため、pub/subパターンはNode.jsアプリケーションで本当に優れていますが、どの言語でも機能します。リアルタイムアプリケーション、ゲームなどで非常にうまく機能します。

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

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

観察者

一部の人々はオブザーバーパターンをpub/subと考えることを選択しているため、これは主観的なものである可能性がありますが、違いがあります。

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

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

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

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

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

基本的に、それは一連の同期データ(それが変数、関数など)を持っています。

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

 a = 2;

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

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

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

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

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

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

流星の...

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

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

後の2つのフレームワークは、特にテンプレートで反応パターンを使用します(つまり、自動更新)。 Angular.jsは単純なダーティーチェック手法を使用します。私はこれを厳密にリアクティブなプログラミングとは呼びませんが、ダーティーチェックはリアルタイムではないため、近いです。 Ember.jsは別のアプローチを使用します。 Ember set()およびget()メソッドを使用すると、依存する値をすぐに更新できます。runloopを使用すると、非常に効率的で、依存する値を増やすことができます。 angularには理論的な制限があります。

約束

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

_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に関する非常に優れたブログ投稿と、状況に応じて機能するかなり厳密で高度なパターンを作成しました。私が強調するいくつかの良い記事:

制御の反転

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

制御の反転の2つの主なサブバージョンは、依存性注入とサービスロケーターです。依存性注入とは対照的に、Service LocatorはJavaScript内で最も簡単であると思います。どうして?主な理由は、JavaScriptが動的言語であり、静的型付けが存在しないためです。 JavaやC#などは、型を検出でき、インターフェイスやクラスなどが組み込まれているため、依存性注入で「知られています」。これにより、かなり簡単になります。ただし、JavaScript内でこの機能を再作成することはできますが、同じであるとは言えず、ややハックなので、システム内でサービスロケーターを使用することを好みます。

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

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

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

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

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

従来のプロトタイプのインターフェースを使用している場合、MVCで作業するときに、手動での作業を行わない限り、構文糖やNice 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のポートで機能します です。

追加リソース:

116
Daniel

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

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

このアイデアは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パターンの文字列キーなど)とは対照的であり、特に動的キーが使用され、メンテナンスプログラマーがそれを認識しなかった場合(デッド)は、コード内にとどまる可能性が高くなります。メンテナンスプログラミングのコードは厄介な問題です)。

ゲームプログラミングでは、これによりye olde更新ループパターンの必要性が減り、イベント/リアクティブプログラミングイディオムにさらに変更されます。サブジェクトは、更新ループの実行を待たずに、変更のすべてのオブザーバーを自動的に更新します。更新ループの使用方法(ゲームの経過時間と同期する必要があるもの)がありますが、コンポーネント自体がこのパターンで自動更新できるときに、混乱を避けたい場合があります。

オブザーバブル関数の実際の実装は、驚くほど簡単に記述して理解することができます(特に、JavaScriptで配列を処理する方法と observer pattern を知っている場合)。

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を自由に試してください。

3
Spoike