【问题标题】:ViewModels in MVC / MVVM / Separation of layers- best practices?MVC / MVVM /层分离中的ViewModels-最佳实践?
【发布时间】:2014-05-14 08:00:27
【问题描述】:

我对使用 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
}

【问题讨论】:

  • 像第一种方法一样授予对模型对象的完全访问权限可能很危险。更好的选择是将您的模型放在私有字段中,并仅公开查看所需的属性。例如公共字符串标题 { 获取 { 返回 this.album.Title; } set{ this.album.Title = value;} }

标签: c# asp.net-mvc mvvm asp.net-mvc-viewmodel


【解决方案1】:

tl;博士

ViewModel 是否可以包含领域模型的实例?

基本上不是因为您实际上是在混合两个层并将它们捆绑在一起。我必须承认,我经常看到这种情况发生,这在一定程度上取决于您项目的 quick-win-level,但我们可以说它不符合单一责任原则 em> SOLID


有趣的部分:这不仅限于 MVC 中的视图模型,它实际上是 good old data, business and ui layers 的分离问题。我稍后会说明这一点,但现在;请记住,它适用于 MVC,但也适用于更多设计模式。

我将首先指出一些普遍适用的概念,然后放大一些实际场景和示例。


让我们考虑一下不混合图层的一些利弊。

你会花多少钱

总有一个问题,我会总结它们,稍后解释,并说明为什么它们通常不适用

  • 重复代码
  • 增加了额外的复杂性
  • 额外的性能提升

你会得到什么

总有胜利,我会总结一下,稍后解释,并说明为什么这实际上是有意义的

  • 图层的独立控制

费用


重复代码

不是DRY

您将需要一个额外的类,它可能与另一个类完全相同。

这是一个无效的参数。不同的层具有明确定义的不同目的。因此,位于一层中的属性与位于另一层中的属性具有不同的用途 - 即使这些属性具有相同的名称!

例如:

这不是重复你自己:

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

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

另一方面,定义一个映射两次,重复你自己:

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% 的模型会重叠。喝杯咖啡需要更多时间;-)

“它需要更多的维护”

是的,这就是为什么您需要对映射进行单元测试(记住,不要重复映射)。

增加了额外的复杂性

不,它没有。它增加了一个额外的层,这使它更复杂。它不会增加复杂性。

我的一个聪明的朋友,曾经这样说:

“飞行的飞机是很复杂的东西。坠落的飞机也很复杂。”

He is not the only one using such a definition,区别在于可预测性,它与 chaos 的度量)有实际关系。

一般来说:模式不会增加复杂性。它们的存在是为了帮助您降低复杂性。它们是众所周知的问题的解决方案。显然,一个实施不佳的模式没有帮助,因此您需要在应用该模式之前了解问题。忽略问题也无济于事。它只是增加了必须在某个时候偿还的技术债务。

添加一个层可以为您提供明确定义的行为,由于明显的额外映射,这将(有点)更复杂。应用更改时,出于各种目的混合图层将导致不可预知的副作用。重命名数据库列将导致 UI 中的键/值查找不匹配,从而使您执行不存在的 API 调用。现在,想一想这与您的调试工作和维护成本有何关系。

额外的性能打击

是的,额外的映射会导致消耗额外的 CPU 资源。但是,与从数据库中获取数据相比,这(除非您将树莓派连接到远程数据库)可以忽略不计。底线:如果这是一个问题:使用缓存。

胜利


图层的独立控制

这是什么意思?

这个(以及更多)的任意组合:

  • 创建可预测的系统
  • 在不影响 UI 的情况下更改业务逻辑
  • 在不影响业务逻辑的情况下更改数据库
  • 在不影响数据库的情况下更改用户界面
  • 能够更改您的实际数据存储
  • 完全独立的功能、隔离且可测试的行为且易于维护
  • 应对变化并为业务赋能

本质上:您可以通过更改定义明确的代码来进行更改,而不必担心令人讨厌的副作用。

注意:商业对策!

“这是为了反映变化,它不会改变!”

变化终将到来:spending trillions of US dollar annually 不能随便溜走。

那很好。但面对现实,作为开发人员;你不犯任何错误的那一天就是你停止工作的那一天。这同样适用于业务需求。

fun fact; software entropy

“我的(微)服务或工具小到足以应付它!”

这可能是最难的,因为这里实际上有一个好点。如果你开发的东西是一次性使用的,它可能根本无法应对变化,无论如何你都必须重建它,如果你真的要重用它。尽管如此,对于所有其他事情:“改变将会到来”,那么为什么要让改变变得更复杂呢?而且,请注意,可能在您的简约工具或服务中省略层通常会使数据层更靠近(用户)界面。如果您正在处理 API,您的实现将需要版本更新,该更新需要在您的所有客户端之间分发。你可以在一次咖啡休息时间这样做吗?

“让我们做起来又快又简单,只是暂时......”

你的工作是“暂时”吗?开个玩笑;-) 但是;你打算什么时候修?可能当您的技术债务迫使您这样做时。那时,它比这短暂的茶歇花费你更多。

“‘封闭修改,开放扩展’呢?这也是一个SOLID原则!”

是的,是的!但这并不意味着您不应该修复错字。或者每个应用的业务规则都可以表示为扩展的总和,或者不允许您修复被破坏的东西。或者正如Wikipedia 所说:

如果一个模块可供其他模块使用,则称该模块已关闭。这假设已经给模块一个定义明确、稳定的描述(信息隐藏意义上的接口)

实际上促进了层的分离。


现在,一些典型的场景:

ASP.NET MVC


因为,这是您在实际问题中使用的:

让我举个例子。想象一下下面的视图模型和领域模型:

注意:这也适用于其他层类型,仅举几例: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 具有嵌套属性,您的视图仅显示“流派名称”

我在真实的场景中看到过这种情况。

在这种情况下,一个常见的问题是使用@Html.EditorFor 会导致嵌套对象的输入。这可能包括Ids 和其他敏感信息。这意味着泄露实现细节!您的实际页面与您的域模型相关联(可能与您的数据库相关联)。完成本课程后,您会发现自己正在创建 hidden 输入。如果您将其与服务器端模型绑定或自动映射器结合使用,则使用 firebug 等工具阻止对隐藏的Id 的操作变得越来越困难,或者忘记在您的属性上设置属性,将使其在您的视图中可用。

虽然阻止其中一些字段是可能的,也许很容易,但是您拥有的嵌套域/数据对象越多,正确处理这部分就越难。和;如果您在多个视图中“使用”这个域模型怎么办?他们的行为会一样吗?另外,请记住,您可能希望更改 DomainModel 的原因不一定针对视图。因此,随着 DomainModel 的每一次更改,您应该意识到它可能会影响控制器的视图和安全方面。

3) 在 ASP.NET MVC 中,通常使用验证属性。

您真的希望您的域包含有关您的视图的元数据吗?或者将视图逻辑应用于您的数据层?您的视图验证是否始终与域验证相同?它是否具有相同的字段(或者其中一些是串联的)?它是否具有相同的验证逻辑?您是否正在使用您的域模型跨应用程序?等等

我认为很明显这不是要走的路。

4) 更多

我可以给你更多的场景,但这只是一个更有吸引力的品味问题。在这一点上,我只希望你能明白这一点:) 不过,我答应了一个插图:

现在,对于真正肮脏和快速获胜的人来说,它会起作用,但我认为你不应该想要它。

构建视图模型需要更多的努力,通常与域模型有 80+% 的相似性。这可能感觉像是在做不必要的映射,但是当出现第一个概念差异时,您会发现这是值得的 :)

因此,作为替代方案,我建议针对一般情况采用以下设置:

  • 创建视图模型
  • 创建域模型
  • 创建数据模型
  • 使用像automapper 这样的库来创建从一个到另一个的映射(这将有助于将Foo.FooProp 映射到OtherFoo.FooProp

好处是,例如;如果您在其中一个数据库表中创建一个额外的字段,它不会影响您的视图。它可能会影响您的业务层或映射,但它会停止。当然,大多数时候您也想更改视图,但在这种情况下,您需要。因此,它将问题隔离在代码的一部分中。

Web API/数据层/DTO

首先要注意:here's 一篇很好的文章,关于如何在某些场景中省略 DTO(它不是视图模型)——我务实的一面完全同意 ;-)

另一个在 Web-API / ORM (EF) 场景中如何工作的具体示例:

这里更直观,特别是当消费者是第三方时,您的域模型不太可能与消费者的实现相匹配,因此视图模型更有可能是完全自包含的。

注意名称“域模型”,有时与 DTO 或“模型”混用

请注意在 Web(或 HTTP 或 REST)API 中;通信通常由数据传输对象 (DTO) 完成,这是在 HTTP 端点上公开的实际“事物”。

那么,您可能会问,我们应该将这些 DTO 放在哪里。它们在域模型和视图模型之间吗?嗯,是;我们已经看到,将它们视为viewmodel 会很困难,因为消费者可能会实现自定义视图。

DTO 能否取代domainmodels 或者他们有理由独立存在?一般来说,分离的概念也适用于DTO'sdomainmodels。但话又说回来:你可以问问自己(这就是我倾向于务实的地方);域内是否有足够的逻辑来明确定义domainlayer?我想你会发现,如果你的服务变得越来越小,实际的logic,它是domainmodels 的一部分,也会减少,可能会一起被忽略,你最终会得到:

EF/(ORM) EntitiesDTO/DomainModelConsumers


免责声明/注意

正如@mrjoltcola 所说:还需要牢记组件过度工程。如果以上都不适用,并且用户/程序员是可以信任的,那么你就可以开始了。但请记住,由于 DomainModel/ViewModel 混合,可维护性和可重用性会降低。

【讨论】:

  • 这是我读过的关于堆栈溢出的最佳答案之一。这是犯罪,自 2014 年以来只有 34 次投票!我来这里是为了寻找普遍适用的模式来映射不同场景的视图对象。 1. 单视图对象到单域对象。 2. 多个视图对象到单个域对象。 3. 单一视图对象到多个相关和(可能暂时依赖的)域对象。有什么想法吗?
  • 您好,谢谢。我认为场景 1 几乎涵盖了这里。场景 2 几乎相同。因为不会混合图层,我假设视图模型没有绑定在一起。对于场景 3,尤其是与场景 2 结合使用时,您可能需要查看:它隔离了您的数据检索,防止了可能的副作用。免责声明:我仍然倾向于“过度设计”事情 ;-) blogs.cuttingedge.it/steven/posts/2011/…
  • 是的,我的个人项目就是这样。疯狂地想到你在 2014 年做了所有这些事情。Java 世界仍在迎头赶上。
【解决方案2】:

意见因技术最佳实践和个人偏好而异。

在您的视图模型中使用域对象,甚至使用域对象作为您的模型并没有错误,很多人都这样做。有些人强烈希望为每个视图创建视图模型,但就个人而言,我觉得许多应用程序都被开发人员过度设计,他们学习并重复他们熟悉的一种方法。事实上,有几种方法可以使用较新版本的 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)
{
    // ...
}

第一个示例是在整个应用程序中强制实施多租户安全的好方法。第二个示例允许自定义每个操作。

在您的方法中保持一致,并为其他开发人员清楚地记录您项目中使用的方法。

我建议您始终将视图模型用于登录/配置文件相关功能,以强制自己“编组”网络控制器和数据访问层之间的字段作为安全练习。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-26
    • 2012-08-19
    • 2017-03-21
    • 1970-01-01
    • 2017-08-04
    相关资源
    最近更新 更多