Hoe ontwerp ik een goed event?

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • 4Real
  • Registratie: Juni 2001
  • Laatst online: 14-09-2024
Ik heb een vraag m.b.t. het ontwerpen van events, waar ik namelijk vastloop is wat je in een event stopt en wat mogelijke metadata kan zijn. Om hier een beetje mee te spelen heb ik een CQRS+Event sourcing constructie opgezet, waarbij commands worden uitgevoerd op een model en deze events creëert die worden opgeslagen in een soort event store. Met deze events wordt later een read model gemaakt. Dit om een beetje te spelen met de concepten en te ondervinden wat er in een event moet komen, zodat later het read model goed opgebouwd kan worden (of opnieuw afgespeeld te worden in een ander model).

Voor nu heb ik de use case genomen van een takenlijst waarbij gebruikers een taak kunnen maken, hierop (en op die van andere) kunnen reageren en de staat kunnen aanpassen (bv, starten, in de wacht zetten, afronden of terugtrekken). Als ze de staat veranderen dan kunnen ze dit met of zonder commentaar doen. Dus als ik naar de requirement kijk van dat een gebruiker op een taak kan reageren dan lijkt het mij aardig simpel. Er moet een functie komen waarbij iemand een commentaar geeft op de taak, dus:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Task extends Aggregateroot
{
    public function addComment(ValueObject\User $commentator, string $comment)
    {
        if ($commentator === null) {
            throw new \Exception("Attribute: 'commentator' may not be null.");
        }

        if (empty($comment)) {
            throw new \Exception("Attribute: 'comment' may not be an empty string.");
        }

        $this->applyChange(new Event\CommentAddedEvent(
            Guid::newGuid(), $commentator->getId(), $comment
        ));
    }
}

Het ValueObject\User bevat eigenlijk een referentie naar een AggregateRoot van de context User, op dit moment alleen een UserId, omdat meer opslaan niet nodig is. En uiteraard de string waarin de content van de reactie zit. In het event zitten dus deze twee waardes, samen met de GUID van het commentaar (of de class Task verantwoordelijk is voor het genereren van dit Id, daar heb ik ook nog twijfels bij). Maar het event geeft in ieder geval aan, wie heeft een reactie geplaatst en wat is de content hiervan.

Echter, in de front-end wil ik ook wel een datum/tijd plaatsen om aan te geven wanneer deze reactie is geplaatst. Nu kan dit in het event, maar dit wordt ook als metadata opgeslagen bij het event. Wat is nu het beste om hierbij te doen? Om de datum mee te nemen in het event, of kan het er buiten, want voor het model boeit hier echt iets.

En dan situatie nummer twee, een gebruiker veranderd de staat van een event en geeft hierbij aan waarom. Als ik gebruik maak van de metadata dan kom ik met de volgende constructie weg:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Task extends Aggregateroot
{
    public function startWithComment(string $comment)
    {
        // logic for valid state change
        
        if (empty($comment)) {
            $this->applyChange(new Event\TaskStartedEvent());
        } else {
            $this->applyChange(new Event\TaskStartedEvent(Guid::newGuid(), $comment));
        }
    }
}

Als de comment een waarde heeft, dan wordt er in het event wat meer informatie opgeslagen, anders alleen het event dat de taak is gestart. Opzich hoeft de taak niet echt te weten wie de taak start, maar stel ik wil het op een tijdlijn zichtbaar krijgen dan wil ik wel weten wie dat heeft gedaan. Dus de gebruiker uit de metadata halen dat is dan wel voldoende.

Echter als ik later de requirement ga vaststellen: "Gebruikers mogen alleen hun eigen reactie's wijzigen", dan heb ik die informatie wel in mijn domein model nodig. Dus zal het ook weer een onderdeel moeten worden van mijn events. Is het dan zo dat als je het in je domein model nodig hebt, dat het daardoor ook leefrecht heeft in je events? En zoja, als ik dit event dan modelleer is structuur hierbij dan van belang? Het hele comment is eigenlijk een toevoeging aan het event, maar zijn wie commentaar geeft is niet direct een parameter van de staat verandering. Dus als voorbeeld:
code:
1
2
3
4
5
class TaskStartedEvent
{
    private $occuredOn; // datetime
    private $comment; // object met als attribute: id, userId, content
}

Ergens zou ik de structuur terug willen zien, want dan weet je ook waar de attributen bij horen. Als ik hierboven kijk dan weet ik dat het Id bij het reactie hoort, dat bij de gebeurtenis is aangeleverd en niet verward wordt door dat hij op een verkeerd niveau staat. Of is deze structuur een overkill voor wat het event allemaal te vertellen heeft?

Zoals je ziet heb ik nogal wat vragen over hoe je een event opbouwt. Hoe maak je de beslissing wat er in een event komt en wat niet? Is het gebruik van metadata een correcte, of is dit eerder een bad-practice? Mogen events complexe structuren bevatten, of kan het best met een platte structuur functioneren? Zijn er regels hoe je events moet opbouwen, of best practices? Want zelfs op z'n relatief simpel voorbeeld loop ik op sommige vlakken vast.

Alle reacties


Acties:
  • 0 Henk 'm!

  • Rowwan
  • Registratie: November 2000
  • Nu online
Meestal maakt het niet zoveel uit wat je in een event stopt, of wat je als meta data opvraagt. Mocht je meerdere listeners op een event hebben, dan is het mijns inziens het beste om de gemeenschappelijk benodigde data direct in het event te stoppen en de rest weg te laten.

In andere situaties is het bijna noodzakelijk om data in het event zelf te stoppen, e.g. als je getimed event hebt wat op de nano seconde nauwkeurig moet worden geregistreerd (ik noem maar wat), dan wil je die timestamp rechtstreeks in het event hebben, en niet later nog met een GetTime moeten ophalen

Acties:
  • 0 Henk 'm!

  • Stoppel
  • Registratie: Januari 2006
  • Laatst online: 01-10 18:23

Stoppel

een diedudabist

En bedenk dat het vaak niet erg is om data toe te voegen aan een event (op een later tijdstip) maar het weghalen van data uit een event is erg lastig omdat je niet weet wie allemaal je event consumeerd en dus ook niet weet welke dependancy een subscriber heeft op die data.

Wij zetten een timestamp vaak in het event wat bij asynchrone verwerking wel fijn kan zijn. Je kan er ook voor kiezen om het als eigenschap in de envelop te zetten. Keuzes...

Je had een hele lap tekst en heb het niet helemaa 100% doorgelezen maar volgens mij was dit een beetje je vraag.

Beauty is in the eye of the beholder


Acties:
  • 0 Henk 'm!

  • Juup
  • Registratie: Februari 2000
  • Niet online
4Real schreef op dinsdag 10 december 2019 @ 21:38:
PHP:
1
            throw new \Exception("Attribute: 'commentator' may not be null.");
Kleine note: buitenlanders begrijpen deze zin niet: er staat nu 'commentator' zou niet-null kunnen zijn.

Een wappie is iemand die gevallen is voor de (jarenlange) Russische desinformatiecampagnes.
Wantrouwen en confirmation bias doen de rest.


Acties:
  • 0 Henk 'm!

  • Corniel
  • Registratie: April 2002
  • Laatst online: 31-03 14:56

Corniel

De wereld is gek!

4Real schreef op dinsdag 10 december 2019 @ 21:38:
Ik heb een vraag (..) (of opnieuw afgespeeld te worden in een ander model).
Een ander model? Dat lijkt me onjuist. Een event is event binnen een domijn, en opnieuw afspelen van een event is bedoeld om bij het opnieuw afspelen in dezelfde staat terecht te komen als toen het werd gecreeërd. Dit lijkt misschien muggenzifterij, maar het legt een fundamenteel onderdeel van CQRS bloot: events v.s. domain events. De eerste zijn enkel bedoeld om een aggregate root bij opnieuw afspelen in de juiste staat te krijgen, de tweede, worden gepubliceerd om wereld buiten de aggregate. De eeste worden opgeslagen in een event store de tweede worden gepubliseerd op een bus. De tweede bestaat uiteraard niet zonder de eerste.
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Task extends Aggregateroot
{
    public function addComment(ValueObject\User $commentator, string $comment)
    {
        // (..)

        if (empty($comment)) {
            throw new \Exception("Attribute: 'comment' may not be an empty string.");
        }

        $this->applyChange(new Event\CommentAddedEvent(
            Guid::newGuid(), $commentator->getId(), $comment
        ));
    }
}
Als je een event al een eigen identifier wilt geven (ID van de Aggregate Root en de Version van het event zijn uniek) zou ik dit een envelope stoppen, en dat door je applyChange() methode laten afhandelen. Of je de actor in je event dan wel envelope wilt opslaan is afhankelijk van wat je er mee wilt, maar in de meeste gevallen hangt er geen domein-logica aan vast (wat pleit voor de envelope). Dat je dit in het domain event wilt publiseren staat los van die keuze.

Ik zou dus geen if-statement doen, tenzij je ander event wilt dispatchen als het $comment empty is.
in de front-end wil ik ook wel een datum/tijd plaatsen om aan te geven wanneer deze reactie is geplaatst. (..)
Dat klinkt alsof het onderdeel is van het domein; dus onderdeel van het event. Kijk als het al in je envelope (Meta data) zit, kan je het skippen, omdat je er als je domain events gaat publiceren bij kan, maar het is er onderdeel van, dus dan wil je er ook in je domein bij kunnen, zelfs als je er nu nog geen constrains voor hebt.
En dan situatie nummer twee, een gebruiker veranderd de staat van een event (..)
Ik neem aan dat je dit verkeerd getypt hebt, en bedoelde: de staat van de Task, want events zijn imutable.
Als de comment een waarde heeft, dan wordt er in het event wat meer informatie opgeslagen, anders alleen het event dat de taak is gestart. Opzich hoeft de taak niet echt te weten wie de taak start, maar stel ik wil het op een tijdlijn zichtbaar krijgen dan wil ik wel weten wie dat heeft gedaan. Dus de gebruiker uit de metadata halen dat is dan wel voldoende.
Wederom, als je het op het scherm wilt tonen, is het onderdeel van je domein, en hoort het in je event. Zo'n envelope (meta data) is bedoelt het opslaan van je events makkelijker te maken, misbruik dat dus niet voor andere doeleinden.
Is het dan zo dat als je het in je domein model nodig hebt, dat het daardoor ook leefrecht heeft in je events?
Je state wordt in CQRS volledig beschreven in events. Immers, anders kan je na een replay je oorspronkelijke state niet terug krijgen.
En zoja, als ik dit event dan modelleer is structuur hierbij dan van belang? Het hele comment is eigenlijk een toevoeging aan het event, maar zijn wie commentaar geeft is niet direct een parameter van de staat verandering.
Zolang alleen de user die een Task gemaakt heeft, deze mag weizigen, zou het redundante info zijn, en dus niet van belang, als dat niet zo is, is het onderdeel van het event, want events zijn zelfbeschrijvend.

[quote]Dus als voorbeeld:
PHP:
1
2
3
4
5
class TaskStartedEvent
{
    private $occuredOn; // datetime
    private $comment; // object met als attribute: id, userId, content
}


Dit lijkt me raar. Volgens mij kom je uit op iets als:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
class EventMessage
{
    $aggrateId; // Guid
    $version; // int, version
    $event; // some event object
}

class TaskStarted // Zeker als je een namespace voor je events, is zo'n Event-postfix ruis.
{
    $startDate; // Date-time
    $starter; // Guid, id of user who started.
    $comment; // optional comment
}


Waarbij je Aggregate Root die EventMessage aanmaakt op het moment dat jij de apply aanroept met TaskStarted.

Ik hoop dat ik e.a. helder heb kunnen uitleggen. Even kort samengevat:
  1. Events zijn zelfbeschrijvend
  2. Events zijn wezenlijk verschillend van Domain Events Die op basis van events eventueel gepubliceerd worden om buiten het domein te worden opgepikt
  3. Events zijn enkel bedoeld voor één domein
  4. Zaken in de evenlope (meta-data) zijn bedoeld voor storage en geen onderdeel van het domein
  5. Events zijn zoveel mogelijk een platte structuur

while (me.Alive) {
me.KickAss();
}