web-dev-qa-db-ja.com

MVC / MVVM /レイヤーの分離のViewModels-ベストプラクティス?

私はViewModelを使用するのはかなり新しいのですが、ドメインモデルのインスタンスをプロパティとしてViewModelに含めることは許容されますか、またはそれらのドメインモデルのプロパティはViewModel自体のプロパティである必要がありますか?たとえば、クラスAlbum.csがある場合

public class Album
{
    public int AlbumId { get; set; }
    public string Title { get; set; }
    public string Price { get; set; }
    public virtual Genre Genre { get; set; }
    public virtual Artist Artist { get; set; }
}

通常、ViewModelにAlbum.csクラスのインスタンスを保持させるか、ViewModelにAlbum.csクラスの各プロパティのプロパティを持たせますか。

public class AlbumViewModel
{
    public Album Album { get; set; }
    public IEnumerable<SelectListItem> Genres { get; set; }
    public IEnumerable<SelectListItem> Artists { get; set; }
    public int Rating { get; set; }
    // other properties specific to the View
}


public class AlbumViewModel
{
    public int AlbumId { get; set; }
    public string Title { get; set; }
    public string Price { get; set; }
    public IEnumerable<SelectListItem> Genres { get; set; }
    public IEnumerable<SelectListItem> Artists { get; set; }
    public int Rating { get; set; }
    // other properties specific to the View
}
23
Iain

tl; dr

ViewModelがドメインモデルのインスタンスを含むことは許容されますか?

基本的にそうではありません。文字通り2つのレイヤーを混合し、それらを結合しているからです。私は認めなければなりません、私はそれがたくさん起こると思います、そしてそれはあなたのプロジェクトの quick-win-level に少し依存しますが、私たちはそれが[〜#〜] solid [〜#〜] の単一責任の原則.


楽しい部分:これはMVCのビューモデルに限定されず、実際には 古き良きデータ、ビジネス、UIレイヤー の分離の問題です。これについては後で説明しますが、今のところは。これはMVCに適用されますが、さらに多くのデザインパターンにも適用されます。

まず、いくつかの一般的な適用可能な概念を指摘し、後で実際のシナリオと例を拡大します。


レイヤーを混ぜないことの長所と短所を考えてみましょう。

費用がかかる

常に問題があります。まとめて、後で説明し、通常は適用されない理由を示します。

  • 重複するコード
  • 複雑さが増す
  • 追加のパフォーマンスヒット

あなたが得るもの

常に勝利があります。まとめて、後で説明し、これが実際に理にかなっている理由を示します

  • レイヤーの独立した制御

コスト


重複するコード

そうではありません [〜#〜] dry [〜#〜]

おそらく他のクラスとまったく同じである追加のクラスが必要になります。

これは無効な引数です。異なるレイヤーには明確に定義された異なる目的があります。したがって、1つのレイヤーに存在するプロパティは、他のプロパティとは異なる目的を持っています-プロパティが同じ名前であっても!

例えば:

これはあなた自身を繰り返すことではありません:

_public class FooViewModel
{
    public string Name {get;set;}
}

public class DomainModel
{
    public string Name {get;set;}
}
_

一方、マッピングを2回定義すると、 is が繰り返されます。

_public void Method1(FooViewModel input)
{
    //duplicate code: same mapping twice, see Method2
    var domainModel = new DomainModel { Name = input.Name };
    //logic
}

public void Method2(FooViewModel input)
{
    //duplicate code: same mapping twice, see Method1
    var domainModel = new DomainModel { Name = input.Name };
    //logic
}
_

もっと面倒です!

ほんとに?コーディングを開始すると、モデルの99%以上が重複します。一杯のコーヒーをつかむには時間がかかります;-)

「より多くのメンテナンスが必要です」

はい、あります。そのため、マッピングを単体テストする必要があります(そして、マッピングを繰り返さないでください)。

複雑さが増す

いいえ、違います。レイヤーが追加され、より複雑になります。複雑さを増すことはありません。

私の賢い友人は、かつてこのように述べました:

「飛行平面は非常に複雑なものです。落下平面は非常に複雑です。」

このような定義を使用しているのは彼だけではありません 、違いは予測可能性にあり、 entropy と実際の関係があります chaos

一般的に:パターンは複雑さを追加しません。これらは、複雑さを軽減するために存在します。彼らはよく知られている問題の解決策です。明らかに、実装が不十分なパターンは役に立たないため、パターンを適用する前に問題を理解する必要があります。問題を無視しても効果はありません。いつか返済しなければならない技術的負債を追加するだけです。

レイヤーを追加すると、明確に定義された動作が得られます。これは明らかな追加のマッピングにより、(ビット)より複雑になります。さまざまな目的でレイヤーを混合すると、変更が適用されたときに予期しない副作用が発生します。データベース列の名前を変更すると、UIのキー/値ルックアップに不一致が生じ、既存のAPI呼び出しを実行できなくなります。これについて考えてみましょう。これがデバッグ作業と保守コストにどのように関係するかを考えてください。

追加のパフォーマンスヒット

はい、マッピングを追加すると、CPUパワーが余分に消費されます。ただし、これは(Raspberry Piがリモートデータベースに接続されている場合を除いて)、データベースからデータをフェッチする場合に比べると無視できます。結論:これが問題の場合:キャッシュを使用します。

勝ち


レイヤーの独立した制御

これは何を意味するのでしょうか?

これ以上の組み合わせ:

  • 予測可能なシステムを作成する
  • uIに影響を与えずにビジネスロジックを変更する
  • ビジネスロジックに影響を与えずにデータベースを変更する
  • データベースに影響を与えることなく、UIを変更する
  • 実際のデータストアを変更できる
  • 完全に独立した機能、十分にテスト可能な分離された動作、および保守の容易さ
  • 変化に対応し、ビジネスに力を与える

つまり、厄介な副作用を心配することなく、明確に定義されたコードを変更することで、変更を加えることができます。

注意:ビジネス対策!

"これは変更を反映するためのものであり、変更されることはありません!"

変化が来るでしょう: 毎年何兆ドルもの米ドルを使う は単純に通り過ぎることはできません。

まあそれはいいです。しかし、開発者としてそれに直面してください。間違いのない日が仕事をやめる日です。同じことがビジネス要件にも当てはまります。

面白い事実;ソフトウェアエントロピー

「私の(マイクロ)サービスまたはツールは、それを処理するのに十分小さいです!」

ここには実際に良い点があるので、これは最も難しいかもしれません。一度使用するものを開発する場合、おそらく変更にまったく対応できず、とにかくそれを再構築する必要があります。提供実際にそれを再利用します。それにもかかわらず、他のすべてのもの:「変更が来る」だから、なぜ変更をより複雑にするのですか?また、おそらく、最小限のツールまたはサービスでレイヤーを除外すると、通常はデータレイヤーが(ユーザー)インターフェースに近くなります。 APIを扱っている場合、実装にはバージョンの更新が必要であり、すべてのクライアントに配布する必要があります。一回のコーヒーブレイクの間にそれをすることができますか?

"今のところ、それをすばやく簡単に実行できます..."

あなたの仕事は "当分の間" ですか?冗談です;-)しかし;いつ修正しますか?おそらくあなたの技術的な負債があなたを強制するとき。当時、この短いコーヒーブレークよりも費用がかかりました。

「「変更のために閉じ、拡張のために開く」についてはどうですか?それもSOLID原則です!]

はい、そうです!しかし、これはタイプミスを修正すべきではないという意味ではありません。または、適用されたすべてのビジネスルールを拡張機能の合計として表現できること、または破損しているものを修正することが許可されていないこと。または Wikipedia のように:

モジュールが他のモジュールで使用できる場合、そのモジュールは閉じられていると言います。これは、モジュールに明確に定義された安定した説明(情報を隠すという意味でのインターフェース)が与えられていることを前提としています。

これは実際にレイヤーの分離を促進します。


さて、いくつかの典型的なシナリオ:

#ASP.NET MVC

なぜなら、これが実際の質問で使用しているものだからです。

例を挙げましょう。次のビューモデルとドメインモデルを想像してください。

note:これは、他のタイプのレイヤーにも適用できます。たとえば、DTO、DAO、Entity、ViewModel、Domainなどです。

_public class FooViewModel
{
    public string Name {get; set;} 

    //hey, a domain model class!
    public DomainClass Genre {get;set;} 
}

public class DomainClass
{
    public int Id {get; set;}      
    public string Name {get;set;} 
}
_

したがって、コントローラのどこかに FooViewModel を入力し、それをビューに渡します。

ここで、以下のシナリオを検討してください。

1)ドメインモデルが変更されます。

この場合は、おそらくビューも調整する必要があります。これは、関心事の分離というコンテキストでは悪い習慣です。

ViewModelをDomainModelから分離している場合は、マッピングの微調整(ViewModel => DomainModel(およびその逆))で十分です。

2)DomainClassにはネストされたプロパティがあり、ビューには "GenreName"のみが表示されます

私はこれが実際のライブシナリオでうまくいかないのを見ました。

この場合の一般的な問題は、_@Html.EditorFor_を使用すると、ネストされたオブジェクトの入力が発生することです。これには、Idsおよびその他の機密情報が含まれる場合があります。これは、実装の詳細が漏洩することを意味します。実際のページはドメインモデル(おそらくデータベースのどこかに関連付けられている)に関連付けられています。このコースを進むと、hidden入力を作成することがわかります。これをサーバー側のモデルバインディングまたはオートマッパーと組み合わせると、非表示のIdの操作をfirebugなどのツールでブロックしたり、プロパティに属性を設定し忘れたりすると、ビューで使用できるようになります。

これらのフィールドの一部をブロックすることは可能ですが、おそらく簡単ですが、ネストされたDomain/Dataオブジェクトが多ければ多いほど、この部分を正しくするのが難しくなります。そして;このドメインモデルを複数のビューで「使用」している場合はどうなりますか?それらは同じように動作しますか?また、必ずしもビューをターゲットにしているわけではない理由で、DomainModelを変更する場合があることにも注意してください。したがって、DomainModelを変更するたびに、それが might によってビューとコントローラーのセキュリティの側面に影響を与えることに注意する必要があります。

3)ASP.NET MVCでは、検証属性を使用するのが一般的です。

ドメインにビューに関するメタデータを本当に含めたいですか?または、データ層にビューロジックを適用しますか?ビューの検証は常にドメインの検証と同じですか?同じフィールドがありますか(またはそれらの一部は連結されていますか)?同じ検証ロジックがありますか?ドメインモデルクロスアプリケーションを使用していますか?等.

これが進むべき道ではないことは明らかだと思います。

4)もっと

私はあなたにもっと多くのシナリオを与えることができますが、それはより魅力的なものへの好みの問題です。私はこの時点であなたがポイントを獲得することを願っています:)それにもかかわらず、私はイラストを約束しました:

Scematic

さて、本当に汚くてすぐ勝つためにはそれはうまくいきますが、私はあなたがそれを欲するべきではないと思います。

通常、ドメインモデルと80%以上類似しているビューモデルを構築するのは少しだけ手間がかかります。これは不必要なマッピングを行うように感じるかもしれませんが、最初の概念的な違いが発生すると、努力する価値があったことがわかります:)

代替として、私は一般的なケースのために次の設定を提案します:

  • ビューモデルを作成する
  • ドメインモデルを作成する
  • データモデルを作成する
  • automapperのようなライブラリを使用して、一方から他方へのマッピングを作成します(これは_Foo.FooProp_を_OtherFoo.FooProp_にマッピングするのに役立ちます)

利点は、例えば;データベーステーブルの1つに追加のフィールドを作成しても、ビューには影響しません。ビジネスレイヤーやマッピングに影響を与える可能性がありますが、そこで停止します。もちろん、ほとんどの場合、ビューも変更したいのですが、この場合、する必要はありません。したがって、コードの一部で問題を分離します。

Web API /データレイヤー/ DTO

これがWeb-API/ORM(EF)シナリオでどのように機能するかについてのもう1つの具体的な例:

ここではより直感的です。特にコンシューマがサードパーティの場合、ドメインモデルがコンシューマの実装と一致する可能性は低いため、ビューモデルは完全に自己完結型である可能性が高くなります。

Web Api Datalayer EF

note「ドメインモデル」という名前は、DTOまたは「モデル」とも呼ばれます

Web(またはHTTPまたはREST)APIでは注意してください。通信は多くの場合、HTTPエンドポイントで公開されている実際の「もの」であるデータ転送オブジェクト(DTO)によって行われます。

したがって、これらのDTOをどこに配置すればよいでしょうか。それらはドメインモデルとビューモデルの間にありますか?はい、そうです;消費者がカスタマイズされたビューを実装する可能性が高いので、それらをviewmodelとして扱うのは難しいだろうことはすでに見てきました。

DTOはdomainmodelsを置き換えることができますか、それとも独自に存在する理由がありますか?一般に、分離の概念は_DTO's_およびdomainmodelsにも適用できます。しかし、もう一度言いましょう。あなたは自分自身に尋ねることができます(そして、これが私が少し実用的である傾向があるところです)ドメイン内にdomainlayerを明示的に定義するのに十分なロジックがありますか?サービスがどんどん小さくなると、logicの一部である実際のdomainmodelsも減少し、一緒に除外されて、で終わる:

EF/(ORM) EntitiesDTOConsumers


免責事項/注記

@mrjoltcolaが述べたように:覚えておくべきコンポーネントのオーバーエンジニアリングもあります。上記のいずれにも該当せず、ユーザー/プログラマーが信頼できる場合は、問題ありません。ただし、DomainModelとViewModelの混合により、保守性と再利用性が低下することに注意してください。

43
Stefan

技術的なベストプラクティスと個人の好みの組み合わせから、意見はさまざまです。

ビューモデルでドメインオブジェクトを使用したり、モデルとしてドメインオブジェクトを使用したりしても、何もありません間違っています。多くの人が行っています。すべてのビューに対してビューモデルを作成することを強く望んでいる人もいますが、個人的には、多くのアプリは、快適な1つのアプローチを学び、繰り返す開発者によって過剰に設計されていると感じています。真実は、ASP.NET MVCの新しいバージョンを使用して目標を達成する方法がいくつかあることです。

ビューモデルとビジネスおよび永続化レイヤーに共通のドメインクラスを使用する場合の最大のリスクは、モデルインジェクションのリスクです。モデルクラスに新しいプロパティを追加すると、それらのプロパティがサーバーの境界外に公開される可能性があります。攻撃者は、表示してはいけないプロパティ(シリアル化)を確認し、変更してはならない値(モデルバインダー)を変更する可能性があります。

インジェクションを防ぐには、全体的なアプローチに関連する安全な方法を使用してください。ドメインオブジェクトを使用する場合は、コントローラーまたはモデルバインダーのアノテーションでホワイトリストまたはブラックリスト(包含/除外)を使用してください。ブラックリストの方が便利ですが、将来のリビジョンを作成する怠惰な開発者は、それらを忘れたり、気づかない可能性があります。ホワイトリスト([Bind(Include = ...)]は必須であり、新しいフィールドが追加されると注意が必要になるため、インラインビューモデルとして機能します。

例:

[Bind(Exclude="CompanyId,TenantId")]
public class CustomerModel
{
    public int Id { get; set; }
    public int CompanyId { get; set; } // user cannot inject
    public int TenantId { get; set; }  // ..
    public string Name { get; set; }
    public string Phone { get; set; }
    // ...
}

または

public ActionResult Edit([Bind(Include = "Id,Name,Phone")] CustomerModel customer)
{
    // ...
}

最初のサンプルは、アプリケーション全体にマルチテナントの安全性を適用する良い方法です。 2番目のサンプルでは、​​各アクションをカスタマイズできます。

アプローチに一貫性を持たせ、プロジェクトで使用されているアプローチを他の開発者に明確に文書化します。

セキュリティ/演習として、ログイン/プロファイル関連機能のビューモデルを常に使用して、Webコントローラーとデータアクセスレイヤーの間のフィールドを「マーシャリング」することをお勧めします。

16
codenheim