代码之家  ›  专栏  ›  技术社区  ›  246tNt

选择正确的子类以编程方式实例化

  •  6
  • 246tNt  · 技术社区  · 15 年前

    好的,上下文是一些序列化/反序列化代码,这些代码将把字节流解析为更易于使用的“对象”表示形式(反之亦然)。

    下面是一个带有基本消息类的简化示例,然后根据“type”头,还存在更多的数据/函数,我们必须选择正确的子类进行实例化:

    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);
    };
    

    在一个真实的例子中,会有数百种不同的消息类型,可能还有几个级别或层次结构,因为有些消息彼此共享字段/函数。

    现在,为了解析一个字节字符串,我要做的事情如下:

    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);
    

    但是我觉得这个巨大的开关并不优雅,我有关于哪个消息有两次“类型值”的信息(一次在构造函数中,一次在这个开关中) 它也相当长…

    我正在寻找一种更好的方法,那会更好…如何改进?

    2 回复  |  直到 5 年前
        1
  •  4
  •   Matthieu M.    15 年前

    事实上,这是一个非常基本的问题(你可以想象,你绝对不是唯一一个在C++中反序列化)。

    你要找的是虚拟建筑。

    C++没有定义虚拟结构,但很容易使用 Prototype 设计图案或使用 Factory 方法。

    我个人更喜欢 工厂 接近,因为 原型 一种是有某种类型的默认实例被复制然后定义…问题是并非所有类都有一个有意义的默认值,因此,有意义的 Default Constructor .

    这个 工厂 方法很简单。

    • 您需要一个用于消息的公共基类,另一个用于解析器
    • 每条消息都有一个标记和一个相关的解析器。

    让我们看看一些代码:

    // 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;
    };
    

    有了这个框架(公认的简单),一些派生类:

    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));
      }
    };
    

    最后,其用途是:

    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)
    }
    

    它起作用,我知道因为我使用它(或者是一个变体)。

    有一些事情需要考虑:

    • 你可能愿意 MessageFactory 它是一个单例,然后允许在库加载时调用它,这样就可以通过实例化静态变量来注册解析器。如果你不想的话,这个很方便 main 要注册每一个解析器类型,就必须:无依赖性的局部性。
    • 标签必须共享。标记由消息类(称为标记)的虚拟方法提供服务也不常见。

    像:

    class Message
    {
    public:
      virtual ~Message();
      virtual const std::string& tag() const = 0;
      virtual void serialize(std::ostream& out) const;
    };
    
    • 序列化的逻辑也必须是共享的,对象处理自己的序列化/反序列化是正常的。

    像:

    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));
      }
    };
    

    模板最棒的是它从不停下来让我惊讶

    class MessageFactory
    {
    public:
      template <class M>
      void register()
      {
        m_parsers[M::Tag()] = new ParserTemplate<M>();
      }
    };
    
    //skipping to registration
      factory.register<MessageA>();
    

    现在是不是很漂亮:)?

        2
  •  10
  •   Laserallan    15 年前

    接近它的一种方法是使用一个映射并为每种消息类型注册某种类型的工厂函数。这意味着您可以摆脱交换机外壳,动态地添加和删除消息。

    代码看起来像:

    // 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);
    

    messagecreator类的外观如下:

    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;
        }
    };