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

246*_*tNt 6 c++ polymorphism serialization design-patterns subclassing

好的,上下文是一些序列化/反序列化代码,它将字节流解析为更容易使用的"对象"表示(反之亦然).

这是一个带有基本消息类的简化示例,然后根据"类型"标题,存在更多数据/函数,我们必须选择正确的子类来实例化:

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);
};
Run Code Online (Sandbox Code Playgroud)

在实际示例中,将存在数百种不同的消息类型以及可能的若干级别或层级,因为一些消息彼此共享字段/功能.

现在,要解析一个字节字符串,我正在做类似的事情:

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);
Run Code Online (Sandbox Code Playgroud)

但是我没有发现这个巨大的开关非常优雅,并且我有关于哪个消息具有哪个'类型值'两次的信息(一次在构造函数中,一个在此切换中)它也很长...

我正在寻找一种更好的方式,只会更好......如何改善这一点?

Las*_*lan 10

接近它的一种方法是使用地图并为每种消息类型注册某种工厂功能.这意味着您可以摆脱开关案例,并可以动态添加和删除消息.

代码看起来像:

// 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);
Run Code Online (Sandbox Code Playgroud)

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;
    }
};
Run Code Online (Sandbox Code Playgroud)


Mat*_* M. 6

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

您正在寻找的被称为虚拟构造。

C ++没有定义虚拟构造,但是使用Prototype设计模式或Factory方法很容易将其近似。

我个人更喜欢这种Factory方法,因为Prototype一个方法意味着要复制某种默认实例,然后定义THEN ...问题是并非所有类都具有有意义的默认值,因此并不是有意义的Default Constructor

Factory方法很容易。

  • 您需要为消息提供一个通用的基类,为解析器提供一个基类。
  • 每个消息都具有标签和关联的解析器

让我们看一些代码:

// 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;
};
Run Code Online (Sandbox Code Playgroud)

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

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));
  }
};
Run Code Online (Sandbox Code Playgroud)

最后,使用:

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)
}
Run Code Online (Sandbox Code Playgroud)

它有效,我知道我可以使用它(或变体)。

有一些事情要考虑:

  • 您可能愿意创建MessageFactory一个单例,然后允许在库加载时调用它,因此您可以通过实例化静态变量来注册解析器。如果您不想main注册每个解析器类型,这将非常方便:局部性>较少的依赖关系。
  • 标签必须共享。标记由Message类的虚拟方法(称为标记)提供服务也很常见。

喜欢:

class Message
{
public:
  virtual ~Message();
  virtual const std::string& tag() const = 0;
  virtual void serialize(std::ostream& out) const;
};
Run Code Online (Sandbox Code Playgroud)
  • 序列化逻辑也必须共享,对象处理自己的序列化/反序列化并不罕见

喜欢:

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));
  }
};
Run Code Online (Sandbox Code Playgroud)

模板的优点在于,它让我惊讶不已

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

//skipping to registration
  factory.register<MessageA>();
Run Code Online (Sandbox Code Playgroud)

现在不是很漂亮:)吗?