Introduktion til CQRS og event sourcing #2

I det forrige indlæg beskrev jeg i grove træk principperne i event sourcing og CQRS, og så lovede jeg at give en introduktion til d60s Cirqus-framework… samtidig med introduktionen til Cirqus vil jeg også sørge for at vi har en fælles ordbog, sådan at vi er nogenlunde enige om hvad vi snakker om resten af tiden.

Here goes:

Domain-driven design

Greg Youngs ide om CQRS udspringer af domain-driven design, altså DDD – faktisk blev konceptet, så vidt jeg ved, initialt omtalt som DDDD, hvilket er et akronym for “distributed domain-driven design”, altså DDD ophøjet til at indeholde en form for distribution.

Eric Evans’ ideer om domain-driven design er således “bagt ind” i CQRS fra begyndelsen, og det er også herfra nogle af CQRS-termerne er hentet. I de følgende afsnit i dette indlæg vil jeg præsentere CQRS-relevante begreber og beskrive hvad der menes med dem.

Aggregate root

En aggregate root er roden i et aggregate: En samling af objekter, som logisk hører sammen. Roden er indgangen til aggregatet, og roden garanterer aggregatets konsistens og interne integritet. Roden ikke må gøre sig antagelser om andre rødders tilstand og skal være i stand til at operere selvstændigt og uafhængigt.

Med Cirqus laver man en aggregate root i sin model ved at nedarve fra klassen AggregateRoot:

public class Beetroot : AggregateRoot {
    public void Squeeze(decimal howMuch) {
    }
}

Command

En command er et objekt, som beskriver en operation, der kan udføres på en aggregate root – dvs. både noget om hvilken operation, der skal udføres, samt alle eventuelle argumenter til den pågældende operation.

Med Cirqus laver man commands ved at arve fra den generisk Command-klasse, som lukkes med den type af aggregate root, som command’en adresserer – bemærk desuden at base-klassen kræver en ID på en aggregate root samt en Execute-metode, som fungerer som en mapning fra command’en til et eller flere funktionskald:

public class SqueezeCommand : Command {
    public SqueezeCommand(Guid aggregateRootId) : base(aggregateRootId) {
    }

    public decimal HowMuch { get; set; }

    public override void Execute(Beetroot aggregateRoot) {
        aggregateRoot.Squeeze(HowMuch);
    }
}

I dette tilfælde vil vores SqueezeCommand resultere i et kald til Squeeze-metoden på vores Beetroot. Så kan den lære det.

Event

Når rødbeden så bliver mast, så skal den benytte sig af Emit/Apply pattern, som er regelen for hvordan domænemodellen i et Cirqus-baseret system ændrer sin tilstand – det betyder at alle tilstandsændringer i en aggregate root udføres som konsekvens af påførelsen af et event, hvorfor vi må benytte et event til at ændre vores tilstand.

Forestil dig at rødbeden holder styr på hvor mast den er vha. en decimal:

public class Beetroot : AggregateRoot {
    decimal _squishiness = 1; //< just default to 1

    public void Squeeze(decimal howMuch) {
        // _squishiness -= howMuch; << NIXEN!!
    }
}

Nu siger Emit/Apply-regelen så at vi skal Emitte et event, og i den tilhørende Apply-metode må vi så ændre vores tilstand – det ser sådan ud:

public class Beetroot : AggregateRoot, IEmit<BeetrootSquashed> {
    decimal _squishiness = 1; //< just default to 1

    public void Squeeze(decimal howMuch) {
        Emit(new BeetrootSquashed { HowMuch = howMuch });
    }

    public void Apply(BeetrootSquashed e) {
        _squishiness -= e.HowMuch; //< sådan der!
    }
}

Emit/Apply pattern ser måske lidt fjollet ud ved første øjekast, men det har den essentielle egenskab at alle tilstandsændringer i domænemodellen er forårsaget af events, og KUN events! – på den måde sikrer vi at events er fyldestgørende til at beskrive alt hvad der sker, hverken mere eller mindre!

Og da events er sådan nogle små klumper af data, som kan gemmes i en database – i vores event store – så har vi faktisk alt hvad der skal til for at lave en model, hvis tilstand udelukkende er baseret på events.

Lad os lige gøre rødbeden færdig, sådan at den gør verden opmærksom på det når den er fuldstændig mast:

public class Beetroot : AggregateRoot,
    IEmit<BeetrootSquashed>,
    IEmit<BeetrootCompletelyCrushed> {
    decimal _squishiness = 1; //< just default to 1

    public void Squeeze(decimal howMuch) {
        if (howMuch <= 0) {
            var message = string.Format("Attempted to squeeze by {0}, but it must be positive!", howMuch);
            throw new ArgumentException(message, "howMuch");
        }

        // nothing happens when we're already crushed
        if (_squishiness <= 0) return;

        // emit crushed when we go below zero
        if (_squishiness - howMuch <= 0) {
            Emit(new BeetrootCompletelyCrushed());
            return;
        }

        // otherwise, just subtract how much
        Emit(new BeetrootSquashed { HowMuch = howMuch });
    }

    public void Apply(BeetrootSquashed e) {
        _squishiness -= e.HowMuch;
    }

    public void Apply(BeetrootCompletelyCrushed e) {
        _squishiness = 0;
    }
}

Nu har vi kigget på en AggregateRoot, en Command og en DomainEvent – i næste del vil jeg vise hvordan man med Cirqus kan bringe liv i vores Beetroot, sådan at vi kan bygge et system, der kan mase den ene rødbede efter den anden og samle vigtige stats i forbindelse hermed… stay tuned!

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *