1

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

使用事件溯源时,域事件是持久的。因此,如果他们使用域对象作为他们的字段,那么域对象也是持久的。这削弱了采用 CQRS 和事件溯源所获得的优势,使域对象更难以更改和发展。

考虑 Eric Evans 的 dddsample 的 CQRS 版本,用户故事是:

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 的字段来将领域模型(行程、路由状态)与事件持久性问题分开。但这可能会在事件处理程序方面造成一些不便和麻烦。如果 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 和它所需要的映射器(域对象 <--> DTO):

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

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

1 回答 1

5

您的事件定义是正确的。事件可以通过网络传输,从而序列化,您不想发送大而丰富的对象。添加域事件被其他有界上下文使用的事实,这些上下文可能对行程的含义有非常不同的定义,或者他们不知道其他域对象。关键不是耦合有界上下文(BC),而是尽可能使用“中性”信息来传达发生的事情。

基元很棒,其次是价值对象,特别是如果它们在整个域中都意味着相同的话。

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

域对象不可能猜测任何订阅者的需求。域对象的职责只是生成说明发生了什么的事件。我认为没有关于如何定义事件的秘诀。它们必须表示域过去的操作,但是选择如何在代码中表示它取决于开发人员和应用程序的复杂性。直接使用所涉及的聚合根或丰富的实体是一个技术上的麻烦。我更喜欢使用原语(如果可能的话)或作为域对象初始化数据的备忘录(DTO)。但是,仅当我认为事件确实需要包含域对象时。我总是试图在不涉及丰富对象的情况下定义一个事件。

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

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

如果事件仅包含域对象备忘录,则由域对象来处理其版本控制。基本上它只是添加一个以 MementoV2 作为参数的新构造函数。

于 2013-11-06T09:35:49.363 回答