Должен ли я избегать удушения при использовании заводского шаблона?
Я работаю над серверным проектом, который реализует собственный протокол. Сервер реализован с фабричным шаблоном в 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 */
Этот фабричный шаблон работал очень хорошо, и новые типы устройств легко тратить.
Однако в последнее время мы пытаемся взаимодействовать с существующей системой управления, которая обеспечивает похожую функциональность. Целевая система довольно старая и предоставляет только последовательный интерфейс, подобный AT-командам, на основе ASCII. Нам удалось решить проблему связи с 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 */
}
};
Другими словами, нам нужно вызвать метод производного класса, а метод не определен в базовом классе. И мы используем шаблон фабрики для генерации экземпляра производного класса.
Теперь у нас есть несколько вариантов:
Просто объединить две строки в одну не работает. Обе строки содержат некоторую указанную устройством информацию, и они должны анализироваться отдельно. Если мы воспользуемся этим подходом, мы выполним некоторый "предварительный анализ" из экземпляра синтаксического анализатора, прежде чем сможем объединить строку. И мы не думаем, что это хорошая идея.
Понизить возврат parser_factory::get_instance() к legacy_parser.
Создайте другую независимую фабрику, которая содержит только классы, производные от legacy_parser.
Измените определение 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); };
Реализуйте "умный указатель" с шаблоном Visitor для обработки экземпляров, полученных из 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<> - это самый простой подход для нас, но никому из нас не нравится эта идея, потому что мы все чувствуем, что это "зло" - что-то унижать. Однако никто не может точно объяснить, почему это "зло".
Прежде чем мы примем решение, я хотел бы услышать больше комментариев об этих вариантах.
3 ответа
http://en.wikipedia.org/wiki/Circle-ellipse_problem - ваш первый пример зла. Если вы видите, что можете что-то сделать с нарушением базовых принципов, то вам нужно изобрести другое колесо или попробовать другую шляпу: http://en.wikipedia.org/wiki/Six_Thinking_Hats
Унижение, особенно в реализации фабричного шаблона, имеет для меня довольно хороший смысл. Это на самом деле хорошо сочетается с идеологией "программа к интерфейсу". Не уверен, почему люди считают, что уныние - это плохо. Проверьте контрастность, так как это то, на что вы смотрите.
Проблема в том, что legacy_parser ожидает два кадра, а не один в вашем исходном парсере. Таким образом, возможное решение состоит в том, чтобы немного изменить ваш оригинальный синтаксический анализатор и заставить его работать с более чем одним кадром. Например, parse может вернуть предопределенную константу, если парсер хочет больше кадров, и тогда 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_;
};
На существующий код синтаксического анализатора не следует воздействовать, если они случайно не используют значение, определенное для значения "больше кадров".