書かれているように、それは「匂いがする」が、それはあなたが与えた単なる例かもしれない。汎用オブジェクトコンテナーにデータを保存し、それをキャストしてデータにアクセスすることは、自動的にコードの匂いを嗅ぎません。多くの状況で使用されることがわかります。ただし、使用するときは、自分が何をしているのか、どのようにやっているのか、そしてその理由を知っておく必要があります。この例を見ると、文字列ベースの比較を使用して、どのオブジェクトが何であるかを知ることができます。ここで何をしているのか完全にわからないことを示唆しています(プログラマーにここに来る知恵があったので大丈夫です。SE、「私は私がやっていることが好きではないと思います、助けてください私を出して!」)。
このような一般的なコンテナからデータをキャストするパターンの基本的な問題は、データのプロデューサーとデータのコンシューマーが連携する必要があることですが、一目でそうすることは明らかではないかもしれません。 このパターンのすべての例で、臭いの有無にかかわらず、これは根本的な問題です。次の開発者がこのパターンを実行していることを完全に知らず、偶然にそれを破る可能性が非常に高いため、このパターンを使用する場合は、次の開発者を助けるように注意する必要があります。あなたは、彼が存在することを知らないかもしれないいくつかの詳細のために、彼が意図せずにコードを壊さないように簡単にしなければなりません。
たとえば、プレーヤーをコピーしたい場合はどうすればよいですか?プレーヤーオブジェクトの内容を見るだけで、かなり簡単に見えます。私はちょうどコピーする必要がありattack
、defense
およびtools
変数を。やさしい!さて、ポインタを使用すると少し難しくなることがすぐにわかります(ある時点で、スマートポインタを見る価値がありますが、それは別のトピックです)。これは簡単に解決できます。各ツールの新しいコピーを作成し、それらを新しいtools
リストに追加します。結局のところ、Tool
メンバーが1人だけの本当にシンプルなクラスです。だから、のコピーを含むコピーの束を作成しますSword
が、それが剣であることを知りませんでしたので、私はをコピーしましたname
。後で、attack()
関数は名前を見て、それが「剣」であると判断し、それをキャストすると、悪いことが起こります!
このケースを、同じパターンを使用するソケットプログラミングの別のケースと比較できます。次のようなUNIXソケット関数を設定できます。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
なぜこれは同じパターンですか?をbind
受け入れないためsockaddr_in*
、より一般的なを受け入れますsockaddr*
。これらのクラスの定義を見るとsockaddr
、sin_family
* に割り当てたファミリのメンバーが1つだけであることがわかります。家族はあなたにどのサブタイプをキャストするべきかを言いますsockaddr
。 AF_INET
は、アドレス構造体が実際にはであることを示していますsockaddr_in
。であったAF_INET6
場合、アドレスはになりsockaddr_in6
、より大きなIPv6アドレスをサポートするためにより大きなフィールドを持ちます。
これはTool
、整数ではなくを使用してファミリを指定することを除いて、例と同じですstd::string
。しかし、私はそれが臭いがないと主張し、「ソケットを行うための標準的な方法なので、「臭いがしない」」以外の理由でそうしようとします。明らかに同じパターン、なぜジェネリックオブジェクトにデータを保存してキャストするのは自動的にコードの匂いではないと主張するのですが、データの安全性を高める方法にはいくつかの違いがあります。
このパターンを使用する場合、最も重要な情報は、プロデューサーからコンシューマーへのサブクラスに関する情報の伝達をキャプチャすることです。これはname
フィールドで行っていることであり、UNIXソケットはsin_family
フィールドで行います。そのフィールドは、生産者が実際に作成したものを消費者が理解するために必要な情報です。すべてのこのパターンの場合、それが列挙(または非常に少なくとも、列挙のように作用整数)であるべきです。どうして?あなたの消費者が情報をどうしようとしているのかを考えてください。彼らは大きなif
声明を書き出すか、switch
あなたがしたように、正しいサブタイプを決定し、それをキャストし、データを使用するステートメント。定義により、これらのタイプはごく少数です。あなたがしたように、それを文字列に保存できますが、それには多くの欠点があります:
- 遅い-
std::string
通常、文字列を保持するために何らかの動的メモリを実行する必要があります。また、サブクラスの種類を把握するたびに、名前と一致する全文比較を行う必要があります。
- 汎用性が非常に高い-非常に危険なことをしているときに、自分に制約を課すために言わなければならないことがあります。このようなシステムがあり、それがどのタイプのオブジェクトを見ているかを伝えるために部分文字列を探しました。これは、オブジェクトの名前にその部分文字列が誤って含まれ、ひどく不可解なエラーを作成するまで、うまくいきました。上記で述べたように、必要なケースはごく少数であるため、文字列のような非常に強力なツールを使用する理由はありません。これはにつながります...
- エラーが発生しやすい-ある消費者が誤って魔法の布の名前をに設定したときに、物事が機能しない理由をデバッグしようとして、殺人的な大暴れをしたいとしましょう
MagicC1oth
。真剣に、そのようなバグは、何が起こったのかを理解するまでに何日も頭を悩ませることがあります。
列挙ははるかに優れています。高速で、安価で、エラーがはるかに少ない:
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
std::string typeName() const {
switch(type) {
case kSword: return "Sword";
case kSheild: return "Sheild";
case kMagicCloth: return "Magic Cloth";
default:
throw std::runtime_error("Invalid enum!");
}
}
};
この例ではswitch
、列挙型を含むステートメントも示していdefault
ます。このパターンの最も重要な部分は、スローするケースです。完璧なことをすれば、決してそのような状況に陥ることはありません。ただし、誰かが新しいツールタイプを追加し、それをサポートするためにコードを更新するのを忘れた場合、エラーをキャッチするものが必要になります。実際、それらを必要としない場合でも追加する必要があるので、それらをお勧めします。
もう1つの大きな利点enum
は、次の開発者に有効なツールタイプの完全なリストをすぐに提供できることです。ボブの壮大なボス戦で使用するボブの特化したフルートクラスを見つけるためにコードを歩いていく必要はありません。
void damageWargear(Tool* tool)
{
switch(tool->type)
{
case Tool::kSword:
static_cast<Sword*>(tool)->damageSword();
break;
case Tool::kShield:
static_cast<Sword*>(tool)->damageShield();
break;
default:
break; // Ignore all other objects
}
}
はい、「空の」デフォルトステートメントを入れました。これは、新しい予期しないタイプが来た場合に何が起こるかを次の開発者に明示するためです。
これを行うと、パターンの臭いが少なくなります。ただし、無臭にするために最後に行う必要があるのは、他のオプションを検討することです。これらのキャストは、C ++レパートリーにあるより強力で危険なツールの一部です。正当な理由がない限り、使用しないでください。
非常に人気のある選択肢の1つは、「ユニオン構造体」または「ユニオンクラス」と呼ばれるものです。あなたの例では、これは実際には非常に適しています。これらのいずれかを作成するTool
には、以前のような列挙でクラスを作成しますが、サブクラス化する代わりにTool
、すべてのサブタイプのすべてのフィールドをそのクラスに配置します。
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
int attack;
int defense;
};
これで、サブクラスはまったく必要ありません。あなただけを見ているtype
その他のフィールドが実際に有効であるかを確認するためにフィールド。これははるかに安全で理解しやすいです。ただし、欠点があります。これを使いたくない場合があります:
- オブジェクトがあまりにも似ていない場合-フィールドの洗濯物リストで終わることがあり、どのオブジェクトが各オブジェクトタイプに適用されるかが不明確になる可能性があります。
- メモリがクリティカルな状況で操作する場合-10個のツールを作成する必要がある場合、メモリを怠ることがあります。5億個のツールを作成する必要があるときは、ビットとバイトを気にし始めます。ユニオン構造体は、常に必要以上に大きくなっています。
この解決策は、APIのオープンエンド性により複雑さの問題が複雑になるため、UNIXソケットでは使用されません。UNIXソケットの目的は、UNIXのあらゆるフレーバーで使用できるものを作成することでした。各フレーバーは、などのサポートするファミリーのリストを定義できAF_INET
、それぞれに短いリストがあります。ただし、新しいプロトコルが登場した場合AF_INET6
は、新しいフィールドを追加する必要があります。ユニオン構造体でこれを行った場合、同じ名前の構造体の新しいバージョンを効果的に作成し、無限の非互換性の問題を作成することになります。これが、UNIXソケットがユニオン構造体ではなくキャストパターンを使用することを選択した理由です。私は彼らがそれを考えたと確信しており、彼らがそれについて考えたという事実は、彼らがそれを使用するときに臭いがない理由の一部です。
ユニオンを実際に使用することもできます。組合は、最大のメンバーと同じくらい大きくなるだけでメモリを節約しますが、独自の問題があります。これはおそらくコードのオプションではありませんが、常に考慮すべきオプションです。
別の興味深いソリューションはboost::variant
です。 Boostは、再利用可能なクロスプラットフォームソリューションが満載された優れたライブラリです。おそらくこれまでに書かれた最高のC ++コードの一部です。 Boost.Variantは基本的にC ++バージョンの共用体です。これは、多くの異なるタイプを含むことができるコンテナーですが、一度に1つだけです。あなたのを作ることができるSword
、Shield
とMagicCloth
クラス、そしてツールがあること作るboost::variant<Sword, Shield, MagicCloth>
ことは、これらの3種類のいずれかを含んで意味。これは、UNIXソケットがそれを使用できないようにする将来の互換性と同じ問題に依然として苦しんでいます(UNIXソケットはCであることに言及して、boost
しかし、このパターンは非常に便利です。バリアントは、たとえば、構文解析ツリーでよく使用されます。構文解析ツリーでは、テキストの文字列を取得し、ルールの文法を使用してテキストを分割します。
思い切ってジェネリックオブジェクトをキャストする方法を使用する前に検討することをお勧めする最後のソリューションは、Visitorデザインパターンです。Visitorは、仮想関数を呼び出すと必要なキャストが効果的に行われ、それが自動的に行われるという観察を利用した強力なデザインパターンです。コンパイラーがそれを行うので、決して間違ってはなりません。したがって、Visitorは列挙型を保存する代わりに、オブジェクトがどの型であるかを知っているvtableを持つ抽象基本クラスを使用します。次に、作業を行うきちんとした小さな二重間接呼び出しを作成します。
class Tool;
class Sword;
class Shield;
class MagicCloth;
class ToolVisitor {
public:
virtual void visit(Sword* sword) = 0;
virtual void visit(Shield* shield) = 0;
virtual void visit(MagicCloth* cloth) = 0;
};
class Tool {
public:
virtual void accept(ToolVisitor& visitor) = 0;
};
lass Sword : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
};
class Shield : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int defense;
};
class MagicCloth : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
int defense;
};
それで、この神の恐ろしいパターンは何ですか?さて、Tool
仮想関数がありaccept
ます。ビジターを渡すと、向きを変えて、visit
そのビジターでそのタイプの正しい関数を呼び出すことが期待されます。これがvisitor.visit(*this);
各サブタイプで行われていることです。複雑ですが、上記の例でこれを示すことができます。
class AttackVisitor : public ToolVisitor
{
public:
int& currentAttack;
int& currentDefense;
AttackVisitor(int& currentAttack_, int& currentDefense_)
: currentAttack(currentAttack_)
, currentDefense(currentDefense_)
{ }
virtual void visit(Sword* sword)
{
currentAttack += sword->attack;
}
virtual void visit(Shield* shield)
{
currentDefense += shield->defense;
}
virtual void visit(MagicCloth* cloth)
{
currentAttack += cloth->attack;
currentDefense += cloth->defense;
}
};
void Player::attack()
{
int currentAttack = this->attack;
int currentDefense = this->defense;
AttackVisitor v(currentAttack, currentDefense);
for (Tool* t: tools) {
t->accept(v);
}
//some other functions to start attack
}
ここで何が起こるのでしょうか?ビジターを作成します。ビジターは、訪問しているオブジェクトのタイプがわかれば、何らかの作業を行います。次に、ツールのリストを繰り返し処理します。引数のために、最初のオブジェクトがであるとしましょうShield
。しかし、私たちのコードはまだそれを知りません。t->accept(v)
仮想関数であるを呼び出します。最初のオブジェクトはシールドであるため、最終的void Shield::accept(ToolVisitor& visitor)
にを呼び出し、を呼び出しますvisitor.visit(*this);
。さて、どちらvisit
を呼び出すかを調べると、(この関数が呼び出されたため)シールドがあることを既に知っているので、を呼び出すことvoid ToolVisitor::visit(Shield* shield)
になりますAttackVisitor
。これで正しいコードが実行され、防御が更新されます。
訪問者はかさばる。それは非常に不格好なので、私はそれがそれ自身の匂いを持っているとほとんど思います。悪い訪問者パターンを書くのは非常に簡単です。ただし、他のどれにもない大きな利点が1つあります。新しいツールタイプを追加する場合、そのための新しいToolVisitor::visit
関数を追加する必要があります。これを行うと、プログラム内のすべて ToolVisitor
の仮想関数が欠落しているため、コンパイルが拒否されます。これにより、何かを見逃したすべてのケースを簡単にキャッチできます。if
やswitch
ステートメントを使用して作業を行うことを保証するのははるかに困難です。これらの利点は十分に優れており、Visitorは3Dグラフィックシーンジェネレーターに素敵なニッチを見つけました。彼らは訪問者が提供するまさにそのような行動を必要とするので、それは素晴らしい作品です!
全体として、これらのパターンは次の開発者にとって難しいことです。時間をかけて簡単に行えば、コードの臭いもなくなります。
*技術的には、仕様を見ると、sockaddrにはという名前のメンバーが1つありsa_family
ます。ここでは、Cレベルで行われるトリッキーなことがありますが、それは私たちにとっては重要ではありません。実際の実装を見ることができますが、この回答ではsa_family
sin_family
、散文で最も直感的なものを使用して、Cのトリックが重要でない詳細を処理することを信頼して、他の完全に互換性のあるものを使用します。