将DTO接口传递到领域工厂中?

3
我将尝试在当前项目中应用DDD的原则。我会通过一个例子来问一个(冗长的)问题,希望能让您理解。

当创建一个新成员时,我的表示层调用了定义在应用层中的MemberService.CreateMember(MemberDTO memberDTO)。

我的表示层大致如下:

MemberDTO member = new MemberDTO(); //Defined in Application Layer

member.Username = username;
member.Password = password;
//...etc

我的应用层在领域层中调用以下工厂方法来创建成员:

public static Member MemberFactory.CreateMember(string memberDTO.Username, string memberDTO.Password...)
{
  var member = new Member(); //Domain.Model.Member

  member.Id = GenerateIdentity();

  member.Username = memberDTO.Username;

  //... etc

  return member;
}

该Member被传回到MemberService(应用层),该层将其保存(基础架构层中的Repository)并映射为MemberDTO(使用AutoMapper),然后将其传回到Presentation Layer。

因此,我的Presentation Layer正在设置MemberDTO中的值,然后我的Domain Layer(通过Factory)正在接受单个参数并设置Member的值。如果没有工厂,这将是简单的,但我在这里生成Id。创建一个Domain Service来生成Id是否有错?例如,将MemberService.CreateMember(MemberDTO memberDTO)方法更改为:

public MemberDTO CreateMember(MemberDTO memberDTO)
{
   var member = MemberFactory.CreateMember(memberDTO.Username, memberDTO.Password); 
   //Domain.Model.Member

   SaveMember(member);

   //Pass DTO to presentation layer
   return Mapper.Map<Member, MemberDTO>(member);
}

变成这样:

public MemberDTO CreateMember(MemberDTO memberDTO)
{
   var member = new Member(); //Domain.Model.Member

   Mapper.Map<MemberDTO, Member>(memberDTO);

   //Add this method into a Domain Service to generate the ID and any other defaults
   Domain.MemberService.Initialise(member);

   SaveMember(member);

   //Pass DTO to presentation layer
   return Mapper.Map<Member, MemberDTO>(member);
}

非常抱歉提出冗长的问题,尽管答案可能是简单的是或否!

3个回答

2
关于DDD,如果此身份不是域身份(即基于有界上下文定义用户的内容),则它不属于您的领域模型。在我看来,大多数情况下,用户名称是有界上下文中用户的标识,并且任何生成的ID都将用于数据库访问。我怀疑这可能是您的情况,如果我是正确的,那么生成的ID是基础设施问题,应该由您的存储库实现完成。您的领域不应该有任何关于此生成的ID的概念。
另一方面,假设用户名是您上下文中用户的身份,并且您想使用某些域公式生成该身份。在这种情况下,生成身份将是域服务方法,因为它实际上不属于您的任何领域对象实例。然后将传递生成的ID到您的工厂中,您将拥有:
public MemberDTO CreateMember(MemberDTO memberDTO)
{
   //Domain.Model.Member
   var member = MemberFactory.CreateMember(MemberService.GenerateUserName(), memberDTO.Password); 

   SaveMember(member); //Repository method?

   //Pass DTO to presentation layer
   return Mapper.Map<Member, MemberDTO>(member);
}

总之,答案是否定的,假设ID是限界上下文的实际身份,创建一个域服务来生成ID不会有错。事实上,这就是你应该做的。然而,如果ID不是限界上下文用户的身份,则让您的存储库担心如何生成ID,访问数据库并转换到/从域模型。

谢谢@Aaron,那很有道理。我只是在想,MemberFactory是不是多余的,因为我正在将DTO中的各个参数传递到工厂,并手动编写将这些参数映射到领域模型的代码。 - Alan Marsh
为了生成一个ID,我同意你的观点。但是,你应该保留MemberFactory以实现封装的目的。无论构建Member的代码有多复杂,它都允许你将成员构造函数设置为私有,这样必须使用MemberFactory来获取Member的实例。这使你可以验证输入并在创建Member实例之前潜在地运行其他检查(如密码规则)。 - Aaron Hawkins
有效的观点,在这个特定的工厂中,我传递了12个参数,而且还有一些我可以设置默认值的参数,所以我考虑传递一个接口到DTO中,但我不认为这是允许的? - Alan Marsh
你决定什么是允许的,什么是不允许的,但要意识到每个选择都有后果。话虽如此,如果你遵循洋葱架构并希望你的领域独立,那么领域对象不应引用DTOs,因为它们应该在基础设施项目中,并且引用应该被反转(即你的DTO项目引用你的领域)。此外,应用服务应该负责将领域聚合转换为DTO(这也应该在基础设施项目中)。 - Aaron Hawkins

1

关于DDD中的这个问题,我不确定是否需要额外的复杂性,因为我没有看到一个需要额外复杂性的复杂领域。然而,可能有一个论点是将身份生成定义在另一个结构中,但这主要是为了实现单一职责(SRP)。

我建议您创建一个名为IGenerateMemberIdentity的接口。然后,您可以隔离该复杂性并进行测试。我认为您的直觉是这个功能不属于工厂是正确的。如果这是一个复杂的例程,它似乎与对象创建无关。

然而,对我来说,Initialize静态方法似乎像是代码异味。最好通过接口注入依赖项来提供该功能。

具体回答您的问题。不,我不认为这是错误的。事实上,为了保留纯业务功能而建模复杂性正是DDD最擅长的。


感谢 @dtryon,我简化了示例中的领域。唯一的问题是在“转换后:” 部分,我不再使用工厂来创建会员的实例。这就是为什么我认为需要一个类似 Initialise 方法来设置默认值,例如 Member.IsVerified = false; 或者我应该在 Member 构造函数中设置这些默认值? - Alan Marsh
如果 Member.IsVerified 是一个布尔值,它将默认设置为 false。如果您有更复杂的创建需求,我建议您查看像 Builder 这样的设计模式。然而,我建议您将与配置、默认值和身份生成相关的问题分开处理。在处理成员时,这些都似乎是不同的关注点。 - Davin Tryon
谢谢,正如我对Aaron所说的那样,在这种情况下,MemberFactory只是一个开销,因为我将DTO中的单个参数传递到工厂中,并手动将这些参数映射到域模型中。 - Alan Marsh

1
短答案:是的,您可以将DTO对象传递给工厂。
长答案: 域对象工厂负责创建完全初始化的域对象。但只要您能提供所需信息,就可以选择任何形式的输入。在这种情况下,您可以在工厂中拥有以下方法之一,这是个人选择的问题: - public Member CreateMember(string userName, string password, ...) {...} - public Member CreateMember(MemberDTO memberDTO) {...}
从分层的角度来看,域对象工厂是域模型的一部分。因此,它可能被域模型层和应用程序层中的不同对象使用和依赖。从您描述的情况来看,MemberService似乎属于您的应用程序(服务)层。如果您将创建逻辑移动到其中,可能会遇到困难的依赖问题。例如,如果Booking域对象在确认时创建Member域对象,则可能会导致Booking对象依赖于MemberService,这不是一个好主意。该示例在此处说明:http://thinkinginobjects.com/2012/09/05/abstract-factory-in-domain-modelling
如果我们稍微退后一步,从一些角度来看待这个问题,我想问一下MemberDTO和MemberService到底为您的应用程序提供了什么服务。DTO对象是否用作一组数据字段的包装器?MemberService是否只是将创建调用委托给您的工厂和存储库?如果答案是肯定的,如果我们完全摆脱MemberDTO和MemberService,让演示文稿直接调用MemberFactory和MemberRepository,代码会更简单。在这里我做了很多假设,但是如果您的演示层和服务层物理上分离,您可能仍然会发现DTO和Service非常有用。

感谢您的回复@nwang0。该项目是一个企业级应用程序,因此我希望保留MemberService(以及其他聚合服务)。您提到了public Member CreateMember(MemberDTO memberDTO){...},但由于DTO在应用程序层中定义,将其引用到域层是否不正确? (因为那是工厂所在的地方) - Alan Marsh

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