MVC / MVVM中的ViewModel /层分离 - 最佳实践?

问题描述 投票:22回答:2

我对使用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
}
c# asp.net-mvc asp.net-mvc-viewmodel
2个回答
31
投票

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

tl;博士

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

基本上不是因为你实际上混合了两层并将它们捆绑在一起。我必须承认,我看到它发生了很多,这取决于你的项目的快速获胜水平,但我们可以说它不符合SOLID的单一责任原则。


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


让我们考虑一下不混合各层的优点和缺点。

它会花多少钱

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

  • 重复的代码
  • 增加额外的复杂性
  • 额外的性能打击

你会得到什么

总有一场胜利,我总结一下,稍后解释,并说明为什么这才有意义

  • 独立控制层

The costs


duplicate code

这不是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%的模型将重叠。抓一杯咖啡需要更多时间;-)

“它需要更多的维护”

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

adds extra complexity

不,不是的。它增加了一个额外的层,使其更加复杂。它不会增加复杂性。

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

“飞机是一件非常复杂的事情。坠落的飞机非常复杂。”

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

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

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

extra performance hit

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

The win


independent control of the layers

这是什么意思?

这个(和更多)的任何组合:

  • 创建一个可预测的系统
  • 在不影响UI的情况下改变您的业务逻辑
  • 改变您的数据库,而不会影响您的业务逻辑
  • 改变你的ui,而不影响你的数据库
  • 能够更改您的实际数据存储
  • 完全独立的功能,隔离良好的可测试行为,易于维护
  • 应对变化并赋予企业权力

本质上:你可以通过改变一个明确定义的代码片段来改变,而不必担心令人讨厌的副作用。

beware: business counter measures!

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

变化将来临: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) The domain model changes.

在这种情况下,您可能还需要调整视图,这在关注点分离的背景下是不好的做法。

如果已将ViewModel与DomainModel分开,则对映射(ViewModel => DomainModel(和back))进行微调就足够了。

2) The DomainClass has nested properties and your view just displays the "GenreName"

我在实际场景中看到过这个问题。

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

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

3) In ASP.NET MVC it is common to use validation attributes.

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

我认为这显然不是采取的路线。

4) More

我可以给你更多的场景,但这只是一个品味更具吸引力的问题。我只希望在这一点上你能得到点:)尽管如此,我答应了一个例子:

Scematic

现在,对于非常肮脏和快速的胜利,它会起作用,但我认为你不应该想要它。

构建视图模型只需要多一点努力,通常与域模型类似,为80 +%。这可能感觉就像做了不必要的映射,但是当出现第一个概念差异时,你会发现它值得付出努力:)

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

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

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

Web API / data-layer / DTO

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

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

Web Api Datalayer EF

注意:名称“域模型”,也可以称为DTO或“模型”

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

所以,我们应该在哪里放置这些DTO,你可能会问。它们是域模型还是视图模型之间?嗯,是;我们已经看到将它们视为viewmodel会很难,因为消费者可能会实施定制视图。

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

EF/(ORM) Entities↔↔DTO Consumers


免责声明/注意

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


16
投票

从技术最佳实践和个人偏好的混合看,意见各不相同。

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

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.