この投稿 のコンテキストで、AngularJSのリーダーであるIgor Minarによる:
MVC vs MVVM vs MVP多くの開発者が議論や議論に何時間も費やすことができるなんて物議をかもすトピックです。
数年間、AngularJSはMVC(またはクライアント側の亜種の1つ)に近づいていましたが、時間の経過とともに、多くのリファクタリングとAPIの改善のおかげで、MVVM–$ scopeオブジェクトはViewModelこれは、Controllerと呼ぶ関数によって装飾されています。
フレームワークを分類し、MV *バケットの1つに入れることができることには、いくつかの利点があります。フレームワークで構築されているアプリケーションを表すメンタルモデルを簡単に作成できるようにすることで、開発者がAPIに慣れるのに役立ちます。また、開発者が使用する用語を確立するのにも役立ちます。
そうは言っても、開発者がMV *ナンセンスについて議論する時間を無駄にするのではなく、適切に設計され、懸念の分離に従うキックアスアプリを構築することを望みます。そしてこのため、私はここでAngularJSをMVWフレームワーク-Model-View-Whatever。 Whateverは「whatever works for you」の略です。
Angularは、プレゼンテーションロジックをビジネスロジックやプレゼンテーション状態からうまく分離するための柔軟性を提供します。結局のところ、それほど重要ではないことについての激しい議論ではなく、生産性とアプリケーションの保守性を高めるために使用してください。
クライアント側のアプリケーションにAngularJS MVW(Model-View-Whatever)デザインパターンを実装するための推奨事項やガイドラインはありますか?
膨大な貴重な情報源のおかげで、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のプレゼンテーション を参照してください。
モジュールのコンポーネントは、コントローラー、モデル、ビュー、フィルター、ディレクティブなどのタイプごとに従来のようにグループ化できます。
ただし、モジュール自体はreusable、transferable、および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
あなたが提供した引用に見られるように、イゴールのこれに対する考え方は、はるかに大きな問題の氷山の一角にすぎないと考えています。
MVCおよびその派生物(MVP、PM、MVVM)はすべて単一のエージェント内で適切であり、サーバー/クライアントアーキテクチャはあらゆる目的で2エージェントシステムであり、多くの場合、人々はこれらのパターンに取りつかれているため、目の前の問題がはるかに複雑であることを忘れています。これらの原則を遵守しようとすると、実際には欠陥のあるアーキテクチャになります。
これを少しずつやってみましょう。
Angularコンテキスト内では、ビューはDOMです。ガイドラインは次のとおりです。
行う:
しないでください:
魅力的で、短く、無害であるように見えます:
ng-click="collapsed = !collapsed"
これは、JavascriptファイルとHTMLファイルの両方を検査するためにシステムの動作方法を理解する必要がある開発者を意味します。
行う:
しないでください:
最後のガイドラインの理由は、コントローラーがエンティティではなくビューの姉妹であるためです。また、再利用できません。
ディレクティブは再利用可能であると主張できますが、ディレクティブもビューの姉妹(DOM)です-エンティティに対応することを意図したものではありません。
もちろん、ビューはエンティティを表すこともありますが、それはかなり特殊なケースです。
言い換えると、コントローラーはプレゼンテーションに焦点を合わせます-ビジネスロジックを投入すると、膨らんだ、管理しにくいコントローラーになる可能性が高いだけでなく、懸念の分離に違反します原則。
そのため、Angular内のコントローラーは、実際には Presentation Model またはMVVMに相当します。
それで、もしコントローラーがビジネスロジックを扱うべきでないなら、誰がすべきでしょうか?
オフラインWebアプリケーション、または非常に単純なアプリケーション(いくつかのエンティティ)を作成している場合を除き、クライアントモデルは次のようになります。
従来のMCVでは、モデルはpersistedのみです。モデルについて話すときはいつでも、これらはいつか持続しなければなりません。クライアントはモデルを自由に操作できますが、サーバーへのラウンドトリップが正常に完了するまで、ジョブは完了しません。
上記の2つの点は注意として使用する必要があります。クライアントが保持するモデルは、部分的で、ほとんど単純なビジネスロジックのみを含むことができます。
したがって、クライアントコンテキスト内では、小文字のM
を使用するのが賢明です。したがって、実際にはmVC、mVP、およびmVVmです。大きなM
はサーバー用です。
おそらく、ビジネスモデルに関する最も重要な概念の1つは、それらを2つのタイプに細分できることです(3番目のview-businessは省略します)。
firstName
およびsirName
プロパティを持つモデルを指定すると、getFullName()
のようなゲッターはアプリケーションに依存しないと見なすことができます。クライアントコンテキスト内のこれらの両方が「実際の」ビジネスロジックではない-クライアントにとって重要な部分のみを処理することを強調することが重要です。アプリケーションロジック(ドメインロジックではない)には、サーバーとの通信およびほとんどのユーザーインタラクションを促進する責任があります。一方、ドメインロジックは大部分が小規模で、エンティティ固有であり、プレゼンテーション駆動型です。
問題はまだ残っています-angularアプリケーション内でそれらをどこに投げますか?
これらのMVWフレームワークはすべて3つのレイヤーを使用します。
しかし、クライアントに関しては、これには2つの基本的な問題があります。
この戦略に代わるものは、 4層戦略 です。
ここでの本当の取引は、アプリケーションのビジネスルールレイヤー(ユースケース)であり、これは多くの場合、クライアントでは見逃されます。
このレイヤーは、インタラクター(ボブおじさん)によって実現されます。これは、Martin Fowlerがoperation script service layerと呼ぶものとほぼ同じです。
次のWebアプリケーションを検討してください。
いくつかのことが今起こるはずです:
これらすべてをどこに投げますか?
アーキテクチャに$resource
を呼び出すコントローラーが含まれる場合、これらはすべてコントローラー内で発生します。しかし、より良い戦略があります。
次の図は、Angularクライアントに別のアプリケーションロジックレイヤーを追加することで上記の問題を解決する方法を示しています。
したがって、コントローラーと$ resourceの間にこのレイヤーを追加します(このレイヤーをinteractorと呼びます):
UserInteractor
と呼ばれる場合があります。したがって、上記の具体例の要件を使用して:
validate()
のようなビジネスロジックメソッドで装飾されていますvalidate()
メソッドを呼び出します。createUser()
でインタラクターを呼び出します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
オブジェクトが「混雑しすぎている」ように見える場合、それはサービスが過剰に処理していることを示しています。
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はコントローラと対話してレイアウトを処理し、それに応じて動作します。