独自のプロトコルを実装するサーバー プロジェクトに取り組んでいます。サーバーは C++ のファクトリ パターンで実装されており、ダウンキャストの問題に直面しています。
私が取り組んでいるプロトコルは、RS485、ZigBee、狭帯域 PLC などの低速ネットワークを自動制御するように設計されています。工場のパターンでメイン サーバーを設計しました。新しいフレームが受信されると、まずそのフレームに関連付けられているデバイス タイプを識別し、ファクトリ メソッドを呼び出して新しい「パーサー」インスタンスを生成し、フレームをパーサー インスタンスにディスパッチします。
当社独自のプロトコルは純粋なバイナリで実装されており、必要なすべての情報がフレーム自体に記録されるため、基本インターフェイスを可能な限りシンプルに定義できます。また、ファクトリの自動登録アプローチを実装します (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 との通信の問題は解決できましたが、解決すべき問題はパーサーの実装です。
ターゲット システムのコマンド インターフェイスは非常に限られています。完全なコマンドを取得するには、最初にヘッダーをポーリングし、次にペイロードをポーリングして、2 回ポーリングする必要があります。これは私たちの実装の問題です。2 つのフレームをパーサー インスタンスに渡して動作させる必要があるためです。
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 */
}
};
つまり、派生クラスのメソッドを呼び出す必要があり、そのメソッドは基本クラスでは定義されていません。そして、ファクトリ パターンを使用して派生クラスのインスタンスを生成しています。
いくつかの選択肢があります。
2 つの文字列を 1 つに連結するだけでは機能しません。どちらの文字列にもデバイス固有の情報が含まれており、それらは個別に解析されます。このアプローチを採用する場合、文字列を連結する前に、パーサー インスタンスから「事前解析」を実行する必要があります。そして、それは良い考えだとは思いません。
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); };
legacy_parser から派生したインスタンスを処理するために Visitor パターンで「スマート ポインター」を実装します。
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; } } };
dynamic_cast<> によるダウンキャストが私たちにとって最も簡単なアプローチであることは明らかですが、何かをダウンキャストするのは「悪」だと誰もが感じているため、このアイデアを好む人はいません。しかし、なぜそれが「悪」なのかを正確に説明できる人はいません。
決定を下す前に、これらのオプションについてさらにコメントをお待ちしております。