EF实体 vs. Service模型 vs. View模型 (MVC)

24

我正在尝试理解和找出设计应用程序/领域模型(POCOs / DTOs)的良好实践。

假设我有以下数据库表,Account:

UserID int
Email varchar(50)
PasswordHash varchar(250)
PasswordSalt varchar(250)

当然,EF4会像这样构建实体:

public class Account
{
    public int UserID { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }
    public string PasswordSalt { get; set; }
}

现在,假设我有一个用于注册新用户的视图模型,可能长这样:

public class RegistrationViewModel
{
    public string Email { get; set; }
    public string Password { get; set; }
}

最后,我有一个需要注册用户的服务:

public class RegistrationService
{
    public void RegisterUser(??? registration)
    {
        // Do stuff to register user
    }
}

我正在尝试弄清楚在RegisterUser方法中传入什么参数。视图模型当然位于我的Web应用程序(表示层)下,因此我不想将其传递到我的服务中。

因此,我考虑了以下四种可能性:

1) 设置一个与RegistrationViewModel类似甚至相同的服务模型,并使用它:

public class RegistrationServiceModel
{
    public string Email { get; set; }
    public string Password { get; set; }
}

public class RegistrationService
{
    public void RegisterUser(RegistrationServiceModel registration)
    {
        // Do stuff to register user
    }
}

2) 设置一个模型的接口,并在我的视图模型中继承它,然后设置我的方法来接受这个接口:

public interface IRegistrationModel
{
    string Email;
    string Password;
}

public class RegistrationServiceModel : IRegistrationModel
{
    public string Email { get; set; }
    public string Password { get; set; }
}

public class RegistrationService
{
    public void RegisterUser(IRegistrationModel registration)
    {
        // Do stuff to register user
    }
}

3)在我的控制器中传递Account实体,通过RegistrationViewModel-to-Account映射进行操作:

public class RegistrationService
{
    public void RegisterUser(Account account)
    {
        // Do stuff to register user
    }
}

4) 将我的视图模型从展示层移到域/服务层,并将其传递到服务方法中:

public class RegistrationService
{
    public void RegisterUser(RegistrationViewModel account)
    {
        // Do stuff to register user
    }
}

这三种情况都不是很理想,因为我在每一种情况下都看到了问题。 所以我想知道是否有其他我没想到的方法。

针对这个问题有什么好的做法吗?

提前感谢。


顺便问一下,Jerad,你认为第三种情况的问题是什么? - Vasiliy R
@Vasilio Ruzanni - 我的问题(以及我听到其他人说的)与该选项有关,是因为您正在紧密地耦合服务/业务层与数据模型(EF4实体),这些可能比业务/域模型更频繁地更改(对于术语混合造成的困扰,我仍无法掌握其中的差异)。 - Jerad Rose
好的,我看到你的问题是,你试图解决一个“可能出现但实际上不存在”的问题。你必须在复杂性和可用性之间做出平衡的决定。当然,你的RegistrationModel方法将所有内容都抽象化了,但这并不会使你的系统更易于维护(这是过度工程化的信号)。 - Vasiliy R
1
简而言之,不要过度设计,你的EF实体是你的领域模型的核心!领域服务也是你的领域的一部分。不要将领域模型隐藏在领域模型中。如果你改变了你的实体,你必须改变服务,因为你的实体和服务在概念上是相互关联的。因此,我认为RegistrationService.RegisterUser(Account account, string password)方法已经足够理想了(是的,重点在于“足够”)。 - Vasiliy R
@Vasilio - 谢谢,我慢慢意识到你的意思了。我已经开始使用AutoMapper这条路线,看起来很有道理。感谢你的帮助! - Jerad Rose
3个回答

9

您不应该将视图模型传递给服务。服务甚至不知道您在表示层中定义的视图模型的存在。服务与领域模型一起工作。
使用AutoMapper来映射视图模型和领域模型之间的转换。

就我个人而言,在DDD中从未听说过服务模型(用于服务的视图模型)。


我的术语可能不准确。那么,在DDD中,我不应该采取这种方法吗?还是说你只是在说,在DDD中它们不叫“Service Models”? - Jerad Rose
这个特定情况下唯一的问题是Domain对象没有明文密码属性,因此无法映射。 - Yakimych
@Jerad,是的,请不要采取那种方法。 - šljaker
1
@Yakimych,你可以映射你所拥有的内容...然后调用另一个服务以获取密码哈希和密码盐,然后将修改后的域对象发送到注册服务。自动映射器不是必需的。如果在这种特定情况下手动映射更容易,那么你可以进行手动映射。 - šljaker
知道这已经过时了,但我有一个关于这个解决方案的问题:在我们的领域模型中,我们对构造函数应用了几个验证,以确保对象状态。并不总是ViewModels拥有我们需要创建整个实体所需的所有数据,因此我可以将其传递给服务。在这种情况下,我们该怎么办?我总是遇到这个问题,直到现在,我使用“ApplicationServices”,因为它们位于App域上。但是经过一段时间,这变得不太理想...它看起来很奇怪。 - jpgrassi
显示剩余2条评论

9
请务必使用第三个选项。正如šljaker所说,服务应该不知道应用程序的演示部分(其中您的ViewModel是其中的一部分)。
当然,也不要通过包含大量转换模型(如RegistrationServiceModel)或更糟糕的是IRegistrationModel来使事情过于复杂化(最后一个将在某一天导致“接口爆炸”)。
所以:
  1. 拥有一个域实体(POCO实体,使用Entity Framework、NHibernate、NoRM或其他方式持久化)。
  2. 拥有一个ViewModel,在给定上下文中表示您的领域模型。如果需要,不要犹豫地为每个控制器操作创建一个 ViewModel。严格遵守 ViewModel(与 View 一一对应的 ViewModel)的副作用好处是完全消除了过度发布和欠发布问题。但这取决于您具体的情况/品味。
  3. 在您的ViewModel中使用 DataAnnotation 属性提供基本验证(记得验证业务规则,但它应该位于服务/存储库层内部)。
  4. 不要让 App Service 知道 ViewModel。相反,创建一个领域实体实例并将其馈送到服务中(以进行验证/持久化)。
  5. 使用 AutoMapper 作为快速从领域实体映射到 ViewModel 的选项。
  6. 在控制器操作或自定义 IModelBinder 中将传入的 ViewModelFormCollection 映射到您的实体。
  7. (可选)我建议遵循Thunderdome Principle。这是使用 ViewModel 的一个非常方便的用法。

1
@Yakimych,在我写评论之前我没有看到它。关于AutoMapper - 您可以使用AutoMapper进行自定义投影。此外,据我所理解,这个问题本质上是关于在常见情况下使用ViewModel的“最佳实践”。因此,“Account”和“UserService”只是一个例子。在大多数情况下,AutoMapper确实是避免样板代码的方法。 - Vasiliy R
@Vasilio - 我大部分同意你的观点,是的,你可以调整automapper以将其投影到不同的属性上。但是在这种特殊情况下,根本没有明文密码的属性。实际上,您需要运行业务逻辑以生成盐并散列密码,因此您需要在服务层的某个地方为明文密码腾出空间。在我看来,在这种情况下,DTO效果最佳。关于其他方面,正如我所提到的,我大部分同意。 - Yakimych
@Vasilio - 好的,那么如果没有使用DTO,你会如何解决这个特定问题呢?我的意思是,你会如何将明文密码传递到服务层? - Yakimych
1
@Yakimych,MembershipService.RegisterUser(user, password) - Vasiliy R
@Vasilio - 是的,在这种情况下,这可能是更好的解决方案。只是作为一个注意事项,当您有更多需要传递的参数时(例如在此情况下的密码),这将无法扩展。 - Yakimych
显示剩余2条评论

3
在这种情况下使用DTO(数据传输对象)非常合理。您可以在服务层创建一个AccountDto类,并将其用于向服务传递注册数据。在某些情况下,它可能类似于ViewModel,但通常您可以在View中显示比创建用户所需的更多内容。为了进一步说明这一点,您的ViewModel可能至少看起来像这样:
public class RegistrationViewModel
{
    [Required]
    public string Email { get; set; }

    [Required]
    public string Password { get; set; } 

    [Required]
    [Compare("Password")]
    public string RepeatPassword { get; set; } 
}

虽然您的 DTO 只需要 EmailPassword 属性。

public class AccountDto
{
    public string Email { get; set; }
    public string Password { get; set; }
}

正如您所看到的,ViewModel仅包含View所需的数据。电子邮件验证和密码比较逻辑发生在Web层中。您使用DTO仅获取电子邮件和密码以供服务使用。然后在服务层中,您对密码进行哈希处理,填充实体对象并将值持久化到数据库中。


2
AccountDto真的有必要吗?通常,在注册后,用户会被重定向到个人资料页面。这意味着您需要在AccountDto中包含UserID,并执行以下操作:RegistrationViewModel->AcountDto->Acount->将值持久化到数据库->Acount->AcountDto->返回控制器。看起来有点过度设计了。 - šljaker
1
@šljaker - 这并非必须,但在业务逻辑需要从ViewModel中填充部分领域对象的情况下,这是完全合理的。关于另一个声明,我不认为使用Dto来获取帐户有任何理由。Account account = accountService.RegisterAccount(accountDto);- 然后您就可以获得您的领域对象。它包含了您所需的一切。 - Yakimych
1
@Yakimych 所以你需要将注册视图模式转换为AccountDTO?而在服务层中,您必须将帐户DTO转换为帐户域模型?是这样吗? - Adi Sembiring

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接