생성자에서 또는 첫 번째 이벤트를 적용 할 때 CQRS + ES의 오브젝트를 어디에서 완전히 초기화해야합니까?


9

OOP 커뮤니티에는 클래스 생성자가 객체를 부분적으로 또는 완전히 초기화되지 않은 상태로 두지 말아야한다는 광범위한 동의가있는 것으로 보입니다.

"초기화"는 무엇을 의미합니까? 대략적으로 말하자면 새로 생성 된 객체를 모든 클래스 불변 값이 유지되는 상태로 만드는 원자 프로세스입니다. 객체에 대해 가장 먼저 발생해야하며 (객체 당 한 번만 실행해야 함) 초기화되지 않은 객체를 잡을 수있는 것은 없습니다. (따라서 클래스 생성자에서 바로 객체 초기화를 수행하라는 빈번한 조언이 있습니다. 같은 이유로, Initialize메소드는 원 자성을 분리하고 아직 그렇지 않은 객체를 가져 와서 사용할 수있게하기 때문에 종종 눈살을 찌푸립니다. 잘 정의 된 상태로)

문제 : CQRS가 이벤트 소싱 (CQRS + ES)과 결합 될 때 개체의 모든 상태 변경이 정렬 된 일련의 이벤트 (이벤트 스트림)에서 포착되는 경우, 개체가 실제로 완전히 초기화 된 상태에 도달하는 시점이 궁금합니다. 클래스 생성자의 끝에서 또는 첫 번째 이벤트가 객체에 적용된 후?

참고 : "aggregate root"라는 용어는 사용하지 않습니다. 원하는 경우 "object"를 읽을 때마다이를 대체하십시오.

토론의 예 : 각 객체가 불투명 한 Id값으로 고유하게 식별된다고 가정합니다 (GUID 생각). 해당 객체의 상태 변경을 나타내는 이벤트 스트림은 동일한 Id값으로 이벤트 저장소에서 식별 될 수 있습니다 (올바른 이벤트 순서에 대해 걱정하지 마십시오).

interface IEventStore
{
    IEnumerable<IEvent> GetEventsOfObject(Id objectId); 
}

Customer그리고 두 개의 객체 유형이 있다고 가정합니다 ShoppingCart. 집중하자 ShoppingCart: 쇼핑 카트가 비어 있으며 정확히 하나의 고객과 연결되어 있어야합니다. 마지막 비트는 클래스 불변입니다. a ShoppingCart와 연관되지 않은 객체가 Customer유효하지 않은 상태입니다.

전통적인 OOP에서는 생성자에서이를 모델링 할 수 있습니다.

partial class ShoppingCart
{
    public Id Id { get; private set; }
    public Customer Customer { get; private set; }

    public ShoppingCart(Id id, Customer customer)
    {
        this.Id = id;
        this.Customer = customer;
    }
}

그러나 지연 된 초기화로 끝나지 않고 CQRS + ES에서이를 모델링하는 방법을 잃어 버렸습니다. 이 간단한 초기화 비트는 사실상 상태 변경이므로 이벤트로 모델링 할 필요는 없습니까?

partial class CreatedEmptyShoppingCart
{
    public ShoppingCartId { get; private set; }
    public CustomerId { get; private set; }
}
// Note: `ShoppingCartId` is not actually required, since that Id must be
// known in advance in order to fetch the event stream from the event store.

이것은 분명히 모든 ShoppingCart객체의 이벤트 스트림 에서 첫 번째 이벤트 여야 하며 해당 객체는 이벤트가 적용된 후에 만 ​​초기화됩니다.

따라서 초기화가 이벤트 스트림 "재생"의 일부가되면 ( Customer객체 또는 ShoppingCart객체 또는 해당 문제에 대한 다른 객체 유형에 관계없이 동일하게 작동하는 매우 일반적인 프로세스입니다 )…

  • 생성자가 매개 변수가없고 아무것도하지 않아야합니까? 모든 작업을 일부 void Apply(CreatedEmptyShoppingCart)방법으로 남겨 두어야합니까 (frowned-upon과 거의 동일 Initialize())?
  • 또는 생성자가 이벤트 스트림을 수신하여 재생해야합니까 (초기화를 다시 수행하지만 각 클래스의 생성자에는 동일한 일반 "재생 및 적용"논리, 즉 원치 않는 코드 복제가 포함됨을 의미 함)?
  • 또는 객체를 올바르게 초기화하는 전통적인 OOP 생성자 (위 그림 참조)와 첫 번째 이벤트 제외한 모든 이벤트가 void Apply(…)있어야합니까?

나는 완전히 작동하는 데모 구현을 제공하기위한 답을 기대하지는 않는다. 누군가가 내 추론이 결함 또는 개체 초기화가 정말 있는지 위치를 설명 할 수 있다면 나는 이미 아주 행복 할 것 입니다 가장 CQRS + ES 구현에 "고통 지점".

답변:


3

CQRS + ES를 수행 할 때는 공개 생성자가없는 것을 선호합니다. 집계 루트 생성은 팩토리 (이와 같이 간단하게 구성하기 위해) 또는 빌더 (더 복잡한 집계 루트)를 통해 수행해야합니다.

그런 다음 실제로 객체를 초기화하는 방법은 구현 세부 사항입니다. OOP "초기화를 사용하지 마십시오"라는 조언은 공용 인터페이스 에 관한 것 입니다. 코드를 사용하는 사람은 SecretInitializeMethod42 (bool, int, string)를 호출해야한다는 것을 알고 있어야합니다. 이는 공개 API 디자인이 잘못되었습니다. 그러나 클래스가 공용 생성자를 제공하지 않고 CreateNewShoppingCart (string) 메소드가 포함 된 ShoppingCartFactory가있는 경우 해당 팩토리 구현시 사용자가 알 필요가없는 초기화 / 생성자 마법을 숨길 수 있습니다. (따라서 훌륭한 퍼블릭 API를 제공하지만이면에서 더 고급 객체 생성을 수행 할 수 있습니다).

팩토리는 사람들이 너무 많다고 생각하는 사람들로부터 나쁜 평판을 얻지 만 올바르게 사용하면 이해하기 쉬운 공개 API 뒤에 많은 복잡성을 숨길 수 있습니다. 그것들을 사용하는 것을 두려워하지 마십시오. 더 많은 코드 줄로 살 수 있다면 복잡한 객체 구성을 훨씬 쉽게 만들 수있는 강력한 도구입니다.

가장 적은 코드 줄로 문제를 해결할 수있는 사람을 보는 것은 경쟁이 아닙니다. 그러나 누가 가장 훌륭한 퍼블릭 API를 만들 수 있는지에 대한 지속적인 경쟁입니다! ;)

편집 : 이러한 패턴을 적용하는 방법에 대한 몇 가지 예 추가

두 개의 필수 매개 변수가있는 "쉬운"집계 생성자가있는 경우 매우 기본적인 팩토리 구현으로이 행을 따라갈 수 있습니다.

public class FooAggregate {
     internal FooAggregate() { }

     public int A { get; private set; }
     public int B { get; private set; }

     internal Handle(FooCreatedEvent evt) {
         this.A = a;
         this.B = b;
     }
}

public class FooFactory {
    public FooAggregate Create(int a, int b) {
        var evt = new FooCreatedEvent(a, b);
        var result = new FooAggregate();
        result.Handle(evt);
        DomainEvents.Register(result, evt);
        return result;
    }
}

물론, FooCreatedEvent를 만드는 방법을 정확하게 구분하는 것은이 경우에 달려 있습니다. FooAggregate (FooCreatedEvent) 생성자를 갖거나 이벤트를 생성하는 FooAggregate (int, int) 생성자를 가질 수도 있습니다. 여기에서 책임을 분담하기로 선택한 방법은 가장 깨끗하다고 ​​생각하는 것과 도메인 이벤트 등록을 구현 한 방법에 달려 있습니다. 나는 종종 팩토리가 이벤트를 생성하도록 선택하지만 이벤트 생성은 이제 외부 인터페이스를 변경하지 않고도 언제든지 변경하고 리팩토링 할 수있는 내부 구현 세부 사항이므로 사용자에게 달려 있습니다. 여기서 중요한 세부 사항은 집계에 공용 생성자가 없으며 모든 세터가 비공개라는 것입니다. 다른 사람이 외부에서 사용하는 것을 원하지 않습니다.

이 패턴은 생성자를 대체하거나 교체 할 때 잘 작동하지만 고급 객체 구성을 사용하는 경우 사용하기가 너무 복잡해질 수 있습니다. 이 경우 일반적으로 팩토리 패턴을 포기하고 대신 더 유창한 구문으로 빌더 패턴으로 바꿉니다.

이 예제는 빌드하는 클래스가 매우 복잡하지 않기 때문에 약간 강제적이지만 아이디어를 이해하고 더 복잡한 건설 작업을 쉽게하는 방법을 알 수 있습니다.

public class FooBuilder {
    private int a;
    private int b;   

    public FooBuilder WithA(int a) {
         this.a = a;
         return this;
    }

    public FooBuilder WithB(int b) {
         this.b = b;
         return this;
    }

    public FooAggregate Build() {
         if(!someChecksThatWeHaveAllState()) {
              throw new OmgException();
         }

         // Some hairy logic on how to create a FooAggregate and the creation events from our state
         var foo = new FooAggregate(....);
         foo.PlentyOfHairyInitialization(...);
         DomainEvents.Register(....);

         return foo;
    }
}

그리고 당신은 그것을 사용

var foo = new FooBuilder().WithA(1).Build();

물론 일반적으로 빌더 패턴으로 전환하면 두 개의 정수가 아닌 일부 가치 객체 또는 사전에 더 많은 털이 많은 것들의 목록이 포함될 수 있습니다. 그러나 옵션 매개 변수를 많이 조합 한 경우에도 매우 유용합니다.

이 길을 가기 위해 중요한 조치는 다음과 같습니다.

  • 주요 목표는 외부 사용자가 이벤트 시스템에 대해 알 필요가 없도록 오브젝트 구성을 추상화하는 것입니다.
  • 생성 이벤트를 어디에 또는 누가 등록하는지는 중요하지 않으며, 중요한 부분은 등록 이벤트가 등록되고이를 보장 할 수 있다는 것입니다. 코드에 가장 잘 맞는 것을 수행하고, "이것은 올바른 방법입니다"라는 내 예제를 따르지 마십시오.
  • 원하는 경우이 방법으로 팩토리 / 리포지토리가 구체적인 클래스 대신 인터페이스를 반환하여 단위 테스트를 쉽게 조롱 할 수 있습니다!
  • 이것은 때때로 많은 여분의 코드이므로 많은 사람들이 그것을 부끄러워합니다. 그러나 대안에 비해 코드가 매우 쉬운 경우가 많으며 조만간 변경해야 할 때 가치를 제공합니다. Eric Evans가 DDD 서적에서 팩토리 / 저장소에 대해 DDD의 중요한 부분으로 이야기하는 이유가 있습니다. 사용자에게 특정 구현 세부 정보를 유출시키지 않기 위해 필요한 추상화입니다. 새는 추상화는 나쁘다.

조금 더 도움이되기를 바랍니다. 그렇지 않으면 의견에 설명을 요청하십시오 :)


+1, 공장을 가능한 설계 솔루션으로 생각한 적이 없습니다. 한가지 사실 : 팩토리 생성자가 (공식적으로 아마도 Initialize메소드) 차지했을 것처럼 팩토리가 퍼블릭 인터페이스에서 본질적으로 같은 위치에있는 것처럼 들립니다 . 그것은 당신의 공장이 어떻게 생겼을까 요?
stakx

3

내 생각에, 그 대답은 기존의 집계에 대한 당신의 제안 # 2와 더 비슷하다고 생각합니다. # 3과 같은 새로운 집계의 경우 (그러나 제안한대로 이벤트 처리).

여기 코드가 있습니다. 도움이 되길 바랍니다.

public abstract class Aggregate
{
    Dictionary<Type, Delegate> _handlers = new Dictionary<Type, Delegate>();

    protected Aggregate(long version = 0)
    {
        this.Version = version;
    }

    public long Version { get; private set; }

    protected void Handles<TEvent>(Action<TEvent> action)
        where TEvent : IDomainEvent            
    {
        this._handlers[typeof(TEvent)] = action;
    }

    private IList<IDomainEvent> _pendingEvents = new List<IDomainEvent>();

    // Apply a new event, and add to pending events to be committed to event store
    // when transaction completes
    protected void Apply(IDomainEvent @event)
    {
        this.Invoke(@event);
        this._pendingEvents.Add(@event);
    }

    // Invoke handler to change state of aggregate in response to event
    // Event may be an old event from the event store, or may be an event triggered
    // during the lifetime of this instance.
    protected void Invoke(IDomainEvent @event)
    {
        Delegate handler;
        if (this._handlers.TryGetValue(@event.GetType(), out handler))
            ((Action<TEvent>)handler)(@event);
    }
}

public class ShoppingCart : Aggregate
{
    private Guid _id, _customerId;

    private ShoppingCart(long version = 0)
        : base(version)
    {
         // Setup handlers for events
         Handles<ShoppingCartCreated>(OnShoppingCartCreated);
         // Handles<ItemAddedToShoppingCart>(OnItemAddedToShoppingCart);  
         // etc...
    } 

    public ShoppingCart(long version, IEnumerable<IDomainEvent> events)
        : this(version)
    {
         // Replay existing events to get current state
         foreach (var @event in events)
             this.Invoke(@event);
    }

    public ShoppingCart(Guid id, Guid customerId)
        : this()
    {
        // Process new event, changing state and storing event as pending event
        // to be saved when aggregate is committed.
        this.Apply(new ShoppingCartCreated(id, customerId));            
    }            

    private void OnShoppingCartCreated(ShoppingCartCreated @event)
    {
        this._id = @event.Id;
        this._customerId = @event.CustomerId;
    }
}

public class ShoppingCartCreated : IDomainEvent
{
    public ShoppingCartCreated(Guid id, Guid customerId)
    {
        this.Id = id;
        this.CustomerId = customerId;
    }

    public Guid Id { get; private set; }
    public Guid CustomerID { get; private set; }
}

0

첫 번째 이벤트는 고객이 장바구니를 작성하는 것이므로 장바구니가 작성되면 이벤트의 일부로 이미 고객 ID가 있습니다.

상태가 서로 다른 두 이벤트 사이에 있으면 정의상 유효한 상태입니다. 따라서 유효한 장바구니가 고객과 연결되어 있다고하면 장바구니 자체를 만들 때 고객 정보가 필요하다는 의미입니다.


내 질문은 도메인을 모델링하는 방법에 관한 것이 아닙니다. 질문을하기 위해 의도적으로 단순하지만 불완전한 도메인 모델을 사용하고 있습니다. CreatedEmptyShoppingCart내 질문 의 이벤트를 살펴보십시오 . 제안한 것처럼 고객 정보가 있습니다. 내 질문은 그러한 이벤트가 ShoppingCart구현 중 클래스 생성자와 어떻게 관련되거나 경쟁하는지에 관한 것입니다.
stakx

1
나는 질문을 더 잘 이해한다. 따라서 정답은 세 번째 답이어야합니다. 나는 당신의 실체가 항상 유효한 상태에 있어야한다고 생각합니다. 장바구니에 고객 ID가 있어야하는 경우, 작성시이를 제공해야하므로 고객 ID를 허용하는 생성자가 필요합니다. 나는이주는 그래서 나는 더 많은 도메인 객체는 불변 및 매개 변수의 가능한 모든 조합에 대한 생성자를 가질 경우 클래스에 의해 표현 될 것이다 스칼라에서 CQRS에 익숙한 오전 부여
안드레아

0

참고 : 집계 루트를 사용하지 않으려는 경우 "엔터티"에는 트랜잭션 경계에 대한 우려를 회피하면서 여기에서 요구하는 대부분의 내용이 포함됩니다.

여기에 다른 사고 방식이 있습니다 : 실체는 정체성 + 상태입니다. 가치와는 달리 실체는 상태가 변할 때도 같은 사람입니다.

그러나 국가 자체는 가치의 대상으로 생각할 수 있습니다. 이것은 국가가 불변이라는 것을 의미합니다. 엔티티의 히스토리는 하나의 불변 상태에서 다음 불변 상태로의 전환입니다. 각 전환은 이벤트 스트림의 이벤트에 해당합니다.

State nextState = currentState.onEvent(e);

onEvent () 메소드는 물론 쿼리입니다. currentState는 전혀 변경되지 않고 currentState는 nextState를 만드는 데 사용되는 인수를 계산합니다.

이 모델에 따르면 모든 장바구니 인스턴스는 동일한 시드 값에서 시작하는 것으로 생각할 수 있습니다.

State currentState = ShoppingCart.SEED;
for (Event e : history) {
    currentState = currentState.onEvent(e);
}

ShoppingCart cart = new ShoppingCart(id, currentState);

우려의 분리-ShoppingCart는 다음에 어떤 이벤트가 발생할지 파악하는 명령을 처리합니다. ShoppingCart State는 다음 주에가는 방법을 알고 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.