2012-04-11 8 views
2

Ich schreibe ein Tool, das es einem Benutzer ermöglicht, mit ein wenig Hardware zu interagieren, indem Einstellungen geändert und dann Informationen gestreamt werden.Wie kann ich Downcasting verhindern, wenn Informationen über eine Warteschlange übertragen werden?

Dazu habe ich ein paar Threads ausgeführt: EquipmentInterface und DataProcessor die durch eine Queue verbunden sind.

Die EquipmentInterface Gewinde Methoden Einstellungen am Gerät (Rotate und Refocus zum Beispiel) zu ändern und die sich ergebende Information (CurrentAngle und CurrentFocalDistance) zum Queue hinzugefügt wird. Sobald die Einstellungen korrekt sind, gibt es Methoden zu StartStreaming und StopStreaming und sobald Streaming beginnt, werden Daten von der Ausrüstung paketiert und in die Warteschlange aufgenommen.

Alle Informationen in der Warteschlange stammen von einer einzelnen Klasse BaseMessage, die eine Angabe des Nachrichtentyps enthält. Ich habe dann Nachrichtentypen für Winkel, Brennweiten, Anfangs- und End-Streaming und natürlich die Daten selbst abgeleitet.

Die DataProcessor hört auf das andere Ende der Warteschlange und verarbeitet abhängig von der aktuellen Winkel/Brennweite die nachfolgenden Daten.

Nun, die Sache ist, ich habe eine Funktion im Datenprozessor, der eine switch-Anweisung verwendet, um die eingehenden Nachrichten type-check zu überprüfen. Diese Nachrichten werden dann auf den entsprechenden Typ umgewandelt und an einen geeigneten Handler übergeben. In Wirklichkeit gibt es mehr als nur einen DataProcessor, der einer einzelnen Warteschlange zuhört, sondern tatsächlich mehrere Listener in mehreren Warteschlangen (einige speichern auf der Festplatte, andere zeigen Informationen auf einer GUI an). Jedes Mal, wenn ich einige Informationen hinzufüge, muss ich eine neue BaseMessage abgeleitete Klasse erstellen, einen neuen Typ zu dieser Basisklasse hinzufügen und dann die Switch-Anweisungen in jedem der Konsumenten aktualisieren, um mit der neuen Nachricht fertig zu werden.

Etwas über diese Architektur fühlt sich falsch an und ich habe in letzter Zeit eine Menge über Down-Casting gelesen. Nach dem, was ich gesehen habe, scheint der allgemeine Konsens der what I'm doing is a bad code smell zu sein. Ich habe gesehen a suggestion which use Boost, aber sie sehen nicht sauberer als die switch-Anweisung zu mir (vielleicht fehlt mir etwas?).

Also meine Frage ist: Sollte ich versuchen, die Switch-Statement/Downcasting-Lösung zu vermeiden und wenn ja, wie?

Meine Implementierung ist in C++/CLI, also bin ich entweder mit .net oder C++ - Lösungen.

bearbeiten - Basierend auf den Kommentaren von iammilind und stfaanv, das ist das, was Sie vorschlagen:

class QueuedItem 
{ 
public: 
    QueuedItem() { } 
    virtual ~QueuedItem() { } 

}; 

class Angle : public QueuedItem 
{ 
public: 
    Angle() {} 
    virtual ~Angle() { } 
}; 

class FocalLength : public QueuedItem 
{ 
public: 
    FocalLength() {} 
    virtual ~FocalLength() { } 
private: 

}; 


class EquipmentHandler 
{ 
protected: 
    virtual void ProcessAngle(Angle* angle) {}; 
    virtual void ProcessFocalLength(FocalLength* focalLength) {}; 

public: 
    void ProcessMessages(QueuedItem* item) 
    { 
     Angle* pAngle = dynamic_cast<Angle*>(item); 
     if(pAngle != NULL) 
     { 
      ProcessAngle(pAngle); 
     } 
     FocalLength* pFocalLength = dynamic_cast<FocalLength*>(item); 
     if(pFocalLength != NULL) 
     { 
      ProcessFocalLength(pFocalLength); 
     } 

    } 
}; 

class MyDataProcessor : public EquipmentHandler 
{ 
protected: 
    virtual void ProcessAngle(Angle* angle) override { printf("Processing Angle"); } 
    virtual void ProcessFocalLength(FocalLength* focalLength) override { printf("Processing FocalLength"); }; 
}; 


int _tmain(int argc, _TCHAR* argv[]) 
{ 

    // Equipment interface thread... 
    FocalLength* f = new FocalLength(); 
    QueuedItem* item = f; // This gets stuck onto the queue 

    // ...DataProcessor thread (after dequeuing) 
    QueuedItem* dequeuedItem = item; 

    // Example of a DataProcessor implementation. 
    // In reality, this would 
    MyDataProcessor dataProc; 
    dataProc.ProcessMessages(dequeuedItem); 

    return 0; 
} 

... und kann vereinfacht werden? Die ProcessMessages fühlt sich ein wenig klobig an, aber das ist die einzige Möglichkeit, die ich ohne eine switch-Anweisung und eine Art enumerierter Nachrichtentypkennung in der Basisklasse sehen könnte.

+1

Es scheint, als ob Sie dringend * dynamische Polymorphie und dynamischen Versand * brauchen. –

+0

Warum deklarieren Sie eine "virtuelle" Funktion in einer Basisklasse und implementieren Sie nicht alle untergeordneten Elemente. – iammilind

+0

@iammilind: Das würde sicher retten, die switch-Anweisung mehrmals zu implementieren, aber sie wird es nicht vollständig los. Ist das der effizienteste Weg zu bestimmen, welcher Handler angerufen werden soll? –

Antwort

2

Sie könnten ein Besucher-Entwurfsmuster versuchen: http://en.wikipedia.org/wiki/Visitor_pattern

Jeder Datenprozessor von einer BaseVisitor Klasse erben würde, die für den Umgang mit jeder Art von Nachricht virtuelle Methode definiert. Grundsätzlich sind diese Methoden einfach noop.

Wenn Sie einen neuen Nachrichtentyp definieren, fügen Sie eine neue virtuelle Methode mit einer Noop-Implementierung für diesen Nachrichtentyp in BaseVisitor hinzu. Dann, wenn ein Kind DataProcessor Klasse diesen Nachrichtentyp verarbeiten möchte, überschreiben Sie die virtuelle Methode nur in diesem DataProcessor. Alle anderen DataProcessor bleiben unberührt.

#include <iostream> 


    class FocalLength; 
    class Angle; 
    class EquipmentVisitor; 

    class QueuedItem 
    { 
    public: 
      QueuedItem() { } 
      virtual ~QueuedItem() { } 

      virtual void AcceptVisitor(EquipmentVisitor& visitor) = 0; 
    }; 

    class EquipmentVisitor 
    { 
    public: 
      virtual ~EquipmentVisitor() {} 

      virtual void Visit(FocalLength& item) {} 
      virtual void Visit(Angle& item)  {} 

      void ProcessMessages(QueuedItem* item) 
      { 
        item->AcceptVisitor(*this); 
      } 
    }; 

    class Angle : public QueuedItem 
    { 
    public: 
      Angle() {} 
      virtual ~Angle() { } 

      void AcceptVisitor(EquipmentVisitor& visitor) { visitor.Visit(*this); } 
    }; 

    class FocalLength : public QueuedItem 
    { 
    public: 
      FocalLength() {} 
      virtual ~FocalLength() { } 

      void AcceptVisitor(EquipmentVisitor& visitor) { visitor.Visit(*this); } 
    private: 

    }; 

    class MyDataProcessor : public EquipmentVisitor 
    { 
    public: 
      virtual ~MyDataProcessor() {} 

      void Visit(Angle& angle)    { std::cout << "Processing Angle" << std::endl; } 
      void Visit(FocalLength& focalLength) { std::cout << "Processing FocalLength" << std::endl; } 
    }; 


    int main(int argc, char const* argv[]) 
    { 
      // Equipment interface thread... 
      FocalLength* f = new FocalLength(); 
      QueuedItem* item = f; // This gets stuck onto the queue 

      // ...DataProcessor thread (after dequeuing) 
      QueuedItem* dequeuedItem = item; 

      // Example of a DataProcessor implementation. 
      // In reality, this would 
      MyDataProcessor dataProc; 
      dataProc.ProcessMessages(dequeuedItem); 

      return 0; 
    } 
+0

Etwas wie das Beispiel, das ich der Frage hinzugefügt habe? –

+0

Fantastisch! Dies ist eine wirklich saubere Lösung jetzt, da ich tatsächlich meinen Kopf herum wie es funktioniert hat :-) –

0

Sie können eine der folgenden tun:

Delegierter der Behandlungscode (wie in jeder case in Ihrer switch Anweisung) zu Handler Objekte - entweder eine Hierarchie von HandlerBase Objekte oder völlig unabhängig Typen.

Sie haben dann Ihre Nachrichten behalten einen Verweis auf die Handler Objekt (wenn es eine Hierarchie ist, können Sie es unterEbene, wenn nicht verwandte Objekte, dann als Teil der einzelnen spezialisierten Nachrichtentypen), zu denen Sie dann übergeben Sie sie, wenn Sie sie behandeln, durch eine BaseMessage::Handle() Methode. Edit: Diese Methode ist NICHT virtuell.

Natürlich, wenn Sie den Pfad der HandlerBase Hierarchie gehen, müssen Sie immer noch static_cast die Nachrichten zurück, was auch immer sie sind, aber das sollte in Ordnung sein: Sie sollten nur mit ihren eigenen Handler erstellt werden (das ist sollte ihre Typen sowieso kennen.

Beispiel:

// BaseMessage.hpp 
#include <iostream> 

class BaseMessage 
{ 
public: 
    BaseMessage(HandlerBase* pHandler); 
    : m_pHandler(pHandler) 
    {} 

    virtual ~BaseMessage() 
    {} 

    void SetHandler(HandlerBase* pHandler) 
    { 
    m_pHandler = pHandler; 
    } 

    void Handle() 
    { 
    assert(m_pHandler != 0); 
    m_pHandler->Handle(this); 
    } 

protected: 
    HandlerBase* m_pHandler; // does not own it - can be shared between messages 
}; 

// HandlerBase.hpp 
class HandlerBase 
{ 
public: 
    HandlerBase() 
    {} 

    virtual ~HandlerBase() 
    {} 

    virtual void Handler(BaseMessage* pMessage) =0; 
} 

// message and handler implementations 
class AMessage: public BaseMessage 
{ 
public: 
    AMessage(BaseHandler* pHandler) 
    : BaseMessage(pHandler) 
    {} 

    ~AMessage() {} 

    void DoSomeAness() 
    { 
    std::cout << "Being an A..." << std::endl; 
    } 
}; 

class AHandler 
{ 
public: 
    AHandler() 
    {} 

    virtual ~AHandler() 
    {} 

    virtual void Handle(BaseMessage* pMessage) 
    { 
    AMessage *pMsgA(static_cast<AMessage*>(pMessage)); 
    pMsgA->DoSomeAness(); 
    } 
}; 

class BMessage: public BaseMessage 
{ 
public: 
    BMessage(BaseHandler* pHandler) 
    : BaseMessage(pHandler) 
    {} 

    ~BMessage() {} 

    void DoSomeBness() 
    { 
    std::cout << "Being a B..." << std::endl; 
    } 
}; 

class BHandler 
{ 
public: 
    BHandler() 
    {} 

    virtual ~BHandler() 
    {} 

    virtual void Handle(BaseMessage* pMessage) 
    { 
    BMessage *pMsgB(static_cast<BMessage*>(pMessage)); 
    pMsgB->DoSomeBness(); 
    } 
}; 


// the thread 
static std::list<BaseMessage*> msgQueue; 

int HandlerThread(void *pData) 
{ 
    while(true) // find some more sophisticated way to break 
    { 
    while(!msgQueue.empty()) 
    { 
     msgQueue.front()->Handle(); 
     msgQueue.pop_front(); 
    } 
    // delay and stuff 
    } 
    return 0; 
} 

int main(int argc, char** argv) 
{ 
    start_thread(HandlerThread, 0); // your favorite API here 

    AHandler aHandler; 
    BHandler bHandler; 

    msqQueue.push_back(new AMessage(&aHandler)); 
    msqQueue.push_back(new BMessage(&bHandler)); 
    msqQueue.push_back(new BMessage(&bHandler)); 
    msqQueue.push_back(new AMessage(&aHandler)); 
    msqQueue.push_back(new AMessage(&aHandler)); 
    msqQueue.push_back(new BMessage(&bHandler)); 
    msqQueue.push_back(new AMessage(&aHandler)); 
    msqQueue.push_back(new BMessage(&bHandler)); 

    return 0; 
} 

Edit: ja, im Grunde ist dies die visitor pattern.

+0

Bedeutet das, dass jedes Objekt wissen muss (wann es erstellt wird), was damit zu tun hat? Können Sie ein konkretes Beispiel geben? –

+0

wurde ein Beispielcode hinzugefügt. die Objekte müssen nicht unbedingt über ihre Handler wissen, aber es wird ein Problem für SetHandler() sein, nachdem sie erstellt und der Warteschlange hinzugefügt wurden, hauptsächlich aufgrund der Funktionsweise von Warteschlangen und weil sie einfach als BaseMessage * erscheinen. s. Sie können eine andere Einrichtung hinzufügen, die Ihre Nachrichtentypen kennt, den Informationen zugeordnet ist und ihre Handler entsprechend einstellen kann. – zyndor

+0

@ iCE-9: Ihre Lösung scheint in der Mitte zwischen Nachrichtenversand und Doppelversand zu liegen, die beide eleganter sind und das Casting vermeiden. Wenn Ihre Nachrichten für einen Handler spezifisch sind, verwenden Sie einfach den spezifischen Handler in der Nachricht und führen Sie den richtigen Aufruf an den Handler über eine virtuelle handle() - Funktion aus. Wenn die Nachricht potenziell für einen Handler, aber mit unterschiedlichem Ergebnis verwendet wird, doppelter Dispatch durch eine virtuelle handle() (oder accept) -Funktion in der Nachricht und eine überladene (pro Nachricht) act() (oder visit) -Funktion im aufgerufenen Handler von der Funktion msg :: handle(). – stefaanv

0

Simplest Nachricht für das Senden von Nachrichten an 4 2 Menschen nach mir Handhabung:

#include <iostream> 
#include <queue> 
#include <memory> 

class HandlerA 
{ 
public: 
    void doA1() { std::cout << "A1\n"; } 
    void doA2(const std::string& s) { std::cout << "A2: " << s << "\n"; } 
}; 

class HandlerB 
{ 
public: 
    void doB1() { std::cout << "B1\n"; } 
    void doB2(const std::string& s) { std::cout << "B2: " << s << "\n"; } 
}; 


class BaseMsg 
{ 
public: 
    virtual ~BaseMsg() {} 
    void send(); 
    virtual void handle() { execute(); } 
    virtual void execute() = 0; 
}; 

typedef std::shared_ptr<BaseMsg> Msg; 
class Medium 
{ 
    std::queue<Msg> queue; 
public: 
    void send(Msg msg) { queue.push(msg); } 
    void process() 
    { 
    while (! queue.empty()) 
    { 
     std::cout << "Processing\n"; 
     queue.front()->handle(); 
     queue.pop(); 
    } 
    } 
}; 

class BaseMsgHndlrA : public BaseMsg 
{ 
protected: 
    HandlerA& ha; 
public: 
    BaseMsgHndlrA(HandlerA& ha_) : ha(ha_) { } 
}; 

class BaseMsgHndlrB : public BaseMsg 
{ 
protected: 
    HandlerB& hb; 
public: 
    BaseMsgHndlrB(HandlerB& hb_) : hb(hb_) { } 
}; 

class MsgA1 : public BaseMsgHndlrA 
{ 
public: 
    MsgA1(HandlerA& ha_) : BaseMsgHndlrA(ha_) { } 
    virtual void execute() { ha.doA1(); } 
}; 

class MsgA2 : public BaseMsgHndlrA 
{ 
public: 
    MsgA2(HandlerA& ha_) : BaseMsgHndlrA(ha_) { } 
    virtual void execute() { ha.doA2("Msg A2"); } 
}; 

class MsgB1 : public BaseMsgHndlrB 
{ 
public: 
    MsgB1(HandlerB& hb_) : BaseMsgHndlrB(hb_) { } 
    virtual void execute() { hb.doB1(); } 
}; 

class MsgB2 : public BaseMsgHndlrB 
{ 
    std::string s; 
public: 
    MsgB2(HandlerB& hb_, const std::string s_) : BaseMsgHndlrB(hb_), s(s_) { } 
    virtual void execute() { hb.doB2(s); } 
}; 

int main() 
{ 
    Medium medium; 
    HandlerA handlerA; 
    HandlerB handlerB; 

    medium.send(Msg(new MsgA1(handlerA))); 
    medium.send(Msg(new MsgA2(handlerA))); 
    medium.send(Msg(new MsgB1(handlerB))); 
    medium.send(Msg(new MsgB2(handlerB, "From main"))); 

    medium.process(); 
} 

Dies nutzt nur virtuelle Funktionen mit einigen Parametern auf den rechten Seite Handler zu versenden.
Die Funktion handle() ist nicht unbedingt erforderlich, aber hilfreich beim Definieren der Nachrichtenhierarchie.
Eine generische Nachricht könnte eine std :: -Funktion enthalten, die mit bind gefüllt werden kann, so dass tatsächliche Funktionen mit Parametern gesendet werden können, anstatt eine Nachrichtenklasse pro eingereihter Aktion zu erstellen.
Um das eigentliche Senden zu verbergen, können die Handler das Senden selbst durchführen, so dass sie sofort vom sendenden Thread aus erreichbar sind.

Wenn jedoch mehrere Nachrichten an mehrere Handler gesendet werden müssen, kann der doppelte Versand (Besucher) verwendet werden.

+0

Wie ich in der Frage sagte, umfasst die vollständige Anwendung mehrere Nachrichten (alle von einer gemeinsamen Basis abgeleitet) an eine Vielzahl gesendet werden von Handlern. –

+0

Und das beantwortet Ihre Frage (ich zeigte zwei Handler, auf die die Nachrichten zugreifen).Wie ich bereits bemerkt habe, haben Sie die Wahl zwischen dieser Einzelversandlösung und der Double-Dipatch-Lösung von fjardon und sogar einer Casting-Lösung, also ist es Ihr Anruf und anscheinend der doppelte Versand. – stefaanv