「ドメインイベントはドメイン内で起こったことの表現である」という定義に基づいて、ドメインイベントのフィールドとしてドメインオブジェクトを使用するのは自然なことです。
イベント ソーシングを使用する場合、ドメイン イベントは永続的です。そのため、フィールドとしてドメイン オブジェクトを使用する場合、ドメイン オブジェクトも永続的です。これにより、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 のフィールドとして使用して、ドメイン モデル (Itinerary、RoutingStatus) をイベントの永続性の問題から分離しています。ただし、これはイベント ハンドラ側で不便や問題を引き起こす可能性があります。CargoAssignedEvent の一部のサブスクライバーが決定を下すために旅程の派生を必要とする場合はどうなりますか? 次に、RouteCandidateDto を Itinerary にマップする必要があります。
考えられる解決策は、ドメイン オブジェクトをフィールドとして使用することですが、イベント ストアにいくつかのアダプターを導入します。イベントをロードまたは保存するときに、アダプタを使用してドメイン オブジェクトと dto をマップします。
私はそれを正しくやっていますか?どんなアイデアでも大歓迎です。
アップデート
旅程はおそらく特殊なケースです。これは値全体と見なされるため、この値オブジェクトを CargoLegEvent(TrackingId, Leg) のような小さなドメイン イベントのグループに分割することはできません。配送のケースを考えてみましょう。配送は貨物ドメインのもう 1 つの重要な値オブジェクトであり、旅程よりもはるかに豊富です。
/**
* 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;
}