众所周知,服务层负责更新(当然还有读取、写入和删除)模型(或者我可以称它们为实体)。让我们暂时忽略存储库层,因为我不喜欢它,它只是隐藏了很多实体框架功能。
设计良好的系统中的数据流应该是:
Service (model) <<<-->>> Controller (mapping Model <-> ViewModel)
为了能够通过上述数据流更新数据库中的模型,我必须从数据库到服务再到控制器读取整个模型,并使用AutoMapper(例如)将从ViewModel获得的数据映射到模型(现在我们有了一些更改值的旧模型)。现在,我可以将编辑后的模型传递回服务,并执行DbContext.Update(Model)
。
这样做的缺点是:
我们必须发出额外的读取查询来读取整个模型(我们必须这样做,否则,
DbContext.Update(Model)
会将 none 映射属性保留为默认值)。此外,已为整个模型生成更新查询(尽管我可能只更改了模型的一小部分。
我有时会发现设计模式会强制隐藏许多功能,这可能会使程序更有效率。
是否有任何方法(或者让我们说设计模式或任何对服务模式的编辑)我可以将 ViewModel 映射到模型,并将模型传递给服务,并仅更新映射的属性(因此无需在映射属性之前读取整个实体)?
我的回答可能有点偏离主题,但我还是发布了它,因为我觉得我想说的是一个有点广泛的评论话题。
你说
众所周知,服务层负责更新(当然还有读取、写入和删除)模型(或者我可以称之为实体)。
问题是,该服务层实际上不应该像这样使用。我的意思是 - 你根本不应该有所谓的服务。你所描述的被称为贫血域模型。它是实体和服务的组合,其中实体是简单的数据结构(不是正确的对象!),服务用于在实体之上执行操作。
用马丁·福勒的话说
[...]有一组服务对象捕获所有域逻辑,执行所有计算并使用结果更新模型对象。这些服务位于域模型之上,并将域模型用于数据。
这很糟糕。
将方法和数据组合在一个对象中。然后,您将要寻找的是工作单元设计模式。事实上,实体框架已经实现了此模式。至于使用 EF(或者实际上您可以在 .NET 中使用的任何 ORM)实现这种富对象方法,我推荐 Vaughn Vernon 关于使用 EF 设计聚合的文章。总结整篇文章是不可能的,但为了避免仅链接的答案:基本上 Vaughn 提出了两种方法:创建带有实现类和域对象的分离接口,由他称之为状态对象的东西备份。
要处理评论: 基本上是的 - 建议是将数据和方法放在一起,而不是有一个所谓的实体(在大多数情况下仅用于从数据库获取数据的简单数据结构)和称为服务的单独类,它对实体执行操作。
尽管这可能是好主意和有趣的想法,但这仍然是理论上的,对吧?第二个环节是将这种方法付诸实践。当然,当你开始重构你的代码,从贫乏的领域模型转向富域模型时,你必须问自己一个问题——如何将我的富对象存储在数据库中?我的意思是 - 这样做的全部目的是将业务(领域)逻辑封装在对象中,而不必处理所有这些技术混乱。我们的目标是尽可能纯粹的 C# 对象含义 - 尽可能减少依赖关系。让我们谈谈例子。想象一下,你有Order
和Product
课。
[Table("orders")]
class Order
{
[Key]
public int Id { get; set; }
public List<Product> Products { get; set; }
// rest omitted for clarity
}
[Table("products")]
class Product
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
// rest omitted for clarity
}
使用贫血域模型,您将让服务执行诸如
class OrderService
{
public void AddProduct(int orderId, int productId)
{
Order order = this.orderRepository.FirstOrDefault(orderId);
Product product = this.productRepository.FirstOrDefault(productId);
if(!order.Products.Contains(product)
{
order.Products.Add(product);
}
this.orderRepository.Save(order);
}
}
使用丰富的域模型,您将拥有这样的类
[Table("orders")]
class Order
{
[Key]
public int Id { get; set; }
public List<Product> Products { get; set; }
public bool AddProduct(Product product)
{
if(this.Products.Contains(product))
{
return false;
}
order.Products.Add(product);
return true;
}
}
此类将在控制器中使用,例如
class OrderController
{
public ActionResult AddProduct(int orderId, int productId){
Order order = this.orderRepository.FirstOrDefault(orderId);
Product product = this.productRepository.FirstOrDefault(productId);
bool productAdded = order.AddProduct(product);
// do something else
}
}
请注意两件事: 1.我们将技术内容转移到控制器中,并将业务逻辑封装Order
类中(让我们假装if(this.Products.Contains(product)
是我们唯一的业务规则)[好事] 1.Order
类应该只有与业务相关的东西。在阅读和理解这个类时,特定于ORM的注释只引入了技术噪音,更不用说我们的模型中有不必要的依赖关系[坏事]
沃恩讨论了处理这个问题的两种方法。您可以提取与您的业务方法的单独接口
interface IOrder
{
bool AddProduct(Product product);
}
然后像class Order : IOrder
一样在您的类中实现它。链接的博客文章中指出了此解决方案的缺点
无处不在的语言并没有通过使用IProduct,IBacklogItem等接口来真正得到加强,IProduct和IBacklogItem不是我们的通用语言,但产品和BacklogItem是。因此,面向客户端的名称应该是产品、积压项等。我们可以简单地通过命名接口 Product、BacklogItem、Release 和 Sprint 来实现这一点,但这意味着我们必须为实现类提供合理的名称。让我们暂停一下,继续讨论第二个相关问题。
和
创建分离接口确实没有充分的理由。我们不太可能创建两个或多个IProduct或任何其他接口的实现。我们创建分离接口的最佳原因是何时可能存在或有多个实现,并且不会在此核心域中发生。
另一种解决方案是创建两个对象 - 一个仅用于业务逻辑,另一个负责更改跟踪并用于与数据库通信。使用这种方法,您就拥有了业务对象,从业务角度来看,没有任何无关紧要的迹象。正如沃恩总结的那样
最后,我们的目标是远离实体框架
这确实是一个广泛的主题。很难尝试以如此的方式解释它 - 有关于这个主题的整本书;-)。一般来说,我会推荐你阅读弗农的书,书名叫《实现领域驱动设计》。它是关于DDD的,但它也展示了如何编写正确的面向对象的代码。