CQRS + ES में किसी वस्तु को पूरी तरह से कहां से आरंभ किया जाना चाहिए: निर्माणकर्ता में, या पहली घटना को लागू करते समय?


9

ओओपी समुदाय में व्यापक सहमति प्रतीत होती है कि वर्ग निर्माणकर्ता को आंशिक रूप से या पूरी तरह से असंगठित वस्तु नहीं छोड़नी चाहिए।

"इनिशियलाइज़ेशन" से मेरा क्या मतलब है? मोटे तौर पर, परमाणु प्रक्रिया जो एक नव निर्मित वस्तु को एक ऐसी स्थिति में लाती है जहां उसके सभी वर्ग आक्रमणकारी रहते हैं। यह पहली चीज होनी चाहिए जो किसी ऑब्जेक्ट के साथ होती है, (इसे केवल एक बार ऑब्जेक्ट के अनुसार चलना चाहिए) और किसी भी चीज को अन-इनिशियलाइज्ड ऑब्जेक्ट को होल्ड करने की अनुमति नहीं दी जानी चाहिए। (इस प्रकार क्लास कंस्ट्रक्टर में ऑब्जेक्ट इनिशियलाइज़ेशन राईट करने की बार-बार सलाह दी जाती है। उसी कारण से, Initializeतरीकों को अक्सर फेल किया जाता है, क्योंकि ये एटमॉसिटी को तोड़ते हैं और इसे पकड़ना और उपयोग करना संभव बनाते हैं, एक ऐसी वस्तु जो अभी तक नहीं है। एक अच्छी तरह से परिभाषित राज्य में।)

समस्या: जब CQRS को इवेंट सोर्सिंग (CQRS + ES) के साथ जोड़ दिया जाता है, जहां किसी ऑब्जेक्ट के सभी राज्य परिवर्तन ईवेंट (इवेंट स्ट्रीम) की एक क्रमबद्ध श्रृंखला में पकड़े जाते हैं, तो मुझे आश्चर्य होता है कि जब कोई ऑब्जेक्ट वास्तव में पूरी तरह से प्रारंभिक अवस्था में पहुंचता है: वर्ग निर्माता के अंत में, या वस्तु के लिए पहली घटना के बाद लागू किया गया है?

नोट: मैं "कुल रूट" शब्द का उपयोग करने से बच रहा हूं। यदि आप पसंद करते हैं, तो जब भी आप "ऑब्जेक्ट" पढ़ते हैं, तब उसे प्रतिस्थापित करें।

उदाहरण के लिए चर्चा करें: मान लें कि प्रत्येक वस्तु को विशिष्ट रूप से कुछ अपारदर्शी Idमान (GUID) द्वारा पहचाना जाता है । एक घटना धारा जो उस वस्तु के राज्य परिवर्तनों को एक ही Idमान से इवेंट स्टोर में पहचाना जा सकता है : (चलो सही घटना क्रम के बारे में चिंता न करें।)

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

आगे मान लें कि दो वस्तु प्रकार हैं Customerऔर ShoppingCart। आइए ध्यान केंद्रित करें ShoppingCart: जब बनाया जाता है, तो खरीदारी की गाड़ियां खाली होती हैं और उन्हें ठीक एक ग्राहक के साथ जोड़ा जाना चाहिए। वह अंतिम बिट एक वर्ग अयोग्य है : एक 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;
    }
}

हालांकि, मैं एक नुकसान में हूं कि इसे सीक्यूआरएस + ईएस में कैसे स्थगित किया जाए बिना आस्थगित आरंभीकरण के साथ। चूंकि इनिशियलाइज़ेशन का यह सरल सा तरीका प्रभावी रूप से एक राज्य परिवर्तन है, क्या इसे एक घटना के रूप में तैयार नहीं करना होगा ?:

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)विधि पर छोड़ देना चाहिए (जो कि फ्रॉन्ड-ऑन के समान ही है Initialize())?
  • या निर्माणकर्ता को एक घटना स्ट्रीम प्राप्त करना चाहिए और इसे वापस खेलना चाहिए (जो पुन: आरंभीकरण परमाणु बनाता है, लेकिन इसका मतलब है कि प्रत्येक वर्ग के कंस्ट्रक्टर में एक ही जेनेरिक "प्ले बैक और अप्लाई" लॉजिक यानी अवांछित कोड दोहराव शामिल है)?
  • या दोनों पारंपरिक ओओपी निर्माता (जैसा कि ऊपर दिखाया गया है) होना चाहिए जो ऑब्जेक्ट को ठीक से इनिशियलाइज़ करता है, और फिर सभी ईवेंट्स लेकिन पहले से void Apply(…)संबंधित हैं ?

मुझे पूरी तरह से काम करने वाले डेमो कार्यान्वयन प्रदान करने के लिए जवाब की उम्मीद नहीं है; मैं पहले से ही बहुत खुश अगर कोई जहाँ मेरे तर्क त्रुटिपूर्ण किया गया है या वस्तु प्रारंभ वास्तव में समझा सकता है हो जाएगा है एक "दर्द बिंदु" सबसे CQRS + ES कार्यान्वयन में।

जवाबों:


3

CQRS + ES करते समय मैं सार्वजनिक निर्माण करने वालों को बिल्कुल पसंद नहीं करता। मेरी कुल जड़ें बनाना एक कारखाने के माध्यम से किया जाना चाहिए (इस तरह के सरल पर्याप्त निर्माण के लिए) या एक बिल्डर (अधिक जटिल कुल जड़ों के लिए)।

फिर वास्तव में ऑब्जेक्ट को इनिशियलाइज़ कैसे किया जाता है यह एक इम्प्लीमेंटेशन डिटेल है। OOP "इनिशियल का उपयोग न करें" -vvice सार्वजनिक इंटरफेस के बारे में है । आपको यह उम्मीद नहीं करनी चाहिए कि आपके कोड का उपयोग करने वाला कोई भी व्यक्ति जानता है कि उन्हें SecretInitializeMethod42 (बूल, इंट, स्ट्रिंग) को कॉल करना होगा - यह खराब सार्वजनिक एपीआई डिजाइन है। हालाँकि, यदि आपकी कक्षा किसी भी सार्वजनिक निर्माता को प्रदान नहीं करती है, लेकिन इसके बजाय CreateCewShoppingCart (स्ट्रिंग) विधि के साथ एक ShoppingCartFactory है, तो उस कारखाने का कार्यान्वयन बहुत अच्छी तरह से किसी भी तरह के आरंभीकरण / निर्माण जादू को छिपा सकता है, जिसे आपकी जानकारी के लिए आवश्यक नहीं है के बारे में (इस प्रकार एक अच्छा सार्वजनिक एपीआई प्रदान करते हैं, लेकिन आप पर्दे के पीछे और अधिक उन्नत वस्तु निर्माण करने की अनुमति देते हैं)।

फैक्टरियों को लोगों से यह सोचकर बहुत बुरा लगता है कि उनमें से बहुत से हैं, लेकिन सही तरीके से उपयोग किए जाने पर वे एक आसान आसान सार्वजनिक एपीआई के पीछे बहुत सारी जटिलताएं छिपा सकते हैं। उनका उपयोग करने से डरो मत, वे एक शक्तिशाली उपकरण हैं जो आपको जटिल ऑब्जेक्ट निर्माण को बहुत आसान बनाने में मदद कर सकते हैं - जब तक आप कोड के कुछ और लाइनों के साथ रह सकते हैं।

यह देखने की दौड़ नहीं है कि कोड की सबसे कम लाइनों के साथ समस्या का समाधान कौन कर सकता है - हालांकि यह एक निरंतर प्रतिस्पर्धा है कि कौन सबसे अच्छे सार्वजनिक एपीआई बना सकता है! ;)

संपादित करें: इन पैटर्नों को देखने के तरीके को लागू करने पर कुछ उदाहरण जोड़ना

यदि आपके पास बस एक "आसान" एग्रीगेट कंस्ट्रक्टर है, जिसमें कुछ आवश्यक पैरामीटर हैं, तो आप बस बहुत ही बुनियादी फैक्ट्री कार्यान्वयन के साथ जा सकते हैं, इन पंक्तियों में कुछ

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();

और निश्चित रूप से, आम तौर पर जब मैं बिल्डर पैटर्न की ओर मुड़ता हूं तो यह सिर्फ दो ints नहीं होता है, इसमें कुछ मूल्य वस्तुओं या कुछ अन्य प्रकार की बालों वाली चीजों के शब्दकोशों की सूची हो सकती है। हालांकि यह भी काफी उपयोगी है यदि आपके पास वैकल्पिक मापदंडों के कई संयोजन हैं।

इस तरह से जाने के लिए महत्वपूर्ण takeaways है:

  • आपका मुख्य उद्देश्य ऑब्जेक्ट निर्माण को अमूर्त करने में सक्षम हो रहा है ताकि बाहरी उपयोगकर्ता को आपके ईवेंट सिस्टम के बारे में पता न चले।
  • यह महत्वपूर्ण नहीं है कि सृजन कार्यक्रम को कहां या कौन पंजीकृत करता है, महत्वपूर्ण हिस्सा यह है कि यह पंजीकृत हो जाता है और आप गारंटी दे सकते हैं कि - इसके अलावा, यह एक आंतरिक कार्यान्वयन विवरण है। वह करें जो आपके कोड को सबसे अच्छी तरह से फिट करता है, मेरे उदाहरण का अनुसरण न करें क्योंकि यह "यह सही तरीका है" -आइटम है।
  • यदि आप चाहते हैं, तो इस तरह से आप अपने कारखानों / रिपॉजिटरी को ठोस कक्षाओं के बजाय इंटरफेस लौटा सकते हैं - जिससे यूनिट परीक्षणों के लिए उनका मजाक उड़ाना आसान हो जाता है!
  • यह कभी-कभी बहुत अधिक अतिरिक्त कोड होता है, जिससे कई लोग इससे दूर हो जाते हैं। लेकिन यह अक्सर विकल्पों की तुलना में काफी आसान कोड होता है और जब आप जल्दी या बाद में सामान बदलने की आवश्यकता होती है तो यह मूल्य प्रदान करता है। एक कारण है कि एरिक इवांस अपनी डीडीडी-बुक में कारखानों / रिपॉजिटरी के बारे में डीडीडी के महत्वपूर्ण हिस्सों के रूप में बात करते हैं - वे उपयोगकर्ता के लिए कुछ कार्यान्वयन विवरणों को लीक नहीं करने के लिए आवश्यक सार हैं । और टपका हुआ अमूर्त बुरा है।

आशा है कि थोड़ा और मदद करता है, बस टिप्पणियों में स्पष्टीकरण के लिए पूछें अन्यथा :)


+1, मैंने फैक्टरियों के बारे में कभी सोचा भी नहीं था कि यह संभव डिजाइन समाधान है। हालांकि एक बात: ऐसा लगता है कि फैक्ट्रियां अनिवार्य रूप से पब्लिक इंटरफेस में एक ही जगह लेती हैं कि कुलियों (+ शायद एक Initializeविधि) ने कब्जा कर लिया होगा। यह मुझे इस सवाल की ओर ले जाता है कि आपका ऐसा कारखाना कैसा दिख सकता है?
स्टैकक्स

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

ठीक है, पहली घटना यह होनी चाहिए कि ग्राहक खरीदारी की टोकरी बनाता है , इसलिए जब खरीदारी की टोकरी बनाई जाती है, तो आपके पास पहले से ही ग्राहक आईडी होती है।

यदि कोई राज्य दो अलग-अलग घटनाओं के बीच रखता है, तो परिभाषा के अनुसार यह एक वैध राज्य है। इसलिए यदि आप कहते हैं कि एक वैध शॉपिंग कार्ट ग्राहक से जुड़ा है, तो इसका मतलब यह है कि आपको कार्ट बनाते समय ग्राहक की जानकारी होना आवश्यक है


मेरा सवाल डोमेन को मॉडल करने के तरीके के बारे में नहीं है; मैं जानबूझकर सवाल पूछने के लिए एक सरल (लेकिन शायद अपूर्ण) डोमेन मॉडल का उपयोग कर रहा हूं। CreatedEmptyShoppingCartमेरे प्रश्न में घटना पर एक नज़र डालें : इसमें ग्राहक की जानकारी है, जैसे आप सुझाव देते हैं। मेरा प्रश्न इस बारे में अधिक है कि इस तरह की घटना किस तरह से संबंधित है, या ShoppingCartकार्यान्वयन के दौरान कक्षा के निर्माता के साथ प्रतिस्पर्धा करती है।
स्टैकएक्स

1
मैं बेहतर सवाल समझता हूं। तो, मैं कहूंगा, सही उत्तर तीसरा होना चाहिए। मुझे लगता है कि आपकी इकाइयाँ हमेशा एक मान्य स्थिति में होनी चाहिए। यदि इसके लिए यह आवश्यक है कि खरीदारी कार्ट में ग्राहक आईडी हो, तो इसे निर्माण के समय प्रदान किया जाना चाहिए, इसलिए एक ऐसे निर्माता की आवश्यकता होती है जो ग्राहक आईडी स्वीकार करता है। मैं स्काला में CQRS का अधिक आदी हूं, जहां डोमेन ऑब्जेक्ट्स को केस क्लासेस द्वारा दर्शाया जाएगा, जो अपरिवर्तनीय हैं और सभी संभावित संयोजनों के मापदंडों के लिए कंस्ट्रक्टर हैं, इसलिए मैं इसे प्रदान कर रहा हूं
एंड्रिया

0

नोट: यदि आप कुल रूट का उपयोग नहीं करना चाहते हैं, तो "निकाय" आपको लेन-देन की सीमाओं के बारे में चिंताओं को आगे बढ़ाते हुए यहां जो पूछ रहा है, उसमें से अधिकांश को शामिल करता है।

यहां इसके बारे में सोचने का एक और तरीका है: एक इकाई पहचान + राज्य है। एक मूल्य के विपरीत, एक इकाई वही आदमी होता है जब उसका राज्य बदलता है।

लेकिन राज्य को स्वयं एक मूल्य वस्तु के रूप में सोचा जा सकता है। इससे मेरा मतलब है, राज्य अपरिवर्तनीय है; इकाई का इतिहास एक अपरिवर्तनीय स्थिति से अगले तक एक संक्रमण है - प्रत्येक संक्रमण घटना की धारा में एक घटना से मेल खाती है।

State nextState = currentState.onEvent(e);

OnEvent () विधि एक क्वेरी है, बेशक - currentState बिल्कुल नहीं बदला है, इसके बजाय currentState नेक्स्टस्टेट बनाने के लिए उपयोग किए गए तर्कों की गणना कर रहा है।

इस मॉडल का अनुसरण करते हुए, शॉपिंग कार्ट के सभी उदाहरणों को एक ही बीज मूल्य से शुरू करने के बारे में सोचा जा सकता है ...।

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

ShoppingCart cart = new ShoppingCart(id, currentState);

चिंताओं का पृथक्करण - शॉपिंगकार्ट यह जानने के लिए कि कौन सी घटना आगे होनी चाहिए; ShoppingCart राज्य को पता है कि अगले राज्य में कैसे जाना है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.