web-dev-qa-db-ja.com

AngularJS:設計パターンを理解する

この投稿 のコンテキストで、AngularJSのリーダーであるIgor Minarによる:

MVC vs MVVM vs MVP多くの開発者が議論や議論に何時間も費やすことができるなんて物議をかもすトピックです。

数年間、AngularJSはMVC(またはクライアント側の亜種の1つ)に近づいていましたが、時間の経過とともに、多くのリファクタリングとAPIの改善のおかげで、MVVM$ scopeオブジェクトはViewModelこれは、Controllerと呼ぶ関数によって装飾されています。

フレームワークを分類し、MV *バケットの1つに入れることができることには、いくつかの利点があります。フレームワークで構築されているアプリケーションを表すメンタルモデルを簡単に作成できるようにすることで、開発者がAPIに慣れるのに役立ちます。また、開発者が使用する用語を確立するのにも役立ちます。

そうは言っても、開発者がMV *ナンセンスについて議論する時間を無駄にするのではなく、適切に設計され、懸念の分離に従うキックアスアプリを構築することを望みます。そしてこのため、私はここでAngularJSMVWフレームワーク-Model-View-Whatever。 Whateverは「whatever works for you」の略です。

Angularは、プレゼンテーションロジックをビジネスロジックやプレゼンテーション状態からうまく分離するための柔軟性を提供します。結局のところ、それほど重要ではないことについての激しい議論ではなく、生産性とアプリケーションの保守性を高めるために使用してください。

クライアント側のアプリケーションにAngularJS MVW(Model-View-Whatever)デザインパターンを実装するための推奨事項やガイドラインはありますか?

147
Artem Platonov

膨大な貴重な情報源のおかげで、AngularJSアプリでコンポーネントを実装するための一般的な推奨事項があります。


コントローラ

  • コントローラーは、モデルとビューの間の単なるinterlayerでなければなりません。できるだけthinとしてください。

  • コントローラでビジネスロジックを避けるにすることを強くお勧めします。モデルに移動する必要があります。

  • コントローラーは、メソッド呼び出し(子が親と通信したい場合に可能)または$ emit$ broadcastおよび$ onメソッド。送信およびブロードキャストされるメッセージは最小限に抑える必要があります。

  • コントローラーはプレゼンテーションを気にしないまたはDOM操作を行う必要があります。

  • ネストされたコントローラーを避けるを試してください。この場合、親コントローラーはモデルとして解釈されます。代わりにモデルを共有サービスとして注入します。

  • Scopeコントローラのbindingを使用する必要があります
    カプセル化モデルの表示プレゼンテーションモデルデザインパターン。


範囲

スコープをテンプレートでは読み取り専用およびコントローラーでは書き込み専用として扱います。スコープの目的は、モデルではなくモデルを参照することです。

双方向バインディング(ng-model)を行うときは、スコーププロパティに直接バインドしないようにしてください。


モデル

AngularJSのモデルはsingletonで定義されていますservice

モデルは、データと表示を分離する優れた方法を提供します。

モデルは、通常、厳密に1つの依存関係(一般的には$ rootScope)の1つの依存関係を持ち、高度にテスト可能なdomainロジック

  • モデルは特定のユニットの実装と見なされる必要があります。これは、単一責任の原則に基づいています。ユニットは、実世界では単一のエンティティを表し、プログラミング世界ではdata and stateの観点で説明する関連ロジックの独自のスコープを担当するインスタンスです。

  • モデルはアプリケーションのデータをカプセル化し、そのデータにアクセスして操作するためにAPIを提供する必要があります。

  • モデルはportableにして、同様のアプリケーションに簡単に転送できるようにする必要があります。

  • モデル内のユニットロジックを分離することにより、検索、更新、および保守が容易になりました。

  • モデルは、アプリケーション全体に共通のより一般的なグローバルモデルのメソッドを使用できます。

  • コンポーネントの結合を減らし、ユニットtestabilityおよびsabilityを増やすことが実際に依存していない場合、依存性注入を使用して他のモデルがモデルに組み込まれないようにしてください。

  • モデルでイベントリスナーを使用しないようにしてください。テストが難しくなり、通常、単一の責任原則の観点からモデルが殺されます。

モデルの実装

モデルはデータと状態の観点からいくつかのロジックをカプセル化する必要があるため、そのメンバーへのアクセスをアーキテクチャ的に制限する必要があり、疎結合を保証できます。

AngularJSアプリケーションでこれを行う方法は、factoryサービスタイプを使用して定義することです。これにより、プライベートプロパティとメソッドを非常に簡単に定義できます。また、公開されているアクセス可能なプロパティとメソッドを1か所で返すことができ、開発者にとって本当に読みやすくなります。

angular.module('search')
.factory( 'searchModel', ['searchResource', function (searchResource) {

  var itemsPerPage = 10,
  currentPage = 1,
  totalPages = 0,
  allLoaded = false,
  searchQuery;

  function init(params) {
    itemsPerPage = params.itemsPerPage || itemsPerPage;
    searchQuery = params.substring || searchQuery;
  }

  function findItems(page, queryParams) {
    searchQuery = queryParams.substring || searchQuery;

    return searchResource.fetch(searchQuery, page, itemsPerPage).then( function (results) {
      totalPages = results.totalPages;
      currentPage = results.currentPage;
      allLoaded = totalPages <= currentPage;

      return results.list
    });
  }

  function findNext() {
    return findItems(currentPage + 1);
  }

  function isAllLoaded() {
    return allLoaded;
  }

  // return public model API  
  return {
    /**
     * @param {Object} params
     */
    init: init,

    /**
     * @param {Number} page
     * @param {Object} queryParams
     * @return {Object} promise
     */
    find: findItems,

    /**
     * @return {Boolean}
     */
    allLoaded: isAllLoaded,

    /**
     * @return {Object} promise
     */
    findNext: findNext
  };
});

新しいインスタンスを作成する

新しい依存関数を返すファクトリーを使用しないようにしてください。これにより、依存関係の注入が壊れ始め、特にサードパーティの場合、ライブラリが扱いにくくなります。

同じことを実現するためのより良い方法は、ファクトリをAPIとして使用して、オブジェクトのコレクションにgetterメソッドとsetterメソッドを付加して返すことです。

angular.module('car')
 .factory( 'carModel', ['carResource', function (carResource) {

  function Car(data) {
    angular.extend(this, data);
  }

  Car.prototype = {
    save: function () {
      // TODO: strip irrelevant fields
      var carData = //...
      return carResource.save(carData);
    }
  };

  function getCarById ( id ) {
    return carResource.getById(id).then(function (data) {
      return new Car(data);
    });
  }

  // the public API
  return {
    // ...
    findById: getCarById
    // ...
  };
});

グローバルモデル

一般に、このような状況を回避し、モデルを適切に設計して、コントローラーに注入してビューで使用できるようにしてください。

特定の場合、一部のメソッドはアプリケーション内でグローバルなアクセシビリティを必要とします。それを可能にするために、common 'プロパティを$ rootScopeで定義し、commonModelにバインドできます。 アプリケーションのブートストラップ中:

angular.module('app', ['app.common'])
.config(...)
.run(['$rootScope', 'commonModel', function ($rootScope, commonModel) {
  $rootScope.common = 'commonModel';
}]);

すべてのグローバルメソッドは、「common」プロパティ内に存在します。これは、ある種のnamespaceです。

ただし、$ rootScopeでメソッドを直接定義しないでください。これは、ビュースコープ内でngModelディレクティブと一緒に使用すると 予期しない動作 につながる可能性があり、通常スコープを散らかし、スコープメソッドが問題をオーバーライドすることになります。


リソース

リソースを使用すると、さまざまなデータソースと対話できます。

single-responsibility-principleを使用して実装する必要があります。

特定の場合、それはreusable HTTP/JSONエンドポイントへのプロキシです。

リソースはモデルに注入され、データを送信/取得する可能性を提供します。

リソースの実装

RESTfulサーバー側データソースとやり取りできるリソースオブジェクトを作成するファクトリ。

返されるリソースオブジェクトには、低レベルの$ httpサービスと対話する必要なく高レベルの動作を提供するアクションメソッドがあります。


サービス

モデルとリソースの両方がサービス

サービスは関連付けられていない、疎結合自己完結型の機能の単位。

サービスは、Angularがサーバー側からクライアント側のWebアプリにもたらす機能であり、サービスは長い間一般的に使用されてきました。

Angularアプリのサービスは、依存性注入を使用して相互に接続された代替可能なオブジェクトです。

Angularにはさまざまなタイプのサービスが付属しています。それぞれに独自のユースケースがあります。詳細については、 サービスタイプの理解 を参照してください。

アプリケーションで サービスアーキテクチャの主な原則 を検討してください。

一般的には Webサービス用語集

サービスは、プロバイダエンティティとリクエスタエンティティの観点から一貫した機能を形成するタスクを実行する機能を表す抽象リソースです。使用するには、具体的なプロバイダーエージェントによってサービスを実現する必要があります。


クライアント側の構造

一般に、アプリケーションのクライアント側はmodulesに分割されます。各モジュールは、ユニットとしてtestableである必要があります。

タイプではなくfeature/functionalityまたはviewに応じてモジュールを定義してください。詳細については、 Miskoのプレゼンテーション を参照してください。

モジュールのコンポーネントは、コントローラー、モデル、ビュー、フィルター、ディレクティブなどのタイプごとに従来のようにグループ化できます。

ただし、モジュール自体はreusabletransferable、およびtestableのままです。

また、開発者がコードの一部とそのすべての依存関係を見つけるのも簡単です。

詳細については、 大規模なAngularJSおよびJavaScriptアプリケーションのコード編成 を参照してください。

フォルダ構造の例

|-- src/
|   |-- app/
|   |   |-- app.js
|   |   |-- home/
|   |   |   |-- home.js
|   |   |   |-- homeCtrl.js
|   |   |   |-- home.spec.js
|   |   |   |-- home.tpl.html
|   |   |   |-- home.less
|   |   |-- user/
|   |   |   |-- user.js
|   |   |   |-- userCtrl.js
|   |   |   |-- userModel.js
|   |   |   |-- userResource.js
|   |   |   |-- user.spec.js
|   |   |   |-- user.tpl.html
|   |   |   |-- user.less
|   |   |   |-- create/
|   |   |   |   |-- create.js
|   |   |   |   |-- createCtrl.js
|   |   |   |   |-- create.tpl.html
|   |-- common/
|   |   |-- authentication/
|   |   |   |-- authentication.js
|   |   |   |-- authenticationModel.js
|   |   |   |-- authenticationService.js
|   |-- assets/
|   |   |-- images/
|   |   |   |-- logo.png
|   |   |   |-- user/
|   |   |   |   |-- user-icon.png
|   |   |   |   |-- user-default-avatar.png
|   |-- index.html

angularアプリケーションの構造化の良い例は、angular-app- https://github.com/angular-app/angular-appによって実装されます/ tree/master/client/src

これは、最新のアプリケーションジェネレーターでも考慮されています- https://github.com/yeoman/generator-angular/issues/109

223
Artem Platonov

あなたが提供した引用に見られるように、イゴールのこれに対する考え方は、はるかに大きな問題の氷山の一角にすぎないと考えています。

MVCおよびその派生物(MVP、PM、MVVM)はすべて単一のエージェント内で適切であり、サーバー/クライアントアーキテクチャはあらゆる目的で2エージェントシステムであり、多くの場合、人々はこれらのパターンに取りつかれているため、目の前の問題がはるかに複雑であることを忘れています。これらの原則を遵守しようとすると、実際には欠陥のあるアーキテクチャになります。

これを少しずつやってみましょう。

ガイドライン

視聴回数

Angularコンテキスト内では、ビューはDOMです。ガイドラインは次のとおりです。

行う:

  • 現在のスコープ変数(読み取り専用)。
  • アクションのためにコントローラーを呼び出します。

しないでください:

  • ロジックを入れます。

魅力的で、短く、無害であるように見えます:

ng-click="collapsed = !collapsed"

これは、JavascriptファイルとHTMLファイルの両方を検査するためにシステムの動作方法を理解する必要がある開発者を意味します。

コントローラー

行う:

  • スコープにデータを配置して、ビューを「モデル」にバインドします。
  • ユーザーアクションに応答します。
  • プレゼンテーションロジックを処理します。

しないでください:

  • あらゆるビジネスロジックに対処します。

最後のガイドラインの理由は、コントローラーがエンティティではなくビューの姉妹であるためです。また、再利用できません。

ディレクティブは再利用可能であると主張できますが、ディレクティブもビューの姉妹(DOM)です-エンティティに対応することを意図したものではありません。

もちろん、ビューはエンティティを表すこともありますが、それはかなり特殊なケースです。

言い換えると、コントローラーはプレゼンテーションに焦点を合わせます-ビジネスロジックを投入すると、膨らんだ、管理しにくいコントローラーになる可能性が高いだけでなく、懸念の分離に違反します原則。

そのため、Angular内のコントローラーは、実際には Presentation Model またはMVVMに相当します。

それで、もしコントローラーがビジネスロジックを扱うべきでないなら、誰がすべきでしょうか?

モデルとは何ですか?

多くの場合、クライアントモデルは部分的で古くなっています

オフラインWebアプリケーション、または非常に単純なアプリケーション(いくつかのエンティティ)を作成している場合を除き、クライアントモデルは次のようになります。

  • 部分
    • すべてのエンティティを持たない(ページネーションの場合など)
    • または、すべてのデータがありません(ページネーションの場合のように)
  • Stale-システムに複数のユーザーがいる場合、どの時点でも、クライアントが保持するモデルがサーバーが保持するモデルと同じであることを確認できません。

実際のモデルは持続する必要があります

従来のMCVでは、モデルはpersistedのみです。モデルについて話すときはいつでも、これらはいつか持続しなければなりません。クライアントはモデルを自由に操作できますが、サーバーへのラウンドトリップが正常に完了するまで、ジョブは完了しません。

結果

上記の2つの点は注意として使用する必要があります。クライアントが保持するモデルは、部分的で、ほとんど単純なビジネスロジックのみを含むことができます。

したがって、クライアントコンテキスト内では、小文字のMを使用するのが賢明です。したがって、実際にはmVCmVP、およびmVVmです。大きなMはサーバー用です。

ビジネスの論理

おそらく、ビジネスモデルに関する最も重要な概念の1つは、それらを2つのタイプに細分できることです(3番目のview-businessは省略します)。

  • ドメインロジック-別名エンタープライズビジネスルール、アプリケーションに依存しないロジック。たとえば、firstNameおよびsirNameプロパティを持つモデルを指定すると、getFullName()のようなゲッターはアプリケーションに依存しないと見なすことができます。
  • アプリケーションロジック-別名アプリケーションビジネスルール。これはアプリケーション固有です。たとえば、エラーのチェックと処理。

クライアントコンテキスト内のこれらの両方が「実際の」ビジネスロジックではない-クライアントにとって重要な部分のみを処理することを強調することが重要です。アプリケーションロジック(ドメインロジックではない)には、サーバーとの通信およびほとんどのユーザーインタラクションを促進する責任があります。一方、ドメインロジックは大部分が小規模で、エンティティ固有であり、プレゼンテーション駆動型です。

問題はまだ残っています-angularアプリケーション内でそれらをどこに投げますか?

3対4層のアーキテクチャ

これらのMVWフレームワークはすべて3つのレイヤーを使用します。

Three circles. Inner - model, middle - controller, outer - view

しかし、クライアントに関しては、これには2つの基本的な問題があります。

  • モデルは部分的で古く、永続しません。
  • アプリケーションロジックを配置する場所がありません。

この戦略に代わるものは、 4層戦略 です。

4 circles, from inner to outer - Enterprise business rules, Application business rules, Interface adapters, Frameworks and drivers

ここでの本当の取引は、アプリケーションのビジネスルールレイヤー(ユースケース)であり、これは多くの場合、クライアントでは見逃されます。

このレイヤーは、インタラクター(ボブおじさん)によって実現されます。これは、Martin Fowlerがoperation script service layerと呼ぶものとほぼ同じです。

具体例

次のWebアプリケーションを検討してください。

  • アプリケーションは、ページ分割されたユーザーのリストを表示します。
  • ユーザーが「ユーザーの追加」をクリックします。
  • モデルが開き、ユーザーの詳細を入力するフォームが表示されます。
  • ユーザーはフォームに入力し、送信を押します。

いくつかのことが今起こるはずです:

  • フォームはクライアント検証済みである必要があります。
  • 要求はサーバーに送信されます。
  • エラーがあれば、処理されます。
  • ユーザーリストは、(ページネーションのため)更新する必要がある場合とそうでない場合があります。

これらすべてをどこに投げますか?

アーキテクチャに$resourceを呼び出すコントローラーが含まれる場合、これらはすべてコントローラー内で発生します。しかし、より良い戦略があります。

提案された解決策

次の図は、Angularクライアントに別のアプリケーションロジックレイヤーを追加することで上記の問題を解決する方法を示しています。

4 boxes - DOM points to Controller, which points to Application logic, which points to $resource

したがって、コントローラーと$ resourceの間にこのレイヤーを追加します(このレイヤーをinteractorと呼びます):

  • サービスです。ユーザーの場合、UserInteractorと呼ばれる場合があります。
  • ユースケースアプリケーションロジックのカプセル化に対応するメソッドを提供します。
  • It controlsサーバーへのリクエスト。コントローラーが自由形式のパラメーターで$ resourceを呼び出す代わりに、このレイヤーは、サーバーへの要求がドメインロジックが動作できるデータを返すようにします。
  • 返されたデータ構造をdomain logicプロトタイプで装飾します。

したがって、上記の具体例の要件を使用して:

  • ユーザーが「ユーザーの追加」をクリックします。
  • コントローラーはインタラクターに空のユーザーモデルを要求します。これはvalidate()のようなビジネスロジックメソッドで装飾されています
  • 送信すると、コントローラーはモデルのvalidate()メソッドを呼び出します。
  • 失敗した場合、コントローラーはエラーを処理します。
  • 成功した場合、コントローラーはcreateUser()でインタラクターを呼び出します
  • インタラクターは$ resourceを呼び出します
  • 応答すると、インタラクターはエラーをコントローラーに委任し、コントローラーがエラーを処理します。
  • 正常に応答すると、インタラクターは必要に応じてユーザーリストが更新されることを確認します。
46
Izhaki

Artemの答えの素晴らしいアドバイスと比較して小さな問題ですが、コードの可読性の観点から、変数が定義されているかどうかを確認するためにコード内を往復することを最小限に抑えるために、returnオブジェクト内でAPIを完全に定義するのが最善であることがわかりました:

angular.module('myModule', [])
// or .constant instead of .value
.value('myConfig', {
  var1: value1,
  var2: value2
  ...
})
.factory('myFactory', function(myConfig) {
  ...preliminary work with myConfig...
  return {
    // comments
    myAPIproperty1: ...,
    ...
    myAPImethod1: function(arg1, ...) {
    ...
    }
  }
});

returnオブジェクトが「混雑しすぎている」ように見える場合、それはサービスが過剰に処理していることを示しています。

5
Dmitri Zaitsev

AngularJSはMVCを従来の方法で実装するのではなく、MVVM(Model-View-ViewModel)に近いものを実装します。ViewModelはバインダとも呼ばれます(angularの場合は$ scope)。モデル->知っているように、angularのモデルは、単純な古いJSオブジェクトまたはアプリケーションのデータになります。

View-> angleJSのビューは、ディレクティブまたは命令またはバインディングを適用することにより、angularJSによって解析およびコンパイルされたHTMLです。ここでの主なポイントはangularにあります入力は単なるHTML文字列ではありません(innerHTML)ではなく、ブラウザによって作成されたDOMです。

ViewModel-> ViewModelは、実際にビューとモデルの間のバインダー/ブリッジであり、angularJSの場合は$ scopeであり、Controllerを使用して$ scopeを初期化および拡張します。

答えをまとめると、angularJSアプリケーションでは、$ scopeがデータへの参照を持っているため、Controllerは動作を制御し、Viewはコントローラと対話してレイアウトを処理し、それに応じて動作します。

0
Ashutosh