领域模型 vs 数据传输对象 vs 视图模型 - 如何使用它们以及何时使用?

32

在具有领域层(DL)/业务(服务)层(BL)/表示层(PL)的多层项目中,向表示层传递实体的最佳方法是什么?

DO => Domain Object;
DTO = Domain Transfer Object;
VM => View Model;
V => View;

选项1:

DL => DO => BL => DTO => PL => VM => V

这个选项似乎是最佳实践,但同时也很难维护。

选项2:

DL => DO => BL => DTO => PL => V

这种选择似乎不是很好的实践,但由于DTO与VM几乎相同,我们可以直接将其传递给View,并且它更容易实现和维护。

那么对于多个布局,比如对于移动设备,我可能需要从BL获取更少的信息,因此我需要一个不同的VM来适应特定的布局,这种选择是否可靠?

5个回答

13

将DTO传递给视图是可以的。如果需要更改或增强DTO,则创建ViewModel。常见情况是添加链接。ViewModel 引用 DTO 作为复杂属性也是可以的。


谢谢您的回答,但是是否总是使用相同的架构比在某些情况下使用选项1或选项2更好呢? - Patrick
5
我会建议使用正确的技巧来完成手头的任务。我不建议仅仅为了创建一个视图模型而创建视图模型。 - kevin_fitz
2
@Patrick 为了保持一致性,是的。这就是为什么我说“好的”而不是“最佳实践”的原因。我的选择始终是创建 ViewModel 并将 DTO 引用为复杂属性。 - Max Toro
@Max 谢谢,你认为我应该如何在服务层中使用 AutoMapper 初始化映射? - Patrick
1
我强烈推荐Ayende的这个视频http://www.youtube.com/watch?v=0tlMTJDKiug。它肯定会帮助你深入理解概念,对开发和DDD模式可能会产生很大的影响。它不仅仅是几个POCO对象。 - kunjee
1
这绝对是错误的。这是反模式和反世界上所有东西。 - Ashi

4
如果你有不同的视图需要从Dto中获取不同的数据,那么你可能会受益于为这些视图设置不同的视图模型,并将Dto映射到这些视图模型上。
其中一个想法是允许更大的自由度来更改你的视图模型,因为你知道它不会影响应用程序的其他部分。如果你的Dto在多个视图中使用,则每次更改Dto都需要测试每个视图。

1
你可以在某些管理视图中直接使用Dto,或者当它只在一个地方使用时。一旦你看到需要在多个地方使用Dto,就创建视图模型。另一方面,如果你采用领域、Dto、视图模型的方法,创建额外的对象(使用automapper)并不需要太长时间,并且节省了任何决定使用哪个的时间。这使得项目中的其他人更清楚该做什么。在我看来,这可能会在长期内节省更多的时间,并使您的代码更易于维护。只在视图中拥有视图模型有几个好处... - dove
1
一如既往,这取决于您的领域、团队和项目规模。 - dove
1
你可以将Automapper配置文件存储在Dto定义附近,但像其他配置一样,仍然需要在应用程序启动时进行配置。这取决于你。 - dove
1
您可以将个人资料放置在服务层,其中包含映射细节。然而,您还需要在 Windows 服务中调用自动映射配置,只需一条语句或不多于此即可。 - dove
1
现在你需要将AutoMapperConfiguration.Configure()放到Win32应用程序的开头(我原以为这是一个服务)。 - dove
显示剩余5条评论

3
对我来说,这个决定基于验证逻辑的放置位置。
场景#1:在ViewModel(在UI层)中添加数据注释大大简化了编程。在大多数情况下,DTO和ViewModel之间将会有一对一的映射。在这种情况下,选项1是不错的。 DL => DO => BL => DTO => PL => VM => V
场景#2:如果验证未附加到ViewModels,则将DTO替换为ViewModel,并将ViewModel驻留在业务层中。 DL => DO => BL => VM => PL => V
场景#2可以在领域模型包含验证逻辑并且这些模型被许多UI应用程序使用的情况下完美地实现。 UI只列出由业务层给出的错误列表(虽然不太用户友好)。随着应用程序经历业务规则的变化,我们只更改领域模型。同样,如果使用.NET,则可以通过EF自动生成与数据库相关的验证,甚至减少了更改的范围。

1
非常抱歉回复晚了,但感谢您的答复。 - Patrick

2

我用以下方法创建了几个中等规模的应用程序,并且晚上睡得很好 ;-)

实体 <=> 服务 <=> 模型

请注意,这3个层必须由包/模块/程序集分离,并且仅限于后端。

实体: 领域层,领域对象,存储库,数据库模型,数据对象层(DAL)

服务: 业务逻辑层(BLL),业务服务层

模型: 数据传输对象(DTO),演示层,视图模型(请从您的字典中删除演示/视图)

视图是一个单独的项目(PWA,渐进式Web应用程序),它使用API并且不需要任何后端代码支持。它有自己的视图模型和业务逻辑。

例如:

MyProject.Entities (DAL):

[Table("customers")]
public class Customer
{
    // Used to instantiate the proxy by DbContext
    protected Customer() {  }
    
    // Used by developers
    public Customer(string name) : this()
    {
        Id = IdGenerator.Next();
        SetName(name);
    }
    
    [Key]
    public virtual string Id { get; protected set; }

    public virtual string Name { get; protected set; }
    
    [Column("summary")]
    public virtual string Description { get; set; }

    public void SetName(string name)
    {
        if (string.IsNullOrEmpty())
            throw new ArgumentNullException(nameof(name));

        Name = name;
    }
}

我的项目.模型(数据传输对象)

public class IdModel
{
    [Required]
    public string Id { get; set; }
}

public class CreateCustomerModel
{
    [Required]
    [MaxLength(50)]
    public string Name { get; set; }
}

public class EditCustomerModel : IdModel
{
    [Required]
    [MaxLength(50)]
    public string Name { get; set; }

    public string Description { get; set; }
}

我的项目.Services (BLL):

public class CustomerService
{
    private readonly DbContext _db;
    private readonly IMapper _mapper;

    public CustomerService(DbContext db, IMapper mapper)
    {
        _db = db;
        _mapper = mapper;
    }

    public async Task<EditCustomerModel> Get(IdModel model)
    {
        var customer = await _db.Get<Customer>(model.Id);

        return _mapper.Map<EditCustomerModel>(customer);
    }

    public async Task<IdModel> Create(CreateCustomerModel model)
    {
        var customer = new Customer(model.Name);

        await _db.SaveOrUpdate(customer);

        return _mapper.Map<IdModel>(customer);
    }

    public async Task Edit(EditCustomerModel model)
    {
        var customer = await _db.Get<Customer>(model.Id);

        customer.SetName(model.Name);
        customer.Description = model.Description;

        await _db.SaveOrUpdate(customer);
    }

    public async Task Delete(IdModel model)
    {
        await _db.Remove<Customer>(model.Id);
    }
}

上面的例子只是一个简单的CRUD服务。但是同样的方式,您可以实现任何领域服务,例如CustomerInvoiceService

[Description("Creates a new customer and invoices it. Returns invoice Id.")]
public async Task<IdModel> CreateCustomerAndInvoiceIt(CreateCustomerInvoiceModel model)
{
    model.InvoiceForm.CustomerId = await _customerService.Create(model.CustomerForm);
    
    var invoiceId = await _invoiceService.Create(model.InvoiceForm);

    return invoiceId;
}

只需公开 API/服务,让前端开发人员完全独立于后端进行工作。如果是桌面应用程序或前端使用后端语言(如C#/Java/Go),他们必须构建自己的“前端逻辑层和模型”,并且在任何情况下都不能允许将“域/业务层”与“表示层”混合在一起。前端是完全分离的东西,正如有人上面提到的 视图,我建议从BLL字典中完全删除该单词。我的观点

你好,感谢您的回答。您能帮我确定一下您示例中的DTO吗?谢谢。 - Patrick
1
MyProject.Models 是数据传输对象(DTOs)。它们的目的是将数据从一个服务传输到另一个服务(例如 CreateCustomerModel)。MyProject.Entities 是领域对象。它们的目的是保持/持久化业务逻辑的状态。 - ADM-IT
在控制器中,我们向视图发送哪个模型? - Patrick
1
抱歉,但是没有控制器,控制器在MVC中。如果您为自己的需求创建API,则可以使用控制器,但它们并不是必需的。使用最小API方法(https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api),您可以注册`public`服务而无需控制器。在我看来,控制器是多余的。 - ADM-IT
1
但是如果你仍然考虑控制器(即使我说它们增加了额外的复杂性),那么MyProject.Models(DTO)用于在视图和BLL之间传输数据。public async Task<MyResultModel> MyControllerAction(MyFormModel model) { return _businessLogic.Process(model); } - ADM-IT

1

请看我的回复: https://dev59.com/vXI-5IYBdhLWcg3wMFIf#14059156

你说: 这个选项似乎是最佳实践,但维护起来也很麻烦。

或许实现起来不太费劲,只需要复制几行代码就可以了,但维护肯定不容易。


非常抱歉回复晚了,但感谢您的答复。 - Patrick

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