在使用事件源时,我是否应该将域对象用作域事件中的字段

时间:2013-11-06 03:08:53

标签: domain-driven-design cqrs event-sourcing

根据“域事件是域中发生的事件的表示”这一定义,将域对象用作域事件中的字段是很自然的。

使用事件源时,域事件是持久的。因此,如果他们使用域对象作为其字段,则域对象也是持久的。这削弱了采用CQRS& amp;事件采购,使域对象更难以改变和发展。

考虑一下CQR版本的Eric Evans的dddsample,用户故事是:

Given a cargo has been registered
And I request possible routes for the cargo
And some routes are shown
When I pick up a candidate
Then the cargo is assigned to the route


public class Cargo { // This is an aggregate
    private TrackingId trackingId;
    private RouteSpecification routeSpecification;

    public void assignToRoute(final Itinerary itinerary) {
        Delivery delivery = Delivery.derivedFrom(routeSpecification, itinerary);
        apply(new CargoAssignedEvent(this.trackingId, 
            itinerary, delivery.routingStatus()));//sending the domain event
    }
}

public class Itinerary { //This is a value object
    private List<Leg> legs;
}

public class Leg { //Another value object
    private VoyageNumber voyageNumber;
    private UnLocode loadLocation;
    private UnLocode unloadLocation;
    private Date loadTime;
    private Date unloadTime;
}

public class CargoAssignedEvent { // This is a domain event
    private final String trackingId;
    private final RouteCandidateDto route; //DTO form of itinerary containing a List of LegDto s
    private final String routingStatus;

    public CargoAssignedEvent(TrackingId trackingId, Itinerary itinerary,
        RoutingStatus routingStatus) {
        this.trackingId = trackingId.getValue(); //transform to primitive
        this.route = toRoute(itinerary); ////transform to DTO
        this.routingStatus = routingStatus.getCode(); //transform to primitive
    }
    ......
}

正如您所看到的,我使用DTO作为DomainEvent的字段来从事件持久性问题中分离域模型(Itinerary,RoutingStatus)。但这可能会在事件处理方面造成一些不便和麻烦。如果CargoAssignedEvent的某些订阅者需要行程的推导来做出决定,该怎么办?然后我必须将RouteCandidateDto映射到行程。

可能的解决方案是使用域对象作为字段,但在事件存储中引入了一些适配器。在加载或保存事件时,使用适配器映射域对象和dto。

我做得对吗?任何想法都表示赞赏。

更新

行程可能是一个特例。它被视为一个整体值,因此我无法将此值对象拆分为一组较小的域事件,如CargoLegEvent(TrackingId,Leg)。考虑交货案例,交货是货物领域中另一个重要的价值对象,它比行程更丰富:

/**
 * The actual transportation of the cargo, as opposed to
 * the customer requirement (RouteSpecification) and the plan (Itinerary). 
 *
 */
public class Delivery {//value object

  private TransportStatus transportStatus;
  private Location lastKnownLocation;
  private Voyage currentVoyage;
  private boolean misdirected;
  private Date eta;
  private HandlingActivity nextExpectedActivity;
  private boolean isUnloadedAtDestination;
  private RoutingStatus routingStatus;
  private Date calculatedAt;
  private HandlingEvent lastEvent;
  .....rich behavior omitted
}

交货表明货物的当前状态,一旦货物的新处理事件被注册或路线规格发生变化,就会重新计算:

//non-cqrs style of cargo
public void specifyNewRoute(final RouteSpecification routeSpecification) {
     this.routeSpecification = routeSpecification;
     // Handling consistency within the Cargo aggregate synchronously
     this.delivery = delivery.updateOnRouting(this.routeSpecification, this.itinerary);
}

/**
  * Updates all aspects of the cargo aggregate status based on the current
  * route specification, itinerary and handling of the cargo. <p/> When
  * either of those three changes, i.e. when a new route is specified for the
  * cargo, the cargo is assigned to a route or when the cargo is handled, the
  * status must be re-calculated. <p/> {@link RouteSpecification} and
  * {@link Itinerary} are both inside the Cargo aggregate, so changes to them
  * cause the status to be updated <b>synchronously</b>, but changes to the
  * delivery history (when a cargo is handled) cause the status update to
  * happen <b>asynchronously</b> since {@link HandlingEvent} is in a
  * different aggregate.
  */
public void deriveDeliveryProgress(final HandlingHistory handlingHistory) {
    this.delivery = Delivery.derivedFrom(routeSpecification(), itinerary(),
            handlingHistory);
}

我想到我最初需要一个CargoDeliveryUpdatedEvent,如:

//cqrs style of cargo
public void deriveDeliveryProgress(final HandlingHistory handlingHistory) {
     apply(new CargoDeliveryUpdatedEvent(
         this.trackingId, delivery.derivedFrom(routeSpecification(), 
         itinerary(), handlingHistory);
}

class CargoDeliveryUpdatedEvent {
    private String trackingId;
    private DeliveryDto delivery;//DTO ?
}

但最后我发现我可以使用较小的事件来更好地揭示意图,例如:

//cqrs style of cargo
public void deriveDeliveryProgress(final HandlingHistory handlingHistory) {
     final Delivery delivery = Delivery.derivedFrom(
         routeSpecification(), itinerary(), handlingHistory);
     apply(new CargoRoutingStatusRecalculatedEvent(this.trackingId, 
         delivery.routingStatus());
     apply(new CargoTransportStatusRecalculatedEvent(this.trackingId, 
         delivery.routingStatus());
     ....sends events telling other aspects of the cargo
}

由于事件更小且更具体,因此不再需要DeliveryDto和它所需的映射器(域对象&lt; - &gt; DTO):

class CargoRoutingStatusRecalculatedEvent{
    private String trackingId;
    private String routingStatus;
}

class CargoTransportStatusRecalculatedEvent{
    private String trackingId;
    private String transportStatus;
}

1 个答案:

答案 0 :(得分:5)

您的活动定义是正确的。事件可以通过线路传输,因此序列化,您不希望发送大而丰富的对象。添加域事件被其他有界上下文使用的事实,这些上下文可能对行程的含义或他们不了解其他域对象有非常不同的定义。关键不是要结合有界上下文(BC),而是尽可能地使用“中性”信息来传达所发生的事情。

原语很棒,其次是值对象,特别是如果它们在整个域中的含义相同。

  

如果CargoAssignedEvent的某些订阅者需要行程的推导来做出决定,该怎么办?

域对象无法猜测任何订阅者的需求。域对象的职责只是生成事件,说明发生了什么。我认为没有关于如何定义事件的方法。他们必须代表Domain过去的操作,但是选择如何在代码中表示它取决于开发人员和应用程序的复杂性。直接使用所涉及的agreggate根或富实体是一个技术上的麻烦。我更喜欢使用的是基元(如果可能)或纪念品(DTO),它充当域对象的初始化数据。但是,只有我认为事件确实需要包含域对象。我总是试图定义一个事件而不涉及丰富的对象。

对于这个具体的例子,我认为事件处理程序可以使用存储库/服务来获取它需要的对象,只需要itineraryId和RouteCandidateDto中包含的一些信息。

EventStore适配器使事情变得复杂,我认为它应该只用于事件版本控制。域对象被重构的事实并没有改变事件定义。

如果事件仅包含域对象memento,那么它就是域对象来处理其版本控制。基本上它只是添加了一个带有MementoV2作为参数的新构造函数。