2009-11-13 10 views
6

Bien, el contexto es un código de serialización/deserialización que analizará un flujo de bytes en una representación de 'objeto' que es más fácil de trabajar (y viceversa).Elegir la subclase correcta para crear una instancia programática

Aquí hay un ejemplo simplificado con una clase de base de mensaje y luego dependiendo de cabecera de un 'tipo', algunos datos más/función están presentes y hay que elegir la subclase derecho a una instancia:

class BaseMessage { 
public: 
    enum Type { 
     MyMessageA = 0x5a, 
     MyMessageB = 0xa5, 
    }; 

    BaseMessage(Type type) : mType(type) { } 
    virtual ~BaseMessage() { } 

    Type type() const { return mType; } 

protected: 
    Type mType; 

    virtual void parse(void *data, size_t len); 
}; 

class MyMessageA { 
public: 
    MyMessageA() : BaseMessage(MyMessageA) { } 

    /* message A specific stuf ... */ 

protected: 
    virtual void parse(void *data, size_t len); 
}; 

class MyMessageB { 
public: 
    MyMessageB() : BaseMessage(MyMessageB) { } 

    /* message B specific stuf ... */ 

protected: 
    virtual void parse(void *data, size_t len); 
}; 

En un verdadero ejemplos, habría cientos de tipos de mensajes diferentes y posiblemente varios niveles o jerarquías porque algunos mensajes comparten campos/funciones entre sí.

Ahora, para analizar una cadena de bytes, lo estoy haciendo algo como:

BaseMessage *msg = NULL; 
Type type = (Type)data[0]; 

switch (type) { 
    case MyMessageA: 
     msg = new MyMessageA(); 
     break; 

    case MyMessageB: 
     msg = new MyMessageB(); 
     break; 

    default: 
     /* protocol error */ 
} 

if (msg) 
    msg->parse(data, len); 

pero no encuentra este enorme interruptor muy elegante, y tengo la información acerca de qué mensaje tiene el que 'Tipo valor 'dos ​​veces (una vez en el constructor, una en este modificador) También es bastante larga ...

Estoy buscando una mejor manera que simplemente sería mejor ... ¿Alguien tiene alguna idea de cómo mejorar esta ?

Respuesta

4

Es una pregunta bastante básica, de hecho (como se puede imaginar, que está definitivamente no es el único que se deserializa en C++).

Lo que está buscando se llama Virtual Construction.

C++ no define la construcción virtual, pero es fácil de aproximar utilizando el patrón de diseño Prototype o mediante el método Factory.

Personalmente prefiero el enfoque Factory, porque el Prototype significa tener algún tipo de instancia predeterminada que se replica y ENTONCES se define ... el problema es que no todas las clases tienen un valor predeterminado significativo, y para ese caso , un significativo Default Constructor.

El enfoque Factory es bastante fácil.

  • Se necesita una clase base común para los mensajes, y otra para los analizadores
  • Cada mensaje tiene tanto una etiqueta y un analizador asociado

Vamos a ver algo de código:

// Framework 
class Message 
{ 
public: 
    virtual ~Message(); 
}; 

class Parser 
{ 
public: 
    virtual ~Parser(); 
    virtual std::auto_ptr<Message> parse(std::istream& serialized) const; 
}; 

// Factory of Messages 
class MessageFactory 
{ 
public: 
    void register(std::string const& tag, Parser const& parser); 
    std::auto_ptr<Message> build(std::string const& tag, std::istream& serialized) const; 
private: 
    std::map<std::string,Parser const*> m_parsers; 
}; 

Y con este marco (ciertamente simple), algunas clases derivadas:

class MessageA: public Message 
{ 
public: 
    MessageA(int a, int b); 
}; 

class ParserA: public Parser 
{ 
public: 
    typedef std::auto_ptr<MessageA> result_type; 
    virtual result_type parse(std::istream& serialized) const 
    { 
    int a = 0, b = 0; 
    char space = 0; 
    std::istream >> a >> space >> b; 
    // Need some error control there 
    return result_type(new MessageA(a,b)); 
    } 
}; 

Y por último, el uso:

int main(int argc, char* argv[]) 
{ 
    // Register the parsers 
    MessageFactory factory; 
    factory.register("A", ParserA()); 

    // take a file 
    // which contains 'A 1 2\n' 
    std::ifstream file = std::ifstream("file.txt"); 
    std::string tag; 
    file >> tag; 
    std::auto_ptr<Message> message = factory.parse(tag, file); 

    // message now points to an instance of MessageA built by MessageA(1,2) 
} 

Funciona, lo sé porque yo lo uso (o una variante).

hay algunas cosas a tener en cuenta:

  • usted puede estar dispuesto a hacer MessageFactory un conjunto unitario, esto entonces permite que se llama a la carga de la biblioteca, y por lo tanto se puede inscribir su analizadores creando instancias de variables estáticas. Esto es muy útil si no desea que main tenga que registrar cada tipo de analizador: localidad> menos dependencias.
  • Las etiquetas tienen que ser compartidas. Tampoco es inusual que la etiqueta sea servida por un método virtual de la clase Message (etiqueta llamada).

igual:

class Message 
{ 
public: 
    virtual ~Message(); 
    virtual const std::string& tag() const = 0; 
    virtual void serialize(std::ostream& out) const; 
}; 
  • La lógica para la serialización tiene que ser compartida también, no es inusual para un objeto de manejar su propia serialización/deserialización

igual:

class MessageA: public Message 
{ 
public: 
    static const std::string& Tag(); 
    virtual const std::string& tag() const; 
    virtual void serialize(std::ostream& out) const; 

    MessageA(std::istream& in); 
}; 

template <class M> 
class ParserTemplate: public Parser // not really a parser now... 
{ 
public: 
    virtual std::auto_ptr<M> parse(std::istream& in) const 
    { 
    return std::auto_ptr<M>(new M(in)); 
    } 
}; 

Lo bueno de t lo que está claro es que nunca deja de sorprenderme

class MessageFactory 
{ 
public: 
    template <class M> 
    void register() 
    { 
    m_parsers[M::Tag()] = new ParserTemplate<M>(); 
    } 
}; 

//skipping to registration 
    factory.register<MessageA>(); 

¿Ahora no es bonito :)?

10

Una forma de abordarlo sería usar un mapa y registrar algún tipo de función de fábrica para cada tipo de mensaje. Esto significa que se deshace de la caja del conmutador y puede agregar y eliminar mensajes dinámicamente.

El código sería algo como:

// Create the map (most likely a member in a different class) 
std::map<BaseMessage::Type, MessageCreator*> messageMap; 
... 

// Register some message types 
// Note that you can add and remove messages at runtime here 
messageMap[BaseMessage::MyMessageA] = new MessageCreatorT<BaseMessageA>(); 
messageMap[BaseMessage::MyMessageB] = new MessageCreatorT<BaseMessageB>(); 
... 

// Handle a message 
std::map<Type, MessageCreator*>::const_iterator it = messageMap.find(msgType); 
if(it == messageMap.end()) { 
    // Unknown message type 
    beepHang(); 
} 
// Now create the message 
BaseMessage* msg = it->second.createMessage(data); 

La clase MessageCreator sería algo como esto:

class MessageCreator { 
    public: 
    virtual BaseMessage* createMessage(void* data, size_t len) const = 0; 
}; 
template<class T> class MessageCreatorT : public MessageCreator { 
    public: 
    BaseMessage* createMessage(void* data, size_t len) const { 
     T* newMessage = new T(); 
     newMessage.parse(data, len); 
     return newMessage; 
    } 
}; 
+0

Enfoque muy interesante de hecho. El aspecto de la alteración del tiempo de ejecución no es realmente útil en mi caso específico, pero es una buena bonificación que podría ser útil en otros casos. Esperaré un poco para ver si alguien más está antes de confirmar como respondí. – 246tNt

+0

Obviamente, no puede agregar tipos de mensajes en tiempo de ejecución. De todos modos, la caja del interruptor se ha transformado en una línea por cada entrada de registro de tipo de mensaje en el mapa, no veo la gran ganancia aquí, ciertamente no hay menos cantidad de código resultante, ¿más eficiente? Menos desorden, probablemente? –

+0

No me preocupa mucho el rendimiento, no estoy procesando cientos de miles de mensajes por segundo, el estilo de código es más importante aquí. También el triunfo que veo aquí (aunque no sea obvio a partir de la pregunta), es que también puedo agregar otros métodos aparte de 'createMessage' si necesito otra cosa del tipo, como definir qué 'clase de control' manejará el mensaje y esas cosas. Para la alteración del tiempo de ejecución: puede modificar el mapa en tiempo de ejecución. Para agregar nuevos mensajes, puede descargarlos desde objetos compartidos como complementos y demás. – 246tNt

Cuestiones relacionadas