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's 和domainmodels。但话又说回来:你可以问问自己(这就是我倾向于务实的地方);域内是否有足够的逻辑来明确定义domainlayer?我想你会发现,如果你的服务变得越来越小,实际的logic,它是domainmodels 的一部分,也会减少,可能会一起被忽略,你最终会得到:
EF/(ORM) Entities ↔ DTO/DomainModel ↔ Consumers
免责声明/注意
正如@mrjoltcola 所说:还需要牢记组件过度工程。如果以上都不适用,并且用户/程序员是可以信任的,那么你就可以开始了。但请记住,由于 DomainModel/ViewModel 混合,可维护性和可重用性会降低。