Composite pattern en data types

Pagina: 1
Acties:

  • YopY
  • Registratie: September 2003
  • Laatst online: 06-11 13:47
Beste tweakerts en andersoortigen,

Ik ben op dit moment bezig met het schrijven van een refactoringsvoorstel voor een demo spel bij een bepaalde 3D engine (voor punten op school hoofdzakelijk), en ik zit met het volgende 'probleem'.

Binnen de engine wordt gebruik gemaakt van een berichtsysteem waarbij berichten tussen de server en clients gestuurd kunnen worden. Een bericht is altijd een subclass van Message, en bevat de gegevens die verstuurd moeten worden als class variables. Een bericht bevat altijd twee methoden, een Compress en een Decompress methode die de class variables in een Compressor resp. uit een Decompressor haalt:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
// pleurt gegevens in de compressor
void SomeMessageType::Compress(Compressor& data) const
{
    data << someClassVariable;
}

// haalt gegevens uit de decompressor in de class var.
bool SomeMessageType::Decompress(Decompressor& data)
{
    data >> someClassVariable;
    return true;
}


Dat is verder strak enzo. Het probleem (als je het echt een probleem wilt noemen, mijns insziens is het gewoon iets wat handiger / flexibeler kan) is hierbij dat voor elk type bericht je een nieuwe class moet schrijven. Dit heeft als resultaat dat je drie verschillende Message types hebt die, bijvoorbeeld, allemaal een long waarde versturen over het netwerk. Dit zou je op kunnen lossen door gewoon een LongMessage klasse te definieren en daar een bericht identifier aan mee te geven, echter dit is maar een gedeeltelijke oplossing.

Een betere, flexibelere en uiteindelijk minder 'arbeidsintensieve' manier is om een design pattern te gebruiken, de meest logische hierbij is het Composite design pattern, waar je, om het zo te beschrijven, een complex object samen kunt stellen uit eenvoudigere objecten.

In dit geval zat ik te denken aan een structuur waarbij er een berichttype (of berictttypeonderdeel) voor elke basiswaarde gedefinieerd wordt (een voor long, voor float etc), die gebruikt kunnen worden om complexere objecten samen te stellen die, bijvoorbeeld, posities in 3D ruimte doorgeven (die eigenlijk gewoon 3 long of float waarden zijn). Uiteindelijk komt er een verzameling functies die berichttypen samenstelt uit deze basisonderdelen (factory method / builder patroon).

Echter, ik loop vast op het weer ophalen van de gegevens uit een composiet bericht. Een composiet bericht bevat in dit geval een X aantal andere composiete berichten, die van het type LongMessage, BoolMessage of, complexer, Vector3DMessage kunnen zijn.

Mijn vraag is dus nu als volgt: Hoe kan ik netjes vaststellen welk type een bepaald berichtcomponent is, en zijn gegevens eruithalen? Met netjes bedoel ik dat ik liever niet een structuur wil hebben waarbij elk component zijn eigen identifier heeft (een numerieke waarde oid) die door een switch gehaald kan worden en zodanig gecast kan worden naar zijn specifieke type.

Zijn daar ook ideeen over? Andere oplossingen? Het basisidee is om een complex berichttype samen te stellen uit eenvoudigere componenten, maar de informatie moet ook weer uit deze componenten gehaald kunnen worden, zodanig dat een ontvanger ook iets aan deze informatie heeft.

De standaarddefinitie van een Composite design pattern houdt daar namelijk geen rekening mee, en definieert alleen dat een onderdeel van een compositie een bepaalde operatie heeft die uitgevoerd kan worden.

Verwijderd

Als het aantal Messages subclasses beperkt is en weinig zal veranderen, kan je het proberen op te lossen met een Visitor.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
virtual void Message::acceptVisitor(Visitor *foo) const = 0;

void  CompositeMessage::acceptVisitor(Visitor *foo) const {
  // for all parts, part->acceptVisitor(foo);
}

void LongMessage::acceptVisitor(Visitor *foo) const {
  foo->acceptLongMessage(this);
}

void BoolMessage::acceptVisitor(Visitor *foo) const {
  foo->acceptBoolMessage(this);
}


Je Visitor subclass zal dan zo'n CompositeMessage uit elkaar kunnen rafelen en hier iets zinvol mee kunnen doen.

  • YopY
  • Registratie: September 2003
  • Laatst online: 06-11 13:47
Hrm, zit iets in. Echter, het lijkt mij dat dat uiteindelijk precies dezelfde oplossing als het eerst was wordt. Je hebt een Message met daarin de gegevens, die kun je er direct uithalen dmv accessors. In jouw voorbeeld (lijkt mij tenminste) zou je een Visitor moeten aanmaken die de gegevens (longs e.d.) in zijn eigen structuur opslaat. Het kan zijn dat ik het niet helemaal goed begrijp, maar krijg je dan niet dat je voor elk type bericht ook weer een visitor aan moet maken waar je die gegevens uit moet halen? Dat je het uiteindelijke probleem uiteindelijk weer overneemt omdat je voor elk type bericht een nieuwe Visitor moet schrijven?

Het zou misschien wel kunnen werken, maar dan zou ik alle stukken code die iets met een bericht doen om moeten vormen zodat ze een Visitor object worden, die eerst alle gegevens uit een bericht opzoekt die het nodig heeft en deze vervolgens verwerkt. Echter dit lijkt mij veel werk, en behelst een volledige herstructurering van het programma zoals het nu is. Wat op zich niet erg is, maar ik wil het graag stukje bij beetje aanpassen (zodat het nog te overzien is).

Edit: Hrm, je zou in de Visitor arrays kunnen bijhouden van gegevens die het uit het complexe bericht haalt (zodanig dat als er 3 LongMessages in het complexe bericht staan, er 3 waarden in een array van de Visitor komen te staan). Functies als GetLongValue() of GetLongValue(int index) zouden vervolgens die gegevens uit de interne array halen, eventueel met interne indexen die de laatst bezochte Long waarde (de index daarvan in de interne array) bijhouden, zodat opeenvolgende aanroepen naar GetLongValue() telkens de 'volgende' long waarde uit het bericht halen.

Hrm. Een alternatief zou zijn dat je in de root van je bericht (oftewel het composite bericht, de node met een aantal subnodes met info) methodes toevoegt als "getLongValue()" of "getVector3DValue()", die elk de eerste long subnode die ze tegenkomen teruggeven. Bij de tweede aanroep geven ze de tweede long subnode terug, en als er geen subnode van type long is geven ze een null waarde terug of gooien ze een exception (ook al worden exceptions niet gebruikt in de engine, boeieuh). Wat vinden jullie daarvan? Is dat een 'slimme' oplossing of zijn er nog andere mogelijke oplossingen voor het samenstellen van een complex object?

[ Voor 14% gewijzigd door YopY op 18-06-2008 10:00 ]


  • ATS
  • Registratie: September 2001
  • Laatst online: 29-10 18:37

ATS

Je zou er ook eens over kunnen denken om de messages templated te maken. Daarbij kan je uitgaan van een basis class met wat algemene message eigenschappen (een typeID, een sender, ...), en een subclass die je als template definieert met de payload. Verschillende types messages kan je nu eenvoudig definiëren met een typedef als je dat leuk vindt. Afhankelijk van het delivery mechanisme kan je er vanuit gaan dat je ontvangende method prima weet hoe om te gaan met het type message dat hij geleverd krijgt. Zo niet, dan kan hij er toch niets zinnigs mee doen.

My opinions may have changed, but not the fact that I am right. -- Ashleigh Brilliant


Verwijderd

YopY schreef op woensdag 18 juni 2008 @ 09:21:
Hrm, zit iets in. Echter, het lijkt mij dat dat uiteindelijk precies dezelfde oplossing als het eerst was wordt. Je hebt een Message met daarin de gegevens, die kun je er direct uithalen dmv accessors. In jouw voorbeeld (lijkt mij tenminste) zou je een Visitor moeten aanmaken die de gegevens (longs e.d.) in zijn eigen structuur opslaat. Het kan zijn dat ik het niet helemaal goed begrijp, maar krijg je dan niet dat je voor elk type bericht ook weer een visitor aan moet maken waar je die gegevens uit moet halen? Dat je het uiteindelijke probleem uiteindelijk weer overneemt omdat je voor elk type bericht een nieuwe Visitor moet schrijven?
Neen. De bedoeling van Visitor is om custom behavior aan een hierarchy van types toe te voegen zonder die hierarchy te moeten extenden. Je hebt als het ware twee losstaande class-hierarchies: de nodes die gevisit worden, en een hierarchy van visitors die behavior aan deze nodes toevoegen (de Visitor subclasses). Het klassieke voorbeeld is een document-structuur (denk XML-achtige DOM), dewelke als een composite in elkaar is gezet. Nu wil men aan deze boom-structuur features toevoegen. Bijvoorbeeld: "tel het aantal woorden", "doe spellchecking", "doe uitlijning". Je zou in de top node een abstracte methode "count()" kunnen toevoegen, en in alle subclasses deze "count()" methode overriden om het correcte aantal woorden te returnen. Analoog voeg je "spellcheck()" en "align()" toe. Na drie keer alle classes van deze hiearchie te hebben moeten aanpassen om deze nieuwe (pure) virtual methodes te overriden om er voor die node iets zinvol mee te doen moet er een belletje gaan rinkelen. In plaats van methodes in de top node van de Composite hierarchy blijven toe te voegen is het beter deze nieuwe behaviors naar eigen objecten te trekken. Je voorziet dan één hook methode "visit(Visitor *v)", en elke subclass moet dan enkel "v->visitMe(this)" te doen. Wat het subtype van Visitor doet gaat je eigenlijk weinig aan. Zo kan je triviaal een Visitor maken die alle woorden optelt, spellcheckt, of wat dan ook.

code:
1
2
3
4
5
6
7
8
9
10
11
struct WordCounter : Visitor { 
  void visitParagraph(ParagraphElement pe) { /* ... */ }
  void visitPicture(PictureElement pe) { /* ... */ }
  void visitWord(WordElemen we) { /* ... */ }
};

struct SpellChecker : Vistor {
  void visitParagraph(ParagraphElement pe) { /* ... */ }
  void visitPicture(PictureElement pe) { /* ... */ }
  void visitWord(WordElemen we) { /* ... */ }
};


Je voegt dus nieuwe algoritmes/behaviors/features toe aan je document, zonder de nodes van je document te hoeven extenden. Je voegt gewoon een nieuwe Visitor toe, de Composite voorziet een hook methode om een willekeurige Visitor door de hele Composite structuur te trekken en elke node callbacks te laten maken naar de Visitor.
Hrm. Een alternatief zou zijn dat je in de root van je bericht (oftewel het composite bericht, de node met een aantal subnodes met info) methodes toevoegt als "getLongValue()" of "getVector3DValue()", die elk de eerste long subnode die ze tegenkomen teruggeven. Bij de tweede aanroep geven ze de tweede long subnode terug, en als er geen subnode van type long is geven ze een null waarde terug of gooien ze een exception (ook al worden exceptions niet gebruikt in de engine, boeieuh). Wat vinden jullie daarvan? Is dat een 'slimme' oplossing of zijn er nog andere mogelijke oplossingen voor het samenstellen van een complex object?
In je eerste post sprak je al over het Builder design pattern. Zonder je probleemstelling beter te bestuderen zou ik iets als het volgende proberen:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Message { virtual void vist(Visitor *v) = 0; };
class LongMessage : public Message { 
 void visit(Visitor *v) {
   v->acceptLong(this);
 }
};
 // andere messages

class Visitor {
  virtual void acceptBoolMessage(BoolMessage *bm) = 0;
  virtual void acceptLongMessage(LongMessage *lm) = 0;
  /* andere callbacks voor andere Message types (zie verder). */
};

class CheatChecker : public Visitor {

  void acceptBoolMessage(BoolMessage *bm) {
    if (!isExpected(bm) || !isPossible(bm)) { reportCheat(GameCtx *gm); }
  }
  /* andere methodes */
};

struct GameUpdate {
  double coords[][][];
  bool     shotsFired;
  string   message;
  /* andere zaken voor een game update */
};

struct GameUpdater : public Visitor {
  
  GameUpdater {
    update = new GameUpdate;
  }
  void acceptLongMessage(LongMessage *lm) {
    gu->stateA = lm->getLong();
  }
  void acceptCoordMessage(CoordMessage *cm) {
    gu->coords = cm->coords();
  }

  GameUpdate *gameUpdate() {
    return update;
  }

  private:
    GameUpdate *update;
};


Je hebt dus verschillende Visitors die de berichten aan allerlei controles onderwerpen. Nog een Visitor zal door de Message Composite doorgestuurd worden, en in zijn callbacks een GameUpdate opbouwen. Eens de traversal voorbij, kan aan de Visitor het GameUpdate object gevraagd worden en aan een Engine of wat dan ook doorgegeven worden om de state in je wereld te muteren.

Nu, het is belangrijk te onderstrepen dat Visitor slechts goed kan werken indien de bezochte klassehierarchie nauwelijks tot niet verandert, dat wil zeggen: dat er geen wijzigingen in de overervingen komen of vaak nieuwe klasses bijkomen. Gebeurt dit wel, dan moet de top Visitor class aangepast worden en álle Visitors moeten dan nieuwe methodes bijkrijgen om bezocht te worden door het nieuwe "Message" type (in dit geval). Liggen alle Messages echter vast en je wilt het mogelijk maken om clients de gekste dingen te laten doen met die messages: ga dan voor Visitor.

  • YopY
  • Registratie: September 2003
  • Laatst online: 06-11 13:47
Juist, het is mij allemaal duidelijk. Ik heb dit topic (of een variant daarvan) ook op het forum van de engine zelf geplaatst, en daar zijn ze er minder positief / behulpzaam over (ook aangezien ze het voordeel er niet helemaal van inzien - wat overigens begrijpelijk is). Ik heb het hele Visitor gebeuren gisteren nog eens uitgewerkt, en heb om te beginnen een soort van 'data container' Visitor ontworpen, die voor elk datatype een lijst met gegevens bijhoudt. Voor elk datatype is er ook een addValue en getValue methode gedefinieerd, die de gegevens ophaalt of juist wegschrijft, in een first in, first out methode.

Een composed bericht kun je vervolgens vullen met gegevens door een volgend stukkie code aan te roepen:

Visitor * insertionVisitor = new Visitor();
insertionVisitor->addVector3DValue(Vector3D(1.0F, 0.5F, 0.0F));
insertionVisitor->addLongValue(9001L); // long value 1
insertionVisitor->addLongValue(1337L); // long value 2
insertionVisitor->addFloatValue(0.642F);

message->FillVisit(insertionVisitor);

(waarbij in elke FillVisit methode van specifieke waarden een 'getXValue()' functie van de visitor aangeroepen wordt, die telkens de 'volgende' waarde van dat type in de lijst teruggeeft).

Ditzelfde wordt vervolgens toegepast op het weer ophalen van gegevens uit het composed bericht, waar dan de "setXValue()" vervangen wordt door een "getXValue" aanroep, zowel in de aanroepende code als de code binnen de berichtcomponenten.

Ik vindt dit zelf wel een goeie oplossing, en het is volledig compatible met het huidige systeem. De bovenstaande oplossing is ook goed, maar dan zijn er veel grotere aanpassingen in het bestaande systeem nodig (het is niet bepaald het toppunt van goed design, en er is nogal de nodige hoeveelheid code).

Maar dank u allen voor de uitleg, het is mij een stuk duidelijker en, belangrijker, ik heb er een sluitend verhaal van kunnen maken voor in mijn document. Het is uiteindelijk niet belangrijk of dit er wel of niet inkomt (aangezien wij daar geen tijd voor hebben, :'( ), maar dat ik er mijn punten voor krijg ;). Zoals gezegd vinden degenen op het forum van de engine er zelf niet zoveel van, maar dat is voor mij niet zo belangrijk.

Hartelijk dank!
Pagina: 1