DDD - 实体状态转换

8
考虑以下简化的例子:
public class Ticket
{
   public int Id;
   public TicketState State;

   public Ticket()
   {
      // from where do I get the "New" state entity here? with its id and name
      State = State.New;
   }

   public void Finished()
   {
      // from where do I get the "Finished" state entity here? with its id and name          
      State = State.Finished;
   }
}

public class TicketState
{
   public int Id;
   public string Name;
}

类状态直接在领域对象ticket中使用。在票的生命周期后期,可能设置其他状态。

票据被持久化到Ticket表中,以及TicketState。因此,在数据库中,票将具有指向票状态表的外键。

当在实体中设置适当的状态时,如何从数据库加载状态实例?我需要将仓库注入到实体中吗?我需要使用像Castle这样的框架吗?还是有更好的解决方案,比如从外部传递状态?

public class Ticket
{
   //...
   public ITicketStateRepository stateRep; //<-- inject

   public Ticket()
   {
      State = stateRep.GetById(NEW_STATE_ID);
   }
   //...
}

有没有最佳实践?到目前为止,我没有使用任何依赖注入框架或其他任何东西,并将任何持久性事物保留在我的领域之外。

另一种方法:

public class Ticket
{
   //...

   public Ticket(NewTicketState newTicketState)
   {
      State = newTicketState;
   }
   public void Finished(FinishedTicketState finishedTicketState)
   {
      State = finishedTicketState;
   }
   //...
}
3个回答

4

票据不会与存储库有任何关联。它将与TicketState具有一对一的关系,而TicketRepository只需执行JOIN操作并将值映射到Ticket中。

当我创建模型对象时,通常不会让它们知道它们是否持久化,因此它们不会被注入存储库。存储库处理所有CRUD操作。

有些人反对这种方法,认为它会导致贫血领域模型;也许你是其中之一。如果是这种情况,请将存储库注入到您的Ticket对象中,但简单地要求它执行JOIN操作并返回一个填充了状态的Ticket。在插入或更新时,必须将两个表作为一个单元进行修改,因此请确保打开事务。

我喜欢将CRUD操作放在域模型对象之外的原因是,它通常不是唯一参与用例或事务的域对象。例如,也许您简单的“购买门票”用例会有一个Ticket对象,但还可能需要处理预订、座位、总分类帐、行李库存和各种其他事情的其他对象。你真的想将几个模型对象持久化为单个工作单元。只有服务层才知道模型对象何时独立运作,何时是更大、更宏伟计划的一部分。
更新:
我不喜欢使用DAO注入模型对象来处理持久性职责的想法的另一个原因是,它会破坏层次结构并引入循环依赖关系。如果保持模型干净,没有任何对持久性类的引用,你可以在不调用其他层的情况下使用它们。这是一种单向依赖关系;持久性知道模型,但模型不知道持久性。
将持久性注入模型中,它们就彼此相互依赖。你不能使用或测试其中任何一个而不另一个。没有分层,没有关注点分离。

+1 对于持久性无知对象。仅仅因为领域模型被污染了存储库对象,并不意味着它是贫血的 - 实际上,这可能隐藏了领域对象没有承担其责任的地方。 - David Hall
谢谢,但也许我的问题没有表达清楚。我想问的是如何在例如票据实例化或其状态更改时设置适当的状态实体。您能否举个例子说明您将如何解决上述问题? - Chris
实例化状态很容易:它应该是NEW。必须有一些东西来协调事件以更改其状态。我通常称之为服务,因为它实现了特定的用例。它将拥有一个存储库实例,它将用于实例化新票证或读取现有票证,将其状态更改为满足用例,将新状态作为单个工作单元持久化,并结束用例。 - duffymo
@David Hall - 的确,一个强大的领域模型对象可能不需要负责其持久状态。有比 CRUD 操作更丰富和有趣的行为。 - duffymo

1

这个答案希望能够延续duffymo的观点。

在DDD的世界观中,你的TicketState是Ticket聚合根的一部分。

因此,你的TicketRepository处理Tickets和TicketStates。

当你从持久层检索一个Ticket时,你允许你的TicketRepository从数据库中检索状态并将其正确地设置在Ticket上。

如果你正在创建一个新的Ticket,那么(我认为)你不需要立即接触数据库。当Ticket最终被持久化时,你会将Ticket的New状态正确地持久化。

你的领域类不应该知道任何关于处理状态的数据库模型,它们只需要知道关于状态的领域模型视图。你的仓库负责这种映射。


但是,如果我实例化一个尚未持久化的新票证,并想要在用户界面上显示其状态名称(存储在数据库中),那么我从哪里获取该信息? - Chris
@Chris - 请看下面我的评论。 - duffymo

0
对我来说,在数据库(或任何持久性介质)中表示状态的简单键值对不需要在域中建模。在域中,我会将TicketState建模为枚举,并让ITicketRepository负责知道如何将其映射到数据库模式的要求。
在票务库中,您可以有一个基于TicketState键入的票务状态ID缓存,它从数据库中惰性加载到静态变量中(只是一种方法)。票务库将Ticket.State值映射到该缓存中的ID以进行插入/更新。
namespace Domain {
  public class Ticket {
    public Ticket() { State = TicketStates.New; }
    public void Finish() { State = TicketStates.Finished; }
    public TicketStates State {get;set;}
  }

  public enum TicketState { New, Finished }
}

namespace Repositories {
  public class SqlTicketRepository : ITicketRepository {
    public void Save(Ticket ticket) {
      using (var tx = new TransactionScope()) { // or whatever unit of work mechanism
        int newStateId = TicketStateIds[ticket.State];
        // update Ticket table with newStateId
      }
    }
  }

  private Dictionary<TicketState, int> _ticketStateIds;
  protected Dictionary<TicketState, int> TicketStateIds{
    get {
      if (_ticketStateIds== null) 
        InitializeTicketStateIds();
      return _ticketStateIds;
    }
  }

  private void InitializeTicketStateIds() {
    // execute SQL to get all key-values pairs from TicketStateValues table
    // use hard-coded mapping from strings to enum to populate _ticketStateIds;
  }
}

就目前模型而言,我同意这一点 - 但是,如果状态转换需要发展以便能够表达业务逻辑,那么拥有一个状态实体就会变得非常有意义。 - David Hall
但是如果我实例化了一张新的票,它还没有被持久化,并且想要在用户界面上显示它的状态名称(存储在数据库中),那么我从哪里获取这些信息呢? - Chris
当你实例化一个对象并在显示到用户界面之前,很容易将其持久化。当持久化对象的状态尚未保存时,为什么要显示它的状态?在任何操作完成之前,您都不会向用户显示任何操作结果,而且只有在持久化完成后才算完成。 - duffymo
1
我的回答假定数据库中的名称是存储级别的细节。如果这些确切的名称实际上是演示数据,那么你可以选择duffymo和David Hall的答案。然而,除非应用程序必须能够修改这些字符串值,否则我实际上更喜欢将它们放入资源(resx)文件中,无论是在域层还是演示层中。 - G-Wiz

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