Выбор правильного подкласса для программной реализации - PullRequest
6 голосов
/ 14 ноября 2009

Хорошо, контекст - это некоторый код сериализации / десериализации, который преобразует поток байтов в представление «объект», с которым легче работать (и наоборот).

Вот упрощенный пример с базовым классом сообщений, а затем, в зависимости от заголовка типа, присутствуют еще некоторые данные / функции, и мы должны выбрать правильный подкласс для создания экземпляра:

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 ]

10 голосов
/ 14 ноября 2009

Один из способов приблизиться к этому - использовать карту и зарегистрировать некоторую фабричную функцию для каждого типа сообщения. Это означает, что вы избавляетесь от случая переключения и можете динамически добавлять и удалять сообщения.

Код будет выглядеть примерно так:

// 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;
    }
};
5 голосов
/ 14 ноября 2009

На самом деле это довольно простой вопрос (как вы можете себе представить, вы определенно не единственный, кто десериализует в C ++).

То, что вы ищете, называется виртуальным строительством.

C ++ не определяет Virtual Construction, но его легко аппроксимировать, используя шаблон проектирования Prototype или метод Factory.

Я лично предпочитаю подход Factory, потому что Prototype означает, что у него есть некоторый экземпляр по умолчанию, который реплицируется и затем определяется ... проблема в том, что не у всех классов есть значимое значение по умолчанию, и в этом отношении значимое 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;
};

И с этой структурой (по общему признанию простой), некоторые производные классы:

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 регистрировал каждый тип парсера: locality> меньше зависимостей.
  • Теги должны быть общими. Также нет ничего необычного в том, что тег обслуживается виртуальным методом класса Message (называемым тегом).

Как:

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

Теперь разве не красиво :)?

...