Trường hợp một đối tượng trong CQRS + ES nên được khởi tạo hoàn toàn: trong hàm tạo hoặc khi áp dụng sự kiện đầu tiên?


9

Dường như có một thỏa thuận rộng rãi trong cộng đồng OOP rằng trình xây dựng lớp không nên để một đối tượng hoặc thậm chí hoàn toàn không được khởi tạo.

"Khởi tạo" nghĩa là gì? Nói một cách đơn giản, quá trình nguyên tử đưa một vật thể mới được tạo ra vào trạng thái mà tất cả các bất biến lớp của nó giữ. Nó phải là điều đầu tiên xảy ra với một đối tượng, (nó chỉ nên chạy một lần cho mỗi đối tượng) và không có gì được phép giữ một đối tượng chưa được khởi tạo. (Do đó, lời khuyên thường xuyên để thực hiện khởi tạo đối tượng ngay trong hàm tạo của lớp. Vì lý do tương tự, Initializecác phương thức thường được tán thành, vì chúng phá vỡ tính nguyên tử và làm cho nó có thể nắm giữ và sử dụng một đối tượng chưa ở trạng thái được xác định rõ.)

Vấn đề: Khi CQRS được kết hợp với tìm nguồn cung ứng sự kiện (CQRS + ES), trong đó tất cả các thay đổi trạng thái của một đối tượng được bắt gặp trong một chuỗi các sự kiện (luồng sự kiện), tôi sẽ tự hỏi khi một đối tượng thực sự đạt đến trạng thái khởi tạo hoàn toàn: Vào cuối của trình xây dựng lớp, hoặc sau khi sự kiện đầu tiên đã được áp dụng cho đối tượng?

Lưu ý: Tôi không sử dụng thuật ngữ "tổng hợp gốc". Nếu bạn thích, thay thế nó bất cứ khi nào bạn đọc "đối tượng".

Ví dụ cho thảo luận: Giả sử rằng mỗi đối tượng được xác định duy nhất bởi một số Idgiá trị mờ (nghĩ GUID). Một luồng sự kiện thể hiện các thay đổi trạng thái của đối tượng đó có thể được xác định trong kho sự kiện theo cùng một Idgiá trị: (Chúng ta đừng lo lắng về thứ tự sự kiện chính xác.)

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

Giả sử thêm rằng có hai loại đối tượng CustomerShoppingCart. Hãy tập trung vào ShoppingCart: Khi được tạo, giỏ mua hàng trống và phải được liên kết với chính xác một khách hàng. Bit cuối cùng đó là một bất biến lớp: Một ShoppingCartđối tượng không liên quan đến a Customerđang ở trạng thái không hợp lệ.

Trong OOP truyền thống, người ta có thể mô hình hóa điều này trong hàm tạo:

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;
    }
}

Tuy nhiên, tôi không biết làm thế nào để mô hình hóa điều này trong CQRS + ES mà không kết thúc với việc khởi tạo hoãn lại. Vì một chút khởi tạo đơn giản này thực sự là một sự thay đổi trạng thái, nên nó có phải được mô hình hóa như một sự kiện không?

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.

Đây rõ ràng sẽ phải là sự kiện đầu tiên trong ShoppingCartluồng sự kiện của bất kỳ đối tượng nào và đối tượng đó sẽ chỉ được khởi tạo khi sự kiện được áp dụng cho sự kiện đó.

Vì vậy, nếu khởi tạo trở thành một phần của "phát lại" luồng sự kiện (đây là một quá trình rất chung chung có thể sẽ hoạt động như nhau, cho dù Customerđối tượng hay ShoppingCartđối tượng hoặc bất kỳ loại đối tượng nào khác cho vấn đề đó)

  • Hàm tạo có nên không tham số và không làm gì cả, để lại tất cả công việc cho một void Apply(CreatedEmptyShoppingCart)phương thức nào đó (giống như phương pháp nhăn mặt Initialize())?
  • Hoặc hàm tạo nên nhận một luồng sự kiện và phát lại (điều này làm cho việc khởi tạo lại nguyên tử, nhưng có nghĩa là hàm tạo của mỗi lớp chứa cùng một logic "phát lại và áp dụng" chung, tức là sao chép mã không mong muốn)?
  • Hoặc nên có cả một hàm tạo OOP truyền thống (như được hiển thị ở trên) để khởi tạo đúng đối tượng, và sau đó tất cả các sự kiện nhưng lần đầu tiên được void Apply(…)liên kết với nó?

Tôi không mong đợi câu trả lời để cung cấp một triển khai demo hoạt động đầy đủ; Tôi đã rất vui nếu ai đó có thể giải thích lý do của tôi thiếu sót ở đâu, hoặc liệu khởi tạo đối tượng có thực sự một "điểm đau" trong hầu hết các triển khai CQRS + ES hay không.

Câu trả lời:


3

Khi làm CQRS + ES tôi không thích có các nhà xây dựng công cộng nào cả. Tạo rễ tổng hợp của tôi nên được thực hiện thông qua một nhà máy (đối với các công trình đủ đơn giản như thế này) hoặc một công cụ xây dựng (đối với các gốc tổng hợp phức tạp hơn).

Làm thế nào để sau đó thực sự khởi tạo đối tượng là một chi tiết thực hiện. OOP "Không sử dụng khởi tạo" -advice là imho về các giao diện công cộng . Bạn không nên hy vọng rằng bất kỳ ai sử dụng mã của bạn đều biết rằng họ phải gọi SecretInitializeMethod42 (bool, int, chuỗi) - đó là thiết kế API công khai tồi. Tuy nhiên, nếu lớp của bạn không cung cấp bất kỳ hàm tạo công khai nào mà thay vào đó là một ShoppingCartFactory với phương thức CreateNewShoppingCart (chuỗi) thì việc triển khai nhà máy đó có thể che giấu rất tốt bất kỳ loại phép thuật khởi tạo / xây dựng nào mà người dùng của bạn không cần biết về (do đó cung cấp một API công khai đẹp, nhưng cho phép bạn thực hiện việc tạo đối tượng nâng cao hơn phía sau hậu trường).

Các nhà máy nhận được một đại diện xấu từ những người nghĩ rằng có quá nhiều người trong số họ, nhưng được sử dụng một cách chính xác, họ có thể che giấu rất nhiều sự phức tạp đằng sau một API công khai dễ hiểu dễ hiểu. Đừng ngại sử dụng chúng, chúng là một công cụ mạnh mẽ có thể giúp bạn xây dựng đối tượng phức tạp dễ dàng hơn nhiều - miễn là bạn có thể sống với một số dòng mã hơn.

Đây không phải là cuộc đua để xem ai có thể giải quyết vấn đề với ít dòng mã nhất - tuy nhiên đây là một cuộc cạnh tranh đang diễn ra để ai có thể tạo ra API công khai đẹp nhất! ;)

Chỉnh sửa: Thêm một số ví dụ về cách áp dụng các mẫu này có thể trông như thế nào

Nếu bạn chỉ có một hàm tạo tổng hợp "dễ dàng" có một vài tham số bắt buộc, bạn có thể thực hiện chỉ với một triển khai nhà máy rất cơ bản, một cái gì đó dọc theo các dòng này

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;
    }
}

Tất nhiên, chính xác cách bạn phân chia việc tạo FooCreatedEvent là trong trường hợp này tùy thuộc vào bạn. Người ta cũng có thể tạo ra một trường hợp để có một hàm tạo FooAggregate (FooCreatedEvent) hoặc có một hàm tạo FooAggregate (int, int) tạo ra sự kiện. Chính xác cách bạn chọn để phân chia trách nhiệm ở đây tùy thuộc vào những gì bạn nghĩ là sạch nhất và cách bạn đã thực hiện đăng ký sự kiện tên miền của mình. Tôi thường chọn để nhà máy tạo sự kiện - nhưng tùy thuộc vào bạn vì việc tạo sự kiện hiện là chi tiết triển khai nội bộ mà bạn có thể thay đổi và cấu trúc lại bất cứ lúc nào mà không cần thay đổi giao diện bên ngoài. Một chi tiết quan trọng ở đây là tổng hợp không có hàm tạo công khai và tất cả các setters là riêng tư. Bạn không muốn bất cứ ai sử dụng chúng bên ngoài.

Mẫu này hoạt động tốt khi bạn chỉ thay thế các nhà xây dựng ít nhiều, nhưng nếu bạn có cấu trúc đối tượng nâng cao hơn thì điều này có thể trở nên quá phức tạp để sử dụng. Trong trường hợp này, tôi thường từ bỏ mẫu nhà máy và chuyển sang mẫu xây dựng thay vào đó - thường với một cú pháp trôi chảy hơn.

Ví dụ này hơi bị ép buộc vì lớp mà nó xây dựng không phức tạp lắm, nhưng bạn có thể hy vọng nắm bắt được ý tưởng và xem nó sẽ giảm bớt các nhiệm vụ xây dựng phức tạp hơn như thế nào

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;
    }
}

Và sau đó bạn sử dụng nó như

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

Và tất nhiên, nói chung khi tôi chuyển sang mẫu xây dựng, nó không chỉ là hai số nguyên, nó có thể chứa danh sách một số đối tượng giá trị hoặc từ điển của một số loại có thể là một số thứ khác có nhiều lông hơn. Tuy nhiên nó cũng khá hữu ích nếu bạn có nhiều kết hợp các tham số tùy chọn.

Các bước quan trọng để đi theo cách này là:

  • Mục tiêu chính của bạn là có thể trừu tượng hóa việc xây dựng đối tượng để người dùng bên ngoài không phải biết về hệ thống sự kiện của bạn.
  • Điều đó không quan trọng ở đâu hoặc ai đăng ký sự kiện sáng tạo, phần quan trọng là nó được đăng ký và bạn có thể đảm bảo rằng - ngoài điều đó, đó là một chi tiết triển khai nội bộ. Hãy làm những gì phù hợp với mã của bạn nhất, đừng làm theo ví dụ của tôi vì một số loại "đây là cách đúng".
  • Nếu bạn muốn, bằng cách này, bạn có thể yêu cầu các nhà máy / kho lưu trữ của mình trả về các giao diện thay vì các lớp cụ thể - làm cho chúng dễ dàng giả hơn để kiểm tra đơn vị!
  • Điều này đôi khi có rất nhiều mã, khiến nhiều người né tránh nó. Nhưng nó thường là mã khá dễ dàng so với các lựa chọn thay thế và nó cung cấp giá trị khi bạn cần sớm hay muộn để thay đổi công cụ. Có một lý do mà Eric Evans nói về các nhà máy / kho lưu trữ trong cuốn sách DDD của mình như là những phần quan trọng của DDD - chúng là những tóm tắt cần thiết để không rò rỉ một số chi tiết triển khai cho người dùng. Và trừu tượng rò rỉ là xấu.

Hy vọng rằng sẽ giúp thêm một chút, chỉ cần yêu cầu làm rõ trong các ý kiến ​​khác :)


+1, tôi thậm chí không bao giờ nghĩ các nhà máy là một giải pháp thiết kế khả thi. Mặc dù vậy, có vẻ như các nhà máy về cơ bản chiếm cùng một vị trí trong giao diện công cộng mà các nhà xây dựng tổng hợp (+ có lẽ là một Initializephương thức) sẽ chiếm giữ. Điều đó dẫn tôi đến câu hỏi, một nhà máy của bạn trông như thế nào?
stakx

3

Theo tôi, tôi nghĩ rằng câu trả lời giống như đề xuất số 2 của bạn cho các tổng hợp hiện có; cho các tổng hợp mới giống như số 3 (nhưng xử lý sự kiện như bạn đề xuất).

Đây là một số mã, hy vọng nó là một số trợ giúp.

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

Chà, sự kiện đầu tiên nên là một khách hàng tạo ra một giỏ hàng , vì vậy khi giỏ hàng được tạo ra, bạn đã có một id khách hàng như một phần của sự kiện.

Nếu một trạng thái giữ giữa hai sự kiện khác nhau, theo định nghĩa, đó là trạng thái hợp lệ. Vì vậy, nếu bạn nói rằng giỏ hàng hợp lệ được liên kết với khách hàng, điều này có nghĩa là bạn cần có thông tin khách hàng khi tự tạo giỏ hàng


Câu hỏi của tôi không phải là về cách mô hình hóa tên miền; Tôi cố tình sử dụng một mô hình miền đơn giản (nhưng có lẽ không hoàn hảo) để đặt câu hỏi. Hãy xem CreatedEmptyShoppingCartsự kiện trong câu hỏi của tôi: Nó có thông tin khách hàng, giống như bạn đề xuất. Câu hỏi của tôi là nhiều hơn về cách một sự kiện như vậy liên quan đến, hoặc cạnh tranh với, người xây dựng lớp ShoppingCarttrong quá trình thực hiện.
stakx

1
Tôi hiểu rõ hơn câu hỏi. Vì vậy, tôi sẽ nói, câu trả lời đúng phải là câu thứ ba. Tôi nghĩ rằng các thực thể của bạn phải luôn ở trong một trạng thái hợp lệ. Nếu điều này yêu cầu giỏ hàng có id khách hàng, thì điều này cần được cung cấp tại thời điểm tạo, do đó cần một nhà xây dựng chấp nhận id khách hàng. Tôi đã quen với CQRS hơn trong Scala, nơi các đối tượng miền sẽ được đại diện bởi các lớp trường hợp, là bất biến và có các hàm tạo cho tất cả các kết hợp tham số có thể, vì vậy tôi đã đưa ra điều này cho phép
Andrea

0

Lưu ý: nếu bạn không muốn sử dụng gốc tổng hợp, "thực thể" sẽ bao gồm hầu hết những gì bạn đang hỏi về vấn đề này trong khi bên cạnh đẩy lùi những lo ngại về ranh giới giao dịch.

Đây là một cách nghĩ khác về nó: một thực thể là danh tính + trạng thái. Không giống như một giá trị, một thực thể là cùng một người ngay cả khi trạng thái của anh ta thay đổi.

Nhưng chính nhà nước có thể được coi là một đối tượng giá trị. Ý tôi là, nhà nước là bất biến; lịch sử của thực thể là một quá trình chuyển đổi từ một trạng thái bất biến sang trạng thái tiếp theo - mỗi chuyển đổi tương ứng với một sự kiện trong luồng sự kiện.

State nextState = currentState.onEvent(e);

Tất nhiên, phương thức onEvent () là một truy vấn - currentState hoàn toàn không thay đổi, thay vào đó currentState đang tính toán các đối số được sử dụng để tạo nextState.

Theo mô hình này, tất cả các phiên bản của Giỏ hàng có thể được coi là bắt đầu từ cùng một giá trị hạt giống ....

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

ShoppingCart cart = new ShoppingCart(id, currentState);

Phân tách các mối quan tâm - ShoppingCart xử lý một lệnh để tìm ra sự kiện nào sẽ xảy ra tiếp theo; Bang mua sắm biết làm thế nào để đến trạng thái tiếp theo.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.