Designfrage für eine Event-Klasse



  • Hallo zusammen, ich grüble grade über dem Design einer Komponente meines aktuellen Projektes, es geht dabei um Events, die durch die Schichten meines Frameworks geschickt werden.
    Die Events beinhalten zum Einen ein Enum, das die Art des Events anzeigt, zum Anderen optional Daten, die je nach Eventart unterschiedlichen Typs sein können.
    Die Events sollen folgendes können:
    - Serialisierung/Deserialisierung, um übers Netzwerk verschickt zu werden
    - einfacher Vergleich von Events
    - einfaches Erzeugen der Events, möglichst unkomplizierte Extraktion der Daten
    - möglichst zentrale Möglichkeit, die Zuordnung Eventart->Datentyp festzulegen, ohne zig Stellen im Code ändern zu können.
    - die Events müssen von einem thread zu einem anderen weitergereicht werden können

    Mein erster Ansatz war, die Daten in ein boost::any zu stopfen, aber dann muss ich zusätzlich eine LUT anlegen, die mir sagt, wie ich aus Events der Art X die Daten rauskriege, ggf. vergleiche, (de-)serialisiere usw. Scheint mir ziemlich kompliziert. Ich hab grade ein Brett vorm Kopf, hat jemand eine elegante Idee?



  • Wie wäre es mit einer union?

    union MetaData
    {
        Point p;
        int i;
        Target t;
        ...
    };
    

    Das Event könnte dann in etwa so aufgebaut sein:

    struct Event
    {
        EventType type;
        MetaData data;
    };
    

    Edit: Alternativ vielleicht ein boost::variant. Allerdings wirds hier doch ein bisschen komplizierter durch das Visitor-Pattern.



  • 314159265358979 schrieb:

    Wie wäre es mit einer union?

    union MetaData
    {
        Point p;
        int i;
        Target t;
        ...
    };
    

    Find ich hässlich, dann müsste ich jedesmal, wenn ich eine neue Eventart mit anderen Daten einbaue, die in das Union einpflegen, an anderer Stelle den Zugriff einbauen usw.
    Dann lieber ein boost::any und eine Map mit Instanzen einer Datahandler-Klasse, die für jede Eventart weiß, wie sie die Daten zu extrahieren/vergleichen/serialisieren hat...

    boost::variant schau ich mir mal an, kenne ich noch nicht. Danke für die Tips...

    /edit: mal ganz abgesehen davon, dass unions nur PODs beinhalten dürfen...



  • variant hat leider das selbe Problem 😞
    Ich hab noch eine Anforderung vergessen, werde sie mal oben einfügen:
    - die Events müssen von einem thread zu einem anderen weitergereicht werden können.



  • pumuckl schrieb:

    /edit: mal ganz abgesehen davon, dass unions nur PODs beinhalten dürfen...

    Ich dachte dir steht C++0x zur Verfügung 😉

    Mir schwebt gerade sowas vor, aber es fällt mir schwer, das zusammenzufassen:

    struct event_base
    {
        const event_type type;
        const int size; // Größe der Metadaten
    
        virtual void read(your_stream_type& is) = 0;
        virtual void write(your_stream_type& os) = 0;
    
    protected:
        event_base(size_t size);
    };
    
    template <typename... Args> // Falls andrescu zu viel arbeit: typ-liste
    class typesafe_event
        : public event_base
        , public andrescu_like_generator<Args...> // GenScatteredHierarchy oder wie das Teil heißt, hier Umwandlung von variadic template nach typ-liste erforderlich, evtl code-generator neu schreiben als variadic template
    {
        // Lesen/Schreiben sizeof(*this) Bytes, können für non-PODs überschrieben werden
        virtual void read(your_stream_type& is);
        virtual void write(your_stream_type& os);
    
        template <int N>
        nth_type_of_args& meta(); // Gibt Metadaten zurück, meta<0> gibt den ersten Typ, meta<1> den zweiten, usw...
    };
    

    Events werden dann von typesafe_event abgeleitet:

    struct enemy_hit : public typesafe_event<int, int> // id, damage z.B.
    {};
    

    Edit, ach, bin ich blöd. Man könnte typesafe_event<Args...> natürlich auch einfach von std::tuple<Args...> ableiten, dann kannst du deine Events wie ein tuple verwenden. get<N>() kommt dann gratis.



  • Zunächst nehme ich an, die Events haben immer den gleichen Typ, also kommt ein Klassentemplate mit verschiedenen Metadaten als Templateparameter nicht in Frage. Wäre nämlich praktisch.

    Hm, ist sogar praktisch. Ich rate zu Type Erasure mit virtuellen Funktionen. Du baust zwar zu einem Teil boost::any nach, aber kannst zusätzliche Funktionalität einbauen, ohne die Daten jeweils extrahieren zu müssen.

    // Basisklasse mit Type Erasure
    struct Data
    {
    	virtual void Serialize(Archive& x) = 0;
    	virtual bool IsEqual(const Data& rhs) const = 0;
    	virtual ~Data() {}
    };
    
    // Konkrete Klasse, die Daten hält
    template <typename D>
    struct ConcreteData : Data
    {
    	ConcreteData(const D& data)
    	: data(data)
    	{
    	}
    
    	virtual void Serialize(Archive& x)
    	{
            // Z.B. freie Funktion aufrufen
            DoSerialize(x, data);
    	}
    
    	virtual bool IsEqual(const Data& rhs) const
    	{
            // Setzt gleiche Typen voraus
            return data == static_cast<const ConcreteData<D>&>(rhs).data;
    	}
    
    	D data;
    };
    
    // Dein Event-Frontend mit Wertsemantik
    class Event
    {
    	public:
        template <typename D>
        Event(EventType type, const D& data)
        : mType(type)
        , mData(new ConcreteData<D>(data))
        {
        }
    
        void Serialize(Archive& x)
        {
        	mData->Serialize(x);
        }
    
    	private:
            EventType          mType;
            CopiedPtr<Data>    mData;
    
    	friend bool operator== (const Event& lhs, const Event& rhs)
    	{
            return lhs.mType == rhs.mType && lhs.mData->IsEqual(*rhs.mData);
    	}
    };
    

    Hier noch ohne Extraktion à la any_cast , das wäre aber nicht allzu schwierig typsicher hinzukriegen. Geht hier sogar ohne RTTI, wenn du dich auf die EventType -Enumeratoren verlassen kannst. Ausserdem habe ich D absichtlich als Templateparameter genommen, statt ihn direkt von einem Interface zu erben. Auf diese Weise kannst du mit ganz normalen Typen arbeiten, die z.B. operator== unterstützen.

    CopiedPtr ist ein intelligenter Smart-Pointer mit Kopiersemantik, so funktioniert alles automatisch, ohne irgendwelchen Big-Three-Boilerplatecode und vor allem ohne Clone() . Ich hätte zufälligerweise noch einen auf Lager 😉



    1. Wie viele unterschiedliche Event Typen gibt es?
    2. Werden später (viele) neue Typen dazukommen?
    3. Wozu werden die Events benutzt?
    4. Wie viele Operationen werden mit den Events durchgeführt?
    5. werden später (viele) neue Operationen dazukommen?

    Wenn es nur wenige Event Typen gibt und auch nur wenig neue hinzukommen, wäre dann eine Objektfabrik in Verbindung mit dem Besuchermuster eine Lösung?

    PS:
    Was genau sind diese "optionalen" Daten? Sind es tatsächlich optionale Daten, d.h. ein Event Typ kann beliebig viele Nutzdaten beeinhalten?



  • 314159265358979 schrieb:

    Ich dachte dir steht C++0x zur Verfügung 😉

    Nur ein paar Brocken (MSVC2010)
    @Nexus: gefällt mir 🙂 Grob etwas in der Richtung hatte ich mal, allerdings hab ich da Event<C> von EventBase abgeleitet und einfach shared_ptr<EventBase> durch die Gegend gekegelt.

    Auf die EventType-enumeratoren kann ich mich dann verlassen, wenn ich den template-Ctor umgestalte:

    template <Eventtype evtType>
    struct DatatypeMapper;
    
    template <>
    struct DatatypeMapper<SOME_EVENT_TYPE>
    {
      typedef int type;
    };
    
    //usw...
    
    template <Eventtype evtType>
    Event::Event(DatatypeMapper<evtType>::type const& data)
      : mType(evtType)
      , mData(new ConcreteData<DatatypeMapper<evtType>::type>(data))
      {
      }
    


  • DocShoe schrieb:

    1. Wie viele unterschiedliche Event Typen gibt es?

    Aktuell 3 oder 4, ich bau grad den ersten Prototypen der noch garnichts kann.

    DocShoe schrieb:

    1. Werden später (viele) neue Typen dazukommen?

    Ja.

    DocShoe schrieb:

    1. Wozu werden die Events benutzt?

    Es sind eigentlich 3 verschiedene Eventklassen, die aber alle nach dem Schema aufgebaut sein werden:
    Ich habe eine Client-Server-Architektur, bei der mehrere Clients mit einem Server verbunden sind. Im Server selbst läuft eine Nutzerspezifische Logik-Komponente. Die Events gehen einmal vom Client zum Server (deshalb die Serialisierung übers Netzwerk), einmal vom Server zum Client, einmal vom Server in die Logik-Komponente. Die Events sind dabei jeweils asynchrone Benachrichtigungen, (z.B. dass ein Client eine Taste gedrückt hat, dass im Server sich ein Wert oder Zustand geändert hat etc.) die je nach Benachrichtigungsart auch Daten beinhalten müssen (welche Taste gedrückt wurde, wie der neue Wert im Server ist etc.)
    Sinn der Event-Klasse ist dabei, die Schnittstellen relativ schmal zu halten.

    DocShoe schrieb:

    1. Wie viele Operationen werden mit den Events durchgeführt?

    Was verstehst du unter Operationen? Erzeugen, durch die Gegend reichen, serialisieren, deserialisieren, weiter rumreichen, verarbeiten?

    DocShoe schrieb:

    1. werden später (viele) neue Operationen dazukommen?

    Wenn ich die Operationen richtig verstanden habe nein.

    DocShoe schrieb:

    Wenn es nur wenige Event Typen gibt und auch nur wenig neue hinzukommen wäre dann eine Objektfabrik in Verbindung mit dem Besuchermuster eine Lösung?

    Fällt weg, denke ich.

    DocShoe schrieb:

    PS:
    Was genau sind diese "optionalen" Daten? Sind es tatsächlich optionale Daten, d.h. ein Event Typ kann beliebig viele Nutzdaten beeinhalten?

    Optional soll heißen, dass für einige Eenttypen keine Daten nötig sind. Der Typ der Daten ist pro Eventtyp-Enumerator immer gleich, d.h. es kann ein Compilezeit-Mapping Enumerator->Typ geben. Der Typ kann aber eben auch void, also keine Daten sein. Fragen beantwortet?



  • pumuckl schrieb:

    template <Eventtype evtType>
    Event::Event(DatatypeMapper<evtType>::type const& data)
      : mType(evtType)
      , mData(new ConcreteData<DatatypeMapper<evtType>::type>(data))
      {
      }
    

    Hier kann der Compiler wahrscheinlich das Template-Argument nicht herleiten. Aber du kannst es umgekehrt machen:

    template <typename D>
    Event(const D& data)
    : mType(Mapper<D>::value)
    , mData(new ConcreteData<D>(data))
    {
    }
    

    Indem du Mapper nicht für alle Typen spezialisierst, erhältst du auch gleich einen Compiletime-Fehler bei unbekanntem Typ D .

    pumuckl schrieb:

    Grob etwas in der Richtung hatte ich mal, allerdings hab ich da Event<C> von EventBase abgeleitet und einfach shared_ptr<EventBase> durch die Gegend gekegelt.

    shared_ptr würde ich nicht nehmen, da dann die Events voneinander abhängen. Fände ich jedenfalls als Benutzer unintutiv, wenn sich beim Laden eines Events plötzlich auch ein anderer Event ändert. Daher der Vorschlag mit kopierbarem Smart-Pointer, den finde ich generell recht praktisch 🙂



  • Nexus schrieb:

    Aber du kannst es umgekehrt machen:

    Nur wenn die Zuordnung Typ -> Enumerator eindeutig ist, was nicht der Fall sein muss/wird.
    Ich kann aber den Ctor privat machen und eine creator-Funktion zum friend

    class Event
    {
      private:
        template <typename D>
        Event(EventType type, const D& data)
        : mType(type)
        , mData(new ConcreteData<D>(data))
        {
        }
    
      public:
        friend template <EventType et>
        Event createEvent(typename DatatypeMapper<et>::type const& data)
        {
          return Event(et, data);
        }
    
        /* ... */
    };
    

Log in to reply