汎用オブジェクトをコンテナに保存してからオブジェクトを取得し、コンテナからオブジェクトをダウンキャストするのはコードの匂いですか?


34

たとえば、プレーヤーの能力を高めるためのいくつかのツールを備えたゲームがあります。

Tool.h

class Tool{
public:
    std::string name;
};

そしていくつかのツール:

Sword.h

class Sword : public Tool{
public:
    Sword(){
        this->name="Sword";
    }
    int attack;
};

Shield.h

class Shield : public Tool{
public:
    Shield(){
        this->name="Shield";
    }
    int defense;
};

MagicCloth.h

class MagicCloth : public Tool{
public:
    MagicCloth(){
        this->name="MagicCloth";
    }
    int attack;
    int defense;
};

そして、プレイヤーは攻撃のためのいくつかのツールを保持するかもしれません:

class Player{
public:
    int attack;
    int defense;
    vector<Tool*> tools;
    void attack(){
        //original attack and defense
        int currentAttack=this->attack;
        int currentDefense=this->defense;
        //calculate attack and defense affected by tools
        for(Tool* tool : tools){
            if(tool->name=="Sword"){
                Sword* sword=(Sword*)tool;
                currentAttack+=sword->attack;
            }else if(tool->name=="Shield"){
                Shield* shield=(Shield*)tool;
                currentDefense+=shield->defense;
            }else if(tool->name=="MagicCloth"){
                MagicCloth* magicCloth=(MagicCloth*)tool;
                currentAttack+=magicCloth->attack;
                currentDefense+=magicCloth->shield;
            }
        }
        //some other functions to start attack
    }
};

if-else各ツールには異なるプロパティがあり、各ツールはプレーヤーの攻撃と防御に影響するため、ツールの仮想メソッドに置き換えることは難しいと思います。プレーヤーの攻撃と防御の更新は、プレーヤーオブジェクト内で行う必要があります。

しかし、私はこのデザインには満足していませんでしたif-else。ダウンキャストが含まれているため、長い声明が出ているからです。この設計は「修正」する必要がありますか?もしそうなら、私はそれを修正するために何ができますか?


4
特定のサブクラス(およびその後のダウンキャスト)のテストを削除する標準的なOOPテクニックは、ifチェーンとキャストの代わりに使用する基本クラスで、またはこの場合は2つの仮想メソッドを作成することです。これを使用して、ifを完全に削除し、実装するサブクラスに操作を委任できます。また、新しいサブクラスを追加するたびにifステートメントを編集する必要もありません。
エリックエイド16年

2
ダブルディスパッチも検討してください。
スパイダーボリス

属性タイプ(攻撃、防御など)とそれに割り当てられた値のディクショナリを保持するプロパティをToolクラスに追加してください。攻撃、防御は値を列挙できます。次に、列挙された定数によってツール自体から値を呼び出すことができます。
user1740075


1
訪問者パターンも参照してください。
JDługosz

回答:


63

はい、それはコードの匂いです(多くの場合)。

if-elseをツールの仮想メソッドに置き換えるのは難しいと思います

あなたの例では、if / elseを仮想メソッドに置き換えるのは非常に簡単です:

class Tool{
 public:
   virtual int GetAttack() const=0;
   virtual int GetDefense() const=0;
};

class Sword : public Tool{
    // ...
 public:
   virtual int GetAttack() const {return attack;}
   virtual int GetDefense() const{return 0;}
};

これで、ifブロックはもう必要ありません。呼び出し元は次のように使用できます

       currentAttack+=tool->GetAttack();
       currentDefense+=tool->GetDefense();

もちろん、より複雑な状況では、そのような解決策は必ずしもそれほど明白ではありません(しかし、それでもほとんどいつでも可能です)。ただし、仮想メソッドを使用してケースを解決する方法がわからない状況になった場合は、「プログラマー」(または、言語または実装固有の場合はStackoverflow)で新しい質問を再度行うことができます。


4
または、そのことについては、gamedev.stackexchange.comで
Kromsterは、サポートMonica

7
Swordコードベースでは、この方法の概念さえ必要ありません。あなただけの可能性new Tool("sword", swordAttack, swordDefense)JSONファイルを例えばから。
-AmazingDreams

7
@AmazingDreams:これは正しい(ここで見たコードの部分について)が、OPは彼が議論したい側面に焦点を当てるために彼の質問のために彼の本当のコードを単純化したと思う。
ドックブラウン

3
これは、元のコードよりもそれほど良くありません(まあ、少しです)。追加のプロパティを持つツールは、追加のメソッドを追加しないと作成できません。この場合、継承よりも構成を優先すべきだと思います。ええ、現時点では攻撃と防御しかありませんが、そのようにする必要はありません。
ポリグノーム

1
@DocBrownはい、本当です。ただし、RPGのように見えますが、キャラクターはツールまたは装備されたアイテムによって変更されるいくつかのステータスを持っています。Tool可能性のあるすべての修飾子を使用してジェネリックを作成し、一部vector<Tool*>をデータファイルから読み取ったもので埋めてから、それらをループして、現在のように統計情報を変更します。ただし、たとえば攻撃に対して10%のボーナスを与えるアイテムが必要な場合、問題が発生します。おそらくtool->modify(playerStats)別のオプションです。
AmazingDreams

23

コードの主な問題は、新しいアイテムを導入するたびに、アイテムのコードを記述および更新するだけでなく、プレーヤー(またはアイテムが使用されている場所)を変更する必要があることです。もっと複雑です。

一般的な経験則として、通常のサブクラス化/継承に頼ることができず、自分でアップキャストする必要がある場合、それは常にちょっと怪しいと思います。

全体をより柔軟にする2つの可能なアプローチを考えることができます。

  • 他の人が述べたように、attackdefenseメンバーを基本クラスに移動し、単にに初期化します0。これは、攻撃のためにアイテムを実際にスイングできるかどうか、または攻撃をブロックするためにそれを使用できるかどうかのチェックとしても機能します。

  • ある種のコールバック/イベントシステムを作成します。これにはさまざまなアプローチがあります。

    シンプルに保つのはどうですか?

    • virtual void onEquip(Owner*) {}やなどの基本クラスメンバーを作成できますvirtual void onUnequip(Owner*) {}
    • 彼らのオーバーロードと呼ばれ、アイテムを装備する場合(未)統計情報を変更することになる、などvirtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }virtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }
    • 短い文字列または定数をキーとして使用するなど、統計に何らかの方法でアクセスできるため、プレーヤーや「所有者」で必ずしも処理する必要のない新しいギア固有の値やボーナスを導入することもできます。
    • 攻撃/防御値をジャストインタイムでリクエストするのに比べて、これは全体をより動的にするだけでなく、不必要な呼び出しを節約し、キャラクターに永続的に影響するアイテムを作成することもできます。

      たとえば、呪われた指輪を装備すると、隠されたステータスが設定されるだけで、キャラクターは永久に呪われているとマークされます。


7

@DocBrownは良い答えを出しましたが、それだけでは十分ではありません。回答の評価を開始する前に、ニーズを評価する必要があります。何が本当に必要ですか?

以下に、ニーズごとに異なる利点を提供する2つの可能なソリューションを示します。

1つ目は非常に単純化されており、特に示した内容に合わせて調整されています。

class Tool {
    public:
        std::string name;
        int attack;
        int defense;
}

public void attack() {
    int attack = this->attack;
    int defense = this->defense;
    for (Tool* tool : tools){
        attack += tool->attack;
        defense += tool->defense;
    }
}

これにより、ツールの非常に簡単なシリアル化/逆シリアル化が可能なり(たとえば、保存またはネットワーク化)、仮想ディスパッチはまったく必要ありません。あなたのコードがあなたが示したすべてであり、あなたがそれが他の多くの進化を期待しないなら、異なる名前とそれらの統計を持つ異なるツールを異なる量だけ持つことを期待するなら、これは行く方法です。

@DocBrownは、まだ仮想ディスパッチに依存しているソリューションを提供しています。これは、表示されていないコードの部分に何らかの方法でツールを特化する場合に役立ちます。ただし、他の動作も本当に必要な場合、または変更したい場合は、次の解決策をお勧めします。

継承を超える構成

後で敏a を変更するツールが必要になったらどうしますか?または速度を実行しますか?私には、あなたはRPGを作っているようです。RPGにとって重要なことの1つは、拡張に対してオープンであることです。これまでに示したソリューションはそれを提供していません。Tool新しい属性が必要になるたびに、クラスを変更し、新しい仮想メソッドを追加する必要があります。

私が示している2番目の解決策は、コメントで以前に示唆したものです-継承の代わりに構成を使用し、「修正のために閉じられ、拡張のために開かれます*原則」に従います。馴染みのあるものになります(作曲をESの小さな兄弟と考えたいです)。

以下に示すのは、JavaやC#などのランタイム型情報を持つ言語では、はるかにエレガントです。したがって、ここで示すC ++コードには、ここで構成を機能させるために単に必要な「簿記」を含める必要があります。おそらく、C ++の経験が豊富な人は、さらに優れたアプローチを提案できるでしょう。

まず、発信者の側をもう一度見てください。この例では、attackメソッド内の呼び出し元として、ツールをまったく気にしません。気にするのは、攻撃ポイントと防御ポイントの2つのプロパティです。それらがどこから来たのかは本当に気にしませし、他のプロパティ(例えば、実行速度、敏g性)についても気にしません。

最初に、新しいクラスを紹介します

class Component {
    public:
        // we need this, in Java we'd simply use getClass()
        virtual std::string type() const = 0;
};

そして、最初の2つのコンポーネントを作成します

class Attack : public Component {
    public:
        std::string type() const override { return std::string("mygame::components::Attack"); }
        int attackValue = 0;
};

class Defense : public Component {
    public:
      std::string type() const override { return std::string("mygame::components::Defense"); }
      int defenseValue = 0;
};

その後、ツールに一連のプロパティを保持させ、他のユーザーがプロパティをクエリできるようにします。

class Tool {
private:
    std::map<std::string, Component*> components;

public:
    /** Adds a component to the tool */
    void addComponent(Component* component) { 
        components[component->type()] = component;
    };
    /** Removes a component from the tool */
    void removeComponent(Component* component) { components.erase(component->type()); };
    /** Return the component with the given type */
    Component* getComponentByType(std::string type) { 
        std::map<std::string, Component*>::iterator it = components.find(type);
        if (it != components.end()) { return it->second; }
        return nullptr;
    };
    /** Check wether a tol has a given component */
    bool hasComponent(std::string type) {
        std::map<std::string, Component*>::iterator it = components.find(type);
        return it != components.end();
    }
};

この例では、各タイプの1つのコンポーネントのみをサポートしていることに注意してください。これにより、作業が簡単になります。理論的には、同じタイプの複数のコンポーネントを許可することもできますが、それは非常に高速になります。1つの重要な側面:Tool今れる変更のため閉鎖 -私たちが今までのソースに触れることはありませんTool再び-しかし、拡張のためのオープン -私たちは他のものをmodifiyngことにより、ツールの動作を拡張し、それに他のコンポーネントを渡すことができます。

次に、コンポーネントタイプごとにツールを取得する方法が必要です。コード例のように、ツールにベクターを使用することもできます。

class Player {
    private:
        int attack = 0; 
        int defense = 0;
        int walkSpeed;
    public:
        std::vector<Tool*> tools;
        std::vector<Tool*> getToolsByComponentType(std::string type) {
            std::vector<Tool*> retVal;
            for (Tool* tool : tools) {
                if (tool->hasComponent(type)) { 
                    retVal.push_back(tool); 
                }
            }
            return retVal;
        }

        void doAttack() {
            int attackValue = this->attack;
            int defenseValue = this->defense;

            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Attack"))) {
                Attack* component = (Attack*) tool->getComponentByType(std::string("mygame::components::Attack"));
                attackValue += component->attackValue;
            }
            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Defense"))) {
                Defense* component = (Defense*)tool->getComponentByType(std::string("mygame::components::Defense"));
                defenseValue += component->defenseValue;
            }
            std::cout << "Attack with strength " << attackValue << "! Defend with strenght " << defenseValue << "!";
        }
};

これを独自のInventoryクラスにリファクタリングし、ルックアップテーブルを保存して、コンポーネントタイプごとの検索ツールを大幅に簡素化し、コレクション全体の繰り返しの繰り返しを回避することもできます。

このアプローチにはどのような利点がありますか?ではattack、2つのコンポーネントを持つツールを処理します。他のことは気にしません。

あなたにwalkTo方法があると想像してみましょう。そして今、あなたは、何らかのツールがあなたの歩行速度を変更する能力を得るならば、それが良い考えであると決定します。問題ない!

最初に、新しいを作成しますComponent

class WalkSpeed : public Component {
public:
    std::string type() const override { return std::string("mygame::components::WalkSpeed"); }
    int speedBonus;
};

次に、このコンポーネントのインスタンスを、起動速度を上げたいツールに追加し、WalkTo作成したコンポーネントを処理する方法を変更します。

void walkTo() {
    int walkSpeed = this->walkSpeed;

    for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components:WalkSpeed"))) {
        WalkSpeed* component = (WalkSpeed*)tool->getComponentByType(std::string("mygame::components::Defense"));
        walkSpeed += component->speedBonus;
        std::cout << "Walk with " << walkSpeed << std::endl;
    }
}

Toolsクラスをまったく変更せずに、Toolsにいくつかの動作を追加したことに注意してください。

文字列をマクロまたは静的なconst変数に移動することができます(そうする必要があります)ので、何度も入力する必要はありません。

このアプローチをさらに進める場合-たとえば、プレイヤーに追加できるコンポーネントを作成し、プレイヤーCombatが戦闘に参加できるようにフラグを立てるコンポーネントを作成する場合は、attackメソッドも削除して処理することができますコンポーネントによって、または他の場所で処理されます。

プレーヤーがコンポーネントを取得できるようにすることの利点は、プレーヤーを変更して異なる動作をさせる必要さえなくなることです。私の例では、Movableコンポーネントを作成することができます。そうすればwalkTo、プレーヤーを動かすためにメソッドをプレーヤーに実装する必要はありません。コンポーネントを作成し、プレーヤーにアタッチし、他の誰かに処理させるだけです。

この要点で完全な例を見つけることができます:https : //gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20

このソリューションは、明らかに他の投稿されたものよりも少し複雑です。しかし、あなたがどれだけ柔軟になりたいか、どこまでそれを取りたいかによって、これは非常に強力なアプローチになります。

編集

他のいくつかの答えは、直接継承を提案しています(剣をツールに拡張し、シールドをツールに拡張します)。これは、継承がうまく機能するシナリオではないと思います。特定の方法でシールドでブロックすると、攻撃者に損害を与える可能性があると判断した場合はどうなりますか?私のソリューションでは、攻撃コンポーネントをシールドに追加するだけで、コードを変更せずにそれを実現できます。継承を使用すると、問題が発生します。RPGのアイテム/ツールは、最初からエンティティシステムを使用して構成するか、直接使用する場合の最有力候補です。


1

一般的に、ifOOP言語で(インスタンスの型を要求することと組み合わせて)使用する必要がある場合、それは何か臭いが起こっているという兆候です。少なくとも、モデルをよく見る必要があります。

私はあなたのドメインを異なってモデル化します。

あなたのユースケースのためにTool持っているAttackBonusDefenseBonus-可能性があり、どちらも0それは羽またはそのような何かのように戦うために無用である場合には。

攻撃の場合、使用する武器のbaserate+ bonusが使用されます。防衛baserate+ についても同じことが言えbonusます。

その結果、攻撃/防御ボニを計算Toolするvirtual方法が必要になります。

tl; dr

より良いデザインを使えば、ハッキングを避けることができますif


スカラー値を比較する場合など、ifが必要になる場合があります。オブジェクトタイプの切り替えの場合、それほど多くはありません。
アンディ

ハハ、ifは非常に重要な演算子であり、使用はコードの匂いだとは言えません。
ティムタム

1
@Tymskiはいくつかの点であなたは正しい。自分をより明確にしました。私はif少ないプログラミングを擁護していません。主にif instanceofなどの組み合わせで。しかし、ifsはコードメルであると主張する立場があり、それを回避する方法があります。そして、あなたは正しいです、それはそれ自身の権利を持っている不可欠なオペレーターです。
トーマスジャンク

1

書かれているように、それは「匂いがする」が、それはあなたが与えた単なる例かもしれない。汎用オブジェクトコンテナーにデータを保存し、それをキャストしてデータにアクセスすることは自動的にコードの匂いを嗅ぎません。多くの状況で使用されることがわかります。ただし、使用するときは、自分が何をしているのか、どのようにやっているのか、そしてその理由を知っておく必要があります。この例を見ると、文字列ベースの比較を使用して、どのオブジェクトが何であるかを知ることができます。ここで何をしているのか完全にわからないことを示唆しています(プログラマーにここに来る知恵があったので大丈夫です。SE、「私は私がやっていることが好きではないと思います、助けてください私を出して!」)。

このような一般的なコンテナからデータをキャストするパターンの基本的な問題は、データのプロデューサーとデータのコンシューマーが連携する必要があることですが、一目でそうすることは明らかではないかもしれません。 このパターンのすべての例で、臭いの有無にかかわらず、これは根本的な問題です。次の開発者がこのパターンを実行していることを完全に知らず、偶然にそれを破る可能性が非常に高いため、このパターンを使用する場合は、次の開発者を助けるように注意する必要があります。あなたは、彼が存在することを知らないかもしれないいくつかの詳細のために、彼が意図せずにコードを壊さないように簡単にしなければなりません。

たとえば、プレーヤーをコピーしたい場合はどうすればよいですか?プレーヤーオブジェクトの内容を見るだけで、かなり簡単に見えます。私はちょうどコピーする必要がありattackdefenseおよび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*。これらのクラスの定義を見るとsockaddrsin_family* に割り当てたファミリのメンバーが1つだけであることがわかります。家族はあなたにどのサブタイプをキャストするべきかを言いますsockaddrAF_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つだけです。あなたのを作ることができるSwordShieldMagicClothクラス、そしてツールがあること作る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の仮想関数が欠落しているため、コンパイルが拒否されます。これにより、何かを見逃したすべてのケースを簡単にキャッチできます。ifswitchステートメントを使用して作業を行うことを保証するのははるかに困難です。これらの利点は十分に優れており、Visitorは3Dグラフィックシーンジェネレーターに素敵なニッチを見つけました。彼らは訪問者が提供するまさにそのような行動を必要とするので、それは素晴らしい作品です!

全体として、これらのパターンは次の開発者にとって難しいことです。時間をかけて簡単に行えば、コードの臭いもなくなります。

*技術的には、仕様を見ると、sockaddrにはという名前のメンバーが1つありsa_familyます。ここでは、Cレベルで行われるトリッキーなことがありますが、それは私たちにとっては重要ではありません。実際の実装を見ることができますが、この回答ではsa_family sin_family、散文で最も直感的なものを使用して、Cのトリックが重要でない詳細を処理することを信頼して、他の完全に互換性のあるものを使用します。


連続して攻撃すると、プレイヤーの例では無限に強力になります。また、ToolVisitorのソースを変更せずにアプローチを拡張することはできません。しかし、それは素晴らしい解決策です。
ポリノーム

@Polygnomeあなたは例について正しいです。コードは奇妙に見えたが、スクロールしてテキストのすべてのページを通過すると、エラーを見逃した。今すぐ修正。ToolVisitorのソースを変更する要件については、Visitorパターンの特徴的な設計です。それは(私が書いたように)祝福であり、(あなたが書いたように)呪いです。この任意に拡張可能なバージョンが必要な場合の処理​​ははるかに難しく、値だけでなく変数の意味を掘り始め、弱く型付けされた変数や辞書、JSONなどの他のパタ​​ーンを開きます。
コートアンモン

1
残念ながら、本当に情報に基づいた決定を下すには、OPの好みと目標について十分な知識がありません。そして、ええ、完全に柔軟なソリューションを実装するのは難しく、C ++がかなり錆びているので、私はほぼ3時間答えに取り組みました:(
Polygnome

0

一般に、データの通信のみを目的とする場合、いくつかのクラス/継承の実装は避けます。単一のクラスに固執し、そこからすべてを実装できます。あなたの例では、これで十分です

class Tool{
    public:
    //constructor, name etc.
    int GetAttack() { return attack }; //Endpoints for your Player
    int GetDefense() { return defense };
    protected:
         int attack;
         int defense;
};

おそらく、あなたはあなたのゲームがいくつかの種類の剣などを実装すると予想していますが、これを実装する他の方法があるでしょう。クラスの爆発はめったに最高のアーキテクチャではありません。複雑にしないでおく。


0

前述のように、これは深刻なコード臭です。ただし、問題の原因は、デザインの構成ではなく継承を使用していると考えることができます。

たとえば、あなたが私たちに見せたことを考えると、明らかに3つの概念があります:

  • 項目
  • 攻撃できるアイテム。
  • 防御できるアイテム。

4番目のクラスは、最後の2つの概念の単なる組み合わせであることに注意してください。ですから、このためにコンポジションを使用することをお勧めします。

攻撃に必要な情報を表すデータ構造が必要です。そして、防御に必要な情報を表すデータ構造が必要です。最後に、これらのプロパティのいずれかまたは両方を持っている場合と持っていない場合を表すデータ構造が必要です。

class Attack
{
private:
  int attack_;

public:
  int AttackValue() const;
};

class Defense
{
private:
  int defense_

public:
  int DefenseValue() const;
};

class Tool
{
private:
  std::optional<Attack> atk_;
  std::optional<Defense> def_;

public:
  const std::optional<Attack> &GetAttack() const {return atk_;}
  const std::optional<Defense> &GetDefense() const {return def_;}
};

また、常に構成するアプローチを使用しないでください:)!この場合、なぜコンポジションを使用するのですか?私はそれが代替ソリューションであることに同意しますが、フィールドを「カプセル化する」ためのクラスを作成する(「」に注意してください)このケースでは奇妙に思えます
...-AilurusFulgens

@AilurusFulgens:今日は「フィールド」です。明日はどうなりますか?この設計はできますAttackDefenseのインタフェースを変更することなく、より複雑になることTool
ニコルボーラス

1
それでもこれでToolをうまく拡張することはできません-確かに、攻撃と防御はより複雑になるかもしれませんが、それだけです。コンポジションを最大限に活用する場合Tool、拡張のために開いたまま、変更のために完全に閉じることができます。
ポリノーム

@Polygnome:このような些細なケースのために、任意のコンポーネントシステム全体を作成するという問題を経験したい場合、それはあなた次第です。個人的には、変更Toolせずに拡張したい理由はありません。そして、もし私がそれを修正する権利を持っているなら、私は任意のコンポーネントの必要性を見ません。
ニコルボーラス

ツールが独自の制御下にある限り、変更できます。しかし、「修正のために閉じられ、拡張のために開かれた」という原則は、正当な理由で存在します(ここで詳しく説明するには長すぎます)。でも、それほど些細なことではないと思います。RPGの柔軟なコンポーネントシステムの計画に適切な時間を費やすと、長期的には多大な報酬を獲得できます。単純なフィールドを使用するよりも、このタイプの構成に追加の利点はありません。攻撃と防御をさらに専門化できることは、非常に理論的なシナリオのようです。しかし、私が書いたように、それはOPの正確な要件に依存します。
ポリノーム

0

なぜ抽象メソッドmodifyAttackをクラスmodifyDefenseで作成しないのToolですか?その後、各子には独自の実装があり、このエレガントな方法を呼び出します。

for(Tool* tool : tools){
    currentAttack = tool->recalculateAttack(currentAttack);
    currentDefense = tool->recalculateDefense(currentDefense);
}
// proceed with new values for currentAttack and currentDefense

参照として値を渡すと、次のことができる場合にリソースを節約できます。

for(Tool* tool : tools){
    tool->recalculateAttack(&currentAttack);
    tool->recalculateDefense(&currentDefense);
}
// proceed with new values for currentAttack and currentDefense

0

ポリモーフィズムを使用する場合、どのクラスを使用するかを考慮するすべてのコードがクラス自体の内部にあることが常に最適です。これは私がそれをコーディングする方法です:

class Tool{
 public:
   virtual void equipTo(Player* player) =0;
   virtual void unequipFrom(Player* player) =0;
};

class Sword : public Tool{
  public:
    int attack;
    virtual void equipTo(Player* player) {
      player->attackBonus+=this->attack;
    };
    //unequipFrom = reverse equip
};
class Shield : public Tool{
  public:
    int defense;
    virtual void equipTo(Player* player) {
      player->defenseBonus+=this->defense;
    };
    //unequipFrom = reverse equip
};
//other tools
class Player{
  public:
    int baseAttack;
    int baseDefense;
    int attackBonus;
    int defenseBonus;

    virtual void equip(Tool* tool) {
      tool->equipTo(this);
      this->tools.push_back(tool)
    };

    //unequip = reverse equip

    void attack(){
      //modified attack and defense
      int modifiedAttack = baseAttack + this->attackBonus;
      int modifiedDefense = baseDefense+ this->defenseBonus;
      //some other functions to start attack
    }
  private:
    vector<Tool*> tools;
};

これには次の利点があります。

  • 新しいクラスを簡単に追加できます:すべての抽象メソッドを実装するだけで、残りのコードは機能します
  • クラスを簡単に削除できます
  • 新しい統計を追加するのが簡単です(統計を気にしないクラスはそれを無視します)

少なくとも、プレーヤーからボーナスを削除するunequip()メソッドも含める必要があります。
ポリノーム

0

このアプローチの欠陥を認識する1つの方法は、論理的な結論に至るまでアイデアを発展させることだと思います。

これはゲームのように見えるので、ある段階でパフォーマンスについて心配し始め、文字列の比較をintorに置き換えますenum。アイテムのリストが長くなると、それif-elseはかなり扱いにくくなり始めるので、リファクタリングしてに検討することができますswitch-case。また、この時点でかなりのテキストの壁があるので、それぞれのアクションをcase個別の機能に分割できます。

このポイントに達すると、コードの構造が馴染み始めます-自作の手巻きvtable *のように見え始めます-通常、仮想メソッドが実装される基本構造です。ただし、これはvtableであり、アイテムタイプを追加または変更するたびに、手動で更新および保守する必要があります。

「実際の」仮想関数に固執することにより、各アイテムの動作の実装をアイテム自体内に保持できます。自己完結型の一貫した方法でアイテムを追加できます。そして、あなたがこれらすべてを行うとき、動的なディスパッチの実装を処理するのはあなたではなくコンパイラです。

特定の問題を解決するには、いくつかのアイテムは攻撃にのみ影響し、一部のアイテムは防御にのみ影響するため、攻撃と防御を更新するための単純な仮想関数のペアの作成に苦労しています。とにかく両方の動作を実装するこのような単純なケースのトリックですが、特定のケースでは効果がありません。GetDefenseBonus()返される可能性があります0またはApplyDefenseBonus(int& defence)単にままにする可能性がありますdefence変わりません。どのように対処するかは、効果がある他のアクションをどのように処理するかによって異なります。より複雑な場合、より多様な動作がある場合は、アクティビティを単一のメソッドに単純に組み合わせることができます。

*(ただし、典型的な実装に関連して転置)


0

すべての可能な「ツール」について知っているコードのブロックを持つことは、素晴らしい設計ではありません(特に、あなたのコードにそのようなブロックがたくさんあるので)。しかし、どちらもTool考えられるすべてのツールプロパティの基本的なスタブを持っていません。現在、Toolクラスは考えられるすべての使用法を知っている必要があります。

どのようなツールが知っていることは、それはそれを使用して文字に寄与することができるものです。したがって、すべてのツールに1つのメソッドを提供しますgiveto(*Character owner)。他のツールが何ができるかを知らずに、プレイヤーの統計を適切に調整します。何が最良か、キャラクターの無関係なプロパティについて知る必要もありません。例えば、シールドも属性について知る必要はありませんattackinvisibilityhealthツールを適用するために必要なのなどのすべてのオブジェクトが必要であることの属性をサポートするための文字です。ロバに剣を渡そうとして、ロバにattack統計がない場合、エラーが発生します。

ツールにはremove()、所有者への影響を逆にする方法も必要です。これは少し注意が必要です(与えられてから取り除かれるとゼロ以外の効果を残すツールになる可能性があります)が、少なくとも各ツールにローカライズされています。


-4

臭いがないと言う答えはないので、私はその意見を表明します。このコードはまったく問題ありません!私の意見は、新しいものを作成するにつれて、次の段階に進みやすくなり、スキルが徐々に向上することがあるという事実に基づいています。完璧なアーキテクチャの作成に何日も立ち往生することもありますが、おそらくプロジェクトを完了したことがないため、誰も実際にそれを見ることはないでしょう。乾杯!


4
個人的な経験でスキルを向上させるのは良いことです。しかし、すでにその個人的な経験を持っている人に尋ねることによってスキルを向上させるので、自分で穴に陥る必要はありません。そもそも人々がここで質問をする理由ですよね?
グラハム

私は同意しません。しかし、私はこのサイトがすべて深くなることについて理解している。時にはそれは過度に物足りなことを意味します。これが私がこの意見を投稿したかった理由です。現実に定着しており、初心者向けのヒントを探しているなら、初心者にとって知っておくと便利な「十分」についての章全体を見逃しています。
オストメイストロ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.