Должен ли я каким-либо образом избегать понижения при использовании фабричного шаблона?

Я работаю над проектом сервера, который реализует проприетарный протокол. Сервер реализован с фабричным шаблоном на C++, и теперь мы сталкиваемся с проблемой понижения.

Протокол, над которым я работаю, предназначен для автоматического управления медленными сетями, такими как RS485, ZigBee, узкополосный ПЛК и т. д. Мы спроектировали основной сервер с заводским шаблоном. При получении нового фрейма мы сначала идентифицируем связанный с этим фреймом тип устройства, вызывая фабричный метод для создания нового экземпляра «парсера», и отправляем кадр в экземпляр парсера.

Наш проприетарный протокол реализован в чистом двоичном формате, вся информация, которая нам может понадобиться, записывается в сам кадр, поэтому базовый интерфейс можно определить как можно проще. Мы также реализуем подход автоматической регистрации для нашей фабрики (подробный код, связанный с операцией std::map, здесь опущен):

// This is our "interface" base-class
class parser
{
public:
    virtual int parse(unsigned char *) = 0;
    virtual ~parser() { }
};

// The next two classes are used for factory pattern
class instance_generator
{
public:
    virtual parser *generate() = 0;
};

class parser_factory
{
private:
    static std::map<int,instance_generator*> classDB;
public:
    static void add(int id, instance_generator &genrator);
    parser *get_instance(int id);
};

// the two template classes are implementations of "auto-regisrtation"
template <class G, int ID> class real_generator : public instance_generator
{
public:
    real_generator()    {   parser_factory::add(ID,this);   }
    parser *generate()  {   return new G;   }
};

template <class T, int N> class auto_reg : virtual public parser
{
private:
    static real_generator<T,N> instance;
public:
    auto_reg() { instance; }
};
template <class T, int N> parser_generator<T,N> auto_reg<T,N>::instance;


// And these are real parser implementations for each device type
class light_sensor : public auto_reg<light_sensor,1>
{
public:
    int parse(unsigned char *str)
    {
        /* do something here */
    }
};

class power_breaker : public auto_reg<power_breaker,2>
{
public:
    int parse(unsigned char *str)
    {
        /* do something here */
    }
};

/* other device parser */

Этот фабричный шаблон работал очень хорошо, и новые типы устройств легко использовать.

Однако в последнее время мы пытаемся взаимодействовать с существующей системой управления, которая обеспечивает аналогичную функциональность. Целевая система довольно старая и предоставляет только последовательный интерфейс на основе ASCII, похожий на AT-команду. Нам удалось решить проблему связи с PTY, но теперь нужно решить проблему с реализацией парсера.

Командный интерфейс целевой системы весьма ограничен. Я не могу просто ждать и слушать, что приходит, я должен опрашивать состояние, и я должен опрашивать дважды — первый опрос для заголовка и второй опрос для полезной нагрузки — чтобы получить полную команду. Это проблема для нашей реализации, потому что мне нужно передать ДВА кадра в экземпляр парсера, чтобы он мог работать:

class legacy_parser : virtual public parser
{
public:
    legacy_parser() { }
    int parse(unsigned char *str)
    {
        /* CAN NOT DO ANYTHING WITHOUT COMPLETE FRAMES */
    }
    virtual int parse(unsigned char *header, unsigned char *payload) = 0;
};

class legacy_IR_sensor : 
    public legacy_parser,
    public auto_reg<legacy_IR_sensor,20>
{
public:
    legacy_IR_sensor(){ }
    int parse(unsigned char *header, unsigned char *payload)
    {
        /* Now we can finally parse the complete frame */
    }
};

Другими словами, нам нужно будет вызвать метод производного класса, а метод не определен в базовом классе. И мы используем фабричный шаблон для создания экземпляра производного класса.

Теперь у нас есть несколько вариантов:

  1. Просто объединить две строки в одну не получится. Обе строки содержат некоторую информацию об устройстве, и они должны анализироваться отдельно. Если мы выберем этот подход, мы выполним некоторый «предварительный разбор» экземпляра синтаксического анализатора, прежде чем сможем объединить строку. И мы не думаем, что это хорошая идея.

  2. Понижение возврата parser_factory::get_instance() до legacy_parser.

  3. Создайте еще одну независимую фабрику, которая содержит только классы, производные от legacy_parser.

  4. Измените определение instance_generator и parser_factory, чтобы они также могли генерировать (legacy_parser*), не затрагивая при этом весь существующий код:

    class instance_generator
    {
    public:
        virtual parser *generate() = 0;
        virtual legacy_parser *generate_legacy() { return NULL; }
    };
    
    class extended_parser_factory : public parser_factory
    {
    public:
        legacy_parser *get_legacy_instance(int id);
    };
    
  5. Реализовать «умный указатель» с шаблоном посетителя для обработки экземпляров, полученных из legacy_parser:

    class smart_ptr
    {
    public:
        virtual void set(parser *p) = 0;
        virtual void set(legacy_parser *p) = 0;
    };
    
    class parser
    {
    public:
        parser() { }
        virtual int parse(unsigned char *) = 0;
        virtual void copy_ptr(smart_ptr &sp)    // implement "Visitor" pattern
        {
            sp.set(this);
        }
        virtual ~parser() { }
    };
    
    class legacy_parser : virtual public parser
    {
    public:
        legacy_parser() { }
        void copy_ptr(smart_ptr &sp)    // implement "Visitor" pattern
        {
            sp.set(this);
        }
        int parse(unsigned char *str)
        {
            /* CAN NOT DO ANYTHING WITHOUT COMPLETE FRAMES */
        }
        virtual int parse(unsigned char *header, unsigned char *payload) = 0;
    };
    
    class legacy_ptr : public smart_ptr
    {
    private:
        parser *miss;
        legacy_parser *hit;
    public:
        legacy_ptr& operator=(parser *rhv)
        {
            rhv->copy_ptr(*this);
            return *this;
        }
        void set(parser* ptr)
        {
            miss=ptr;
            /* ERROR! Do some log or throw exception */
        }
        void set(legacy_parser *ptr)
        {
            hit = ptr;
        }
        legacy_parser& operator*()
        {
            return *hit;
        }
        ~legacy_ptr()
        {
            if(miss) {
                delete miss;
            }
            if(hit) {
                delete hit;
            }
        }
    };
    

Очевидно, что Downcasting с dynamic_cast‹> — это самый простой подход для нас, но никому из нас не нравится эта идея, потому что мы все чувствуем, что «Downcast» — это «зло». Однако никто не может точно объяснить, почему это «зло».

Прежде чем мы примем решение, я хотел бы услышать больше комментариев об этих вариантах.


person RichardLiu    schedule 22.05.2011    source источник
comment
@RichardLu, вопрос слишком длинный, чтобы хватило терпения! Постарайтесь сделать это объективно.   -  person iammilind    schedule 22.05.2011
comment
@iammilind: Ну, мой вопрос сам описан в заголовке :) Мы выяснили несколько подходов (описанных в оставшейся части моего вопроса), чтобы избежать понижения, но мне просто интересно, стоит ли это того.   -  person RichardLiu    schedule 23.05.2011


Ответы (3)


http://en.wikipedia.org/wiki/Circle-ellipse_problem — ваш первый пример. зла. Если вы видите, что можете сделать что-то с нарушением базовых принципов, то вам следует изобрести другое колесо или попробовать другую шляпу: http://en.wikipedia.org/wiki/Six_Thinking_Hats

person gaRex    schedule 22.05.2011
comment
Вы правы... похоже, что то, как мы определяем производный класс (legacy_parser), нарушило принцип Лискова, поскольку исходный метод (parser(unsigned char *)) недействителен в производном классе. Следствием этого могут быть дополнительные процедуры проверки ошибок даже в существующем коде. Похоже, что нам лучше не использовать наследника legacy_parser из нашего исходного интерфейса, так как он совершенно не подходит. Будем лечить самостоятельно. - person RichardLiu; 23.05.2011

Даункастинг, особенно в реализации фабричного шаблона, имеет для меня довольно хороший смысл. На самом деле это хорошо сочетается с идеологией «программы для интерфейса». Не знаю, почему люди считают, что уныние — это плохо. Проверьте контравариантность, так как это то, на что вы смотрите.

person Suroot    schedule 22.05.2011

Проблема в том, что legacy_parser ожидает два кадра, а не один в исходном парсере. Таким образом, возможное решение состоит в том, чтобы немного изменить исходный парсер и заставить его работать с более чем одним фреймом. Например, синтаксический анализ может возвращать предопределенную константу, если синтаксическому анализатору требуется больше кадров, и тогда legacy_parser может быть реализован следующим образом:

class legacy_parser : public parser {
 public:
  int parse(unsigned char *str) {
    if (parse_header_) {
      // store str in header_
      parse_header_ = false;
      return kExpectMoreFrames;
    } else {
      return parse(header_, str);
    }
  }
 private:
  int parse(unsigned char *header, unsigned char *parload) {
    // ...
  }
  bool parse_header_;
  unsigned char *header_;
};

Существующий код синтаксического анализатора не должен быть затронут, если он случайно не использует значение, определенное для значения «больше кадров».

person xfxyjwf    schedule 23.05.2011