AngularJS:デザインパターンを理解する


147

AngularJSのリーダーであるIgor Minarによるこの投稿に関連して:

MVP対MVVM対MVC。多くの開発者が何時間もの時間を費やして議論し、議論することができる、議論の余地のあるトピックです。

数年間AngularJSは(というか1そのクライアント側の変異体)に近いMVCとなりましたが、時間と多くのリファクタリングとAPIの改善のおかげ上で、それが近い今だMVVM - $スコープオブジェクトは、考えることができるのViewModelされていることControllerと呼ばれる関数で装飾されています

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

とは言っても、開発者がMV *のナンセンスについて議論するのに時間を浪費するのではなく、開発者が適切に設計され、懸念の分離に従ってキックアスアプリを構築する方がいいと思います。このため、AngularJSMVWフレームワーク-Model-View-Whateverとして宣言 します。「何でもあなたのために働く」の意味。

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

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


...に賛成しているのは、MV *のナンセンスについて議論するのに時間を浪費しているからです。
Shirgill Farhan 2015

1
単語クラスのデザインパターンに従うためにAngularは必要ありません。
有用なビー

回答:


223

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


コントローラ

  • コントローラは、モデルとビューの間の単なる中間層である必要があります。できるだけ薄くするようにしてください。

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

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

  • コントローラは、プレゼンテーションやDOM操作を気にする必要はありません

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

  • コントローラーのスコープは、プレゼンテーションモデルのデザインパターンと同様に、ビューとモデルをバインドし、ビューモデル
    カプセル化するために使用する必要があります。


範囲

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

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


型番

AngularJSのモデルは、サービスによって定義されるシングルトンです。

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

モデルは通常、1つの依存関係(ある種のイベントエミッター、一般的には$ rootScope)を持ち、テストしやすいドメインロジックを含んでいるため、ユニットテストの主要な候補です。

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

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

  • モデルは、同様のアプリケーションに簡単に転送できるようにポータブルでなければなりません。

  • モデル内のユニットロジックを分離することで、検索、更新、維持がより簡単になりました。

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

  • コンポーネントの結合を減らし、ユニットのテスト容易性使いやすさを高めることが実際に依存しない場合は、依存関係注入を使用して他のモデルをモデルに構成しないようにしてください。

  • モデルでイベントリスナーを使用しないようにします。それはそれらをテストすることを困難にし、一般に単一の責任原理の観点からモデルを殺します。

モデルの実装

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

AngularJSアプリケーションでそれを行う方法は、ファクトリサービスタイプを使用して定義することです。これにより、プライベートプロパティとメソッドを非常に簡単に定義でき、パブリックにアクセス可能なものを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
    // ...
  };
});

グローバルモデル

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

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

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

すべてのグローバルメソッドは、 ' common 'プロパティ内に存在します。これは一種の名前空間です。

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


資源

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

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

特に、HTTP / JSONエンドポイントへの再利用可能なプロキシです。

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

リソースの実装

RESTfulサーバー側データソースとの対話を可能にするリソースオブジェクトを作成するファクトリ。

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


サービス

モデルもリソースもサービスです。

サービスは、関連付けられておらず、緩やかに結合された、自己完結型の機能単位です。

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

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

Angularにはさまざまなタイプのサービスが付属しています。それぞれに独自の使用例があります。詳細については、サービスタイプについてをご覧ください。

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

一般に、Webサービス用語集によると:

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


クライアント側の構造

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

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

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

ただし、モジュール自体は、再利用転送、およびテストが可能なままです。

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

詳細については、大規模な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

角度のアプリケーション構築のよい例がで実装された角度アプリ - https://github.com/angular-app/angular-app/tree/master/client/src

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


5
「コントローラーでビジネスロジックを回避することを強くお勧めします。モデルに移動する必要があります。」ただし、公式ドキュメントからは次のように読むことができます。「一般的に、コントローラーはあまりやりすぎないでください。単一のビューに必要なビジネスロジックのみを含める必要があります。」私たちは同じことを話しているのですか?
op1ekun 2014

3
私は言うでしょう-コントローラをビューモデルとして扱います。
Artem Platonov 2014

1
+1。ここにいくつかの素晴らしいアドバイスがあります!2.残念ながら、の例はsearchModel再利用性に関するアドバイスに従っていません。constantサービス経由で定数をインポートする方が良いでしょう。3.ここでの意味は何ですか?:Try to avoid having a factory that returns a new able function
Dmitri Zaitsev 14

1
また、オブジェクトのprototypeプロパティを上書きすると、継承が無効になり、代わりに使用できますCar.prototype.save = ...
Dmitri Zaitsev

2
@ChristianAichinger、これは、object双方向のバインディング式でを使用して、正確なプロパティまたはsetter関数に確実に書き込むように強制するJavaScriptプロトタイプチェーンの性質についてです。スコープの直接プロパティ(ドットなし)を使用する場合、プロトタイプチェーンの最も近い上位スコープに新しく作成されたプロパティを書き込むときに、目的のターゲットプロパティを非表示にするリスクがあります。これはMiskoのプレゼンテーション
Artem Platonov

46

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

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

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

ガイドライン

ビュー

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

行う:

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

してはいけないこと:

  • ロジックを入れます。

魅力的で短く、無害なものとして、これは次のようになります。

ng-click="collapsed = !collapsed"

これは、JavascriptファイルとHTMLファイルの両方を検査するためにシステムがどのように機能するかを理解するようになった開発者にほとんど意味があります。

コントローラー

行う:

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

してはいけないこと:

  • ビジネスロジックを処理します。

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

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

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

言い換えると、コントローラーはプレゼンテーションに重点を置く必要があります。ビジネスロジックを投入すると、膨らんだ、管理しにくいコントローラーになるだけでなく、懸念分離の原則にも違反します。

そのため、Angularのコントローラーは、実際にはPresentation ModelまたはMVVMに近いものです。

したがって、コントローラーがビジネスロジックを処理しない場合、誰が処理する必要がありますか?

モデルとは?

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

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

  • 部分的
    • (ページネーションの場合のように)それがすべてのエンティティを持っているわけではありません
    • またはそれはすべてのデータを持っていません(ページネーションの場合のように)
  • 古い -システムに複数のユーザーがいる場合、クライアントが保持しているモデルがサーバーが保持しているモデルと同じであるかどうかはいつでもわかりません。

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

従来のMCVでは、モデルは永続化される唯一のものです。モデルについて話すときはいつでも、これらはある時点で持続する必要があります。クライアントは自由にモデルを操作できますが、サーバーへのラウンドトリップが正常に完了するまで、ジョブは実行されません。

結果

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

そのため、おそらくクライアントコンテキスト内では小文字を使用Mするのが賢明ですつまり、実際にはmVCmVP、およびmVVmです。大きいのMはサーバー用です。

ビジネスの論理

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

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

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

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

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

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

3つの円。 内部-モデル、中間-コントローラー、外部-ビュー

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

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

この戦略の代替案は、4層戦略です。

内部から外部への4つの円-エンタープライズビジネスルール、アプリケーションビジネスルール、インターフェースアダプター、フレームワーク、ドライバー

ここでの真の取り決めは、アプリケーションビジネスルールレイヤー(ユースケース)です。

このレイヤーは、インタラクター(ボブおじさん)によって実現されます。これは、Martin Fowlerが操作スクリプトサービスレイヤーと呼んでいるものです。

具体例

次のWebアプリケーションについて考えてみます。

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

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

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

これらすべてをどこに投げればよいでしょうか?

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

提案された解決策

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

4つのボックス-DOMはコントローラーを指し、コントローラーは$ resourceを指すアプリケーションロジックを指します。

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

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

そして、上記の具体例の要件を使用して:

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

したがって、AngularJSはMVW(Wは何でもかまいません)で定義されています。これは、コントローラー(すべてのビジネスロジックを含む)またはビューモデル/プレゼンター(ビジネスロジックはなく、ビューを埋めるコードのみ)をBLで選択できるためです。別のサービス?私は正しいですか?
BAD_SEED 2015

ベストアンサー。4層の角度付きアプリのGitHubに実際の例がありますか?
RPallas

1
@RPallas、いいえ、私はしません(このための時間があれば)。現在、「アプリケーションロジック」が境界インタラクターにすぎないアーキテクチャを試しています。それとコントローラーの間のリゾルバーと、いくつかのビューロジックを持つビューモデル。まだ実験段階なので、賛否両論の100%ではありません。しかし、一度完了したら、どこかでブログを書きたいと思います。
Izhaki

1
@heringer基本的に、モデルを導入しました-ドメインエンティティを表すOOP構成体。コントローラではなく、リソースと通信するのはこれらのモデルです。ドメインロジックをカプセル化します。コントローラはモデルを呼び出し、モデルはリソースを呼び出します。
Izhaki

1
@ alex440いいえ。このトピックについての深刻なブログ投稿が私の指先にあるのは今から2か月前ですが。クリスマスが来る-おそらくその後。
イザキ、2016

5

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オブジェクトが「混みすぎている」ように見える場合は、サービスが過度に実行していることを示しています。


0

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

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

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

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


-1

質問を明確にするために、Angularは、通常のプログラミングですでに遭遇したさまざまなデザインパターンを使用しています。1)モジュールに関してコントローラーまたはディレクティブ、ファクトリー、サービスなどを登録するとき。ここでは、グローバルスペースからデータを隠しています。これはモジュールパターンです。2)angularがスコープ変数の比較にダーティチェックを使用する場合、ここではObserverパターンを使用します。3)コントローラーのすべての親子スコープは、プロトタイプパターンを使用します。 4)サービスを注入する場合、ファクトリパターンを使用します。

全体として、既知のさまざまな設計パターンを使用して問題を解決します。

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