web-dev-qa-db-ja.com

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

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

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ツールの仮想メソッドを使用します。各ツールには異なるプロパティがあり、各ツールはプレーヤーの攻撃と防御に影響を与えるため、プレーヤーの攻撃と防御の更新はPlayerオブジェクト内で行う必要があります。

しかし、このデザインには、長いif-elseステートメント。このデザインは「修正」する必要がありますか?その場合、どうすれば修正できますか?

35
ggrr

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

ツールで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で)。

64
Doc Brown

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

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

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

  • 他の人が述べたように、attackおよびdefenseメンバーを基本クラスに移動し、それらを_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); }
    • 統計には、動的な方法でアクセスできます。短い文字列または定数をキーとして使用することで、プレーヤーや「所有者」で必ずしも処理する必要のない新しいギア固有の値やボーナスを導入することもできます。
    • 攻撃/防御の値をちょうど間に合うように要求するだけの場合と比較すると、これにより全体がより動的になるだけでなく、不要な呼び出しが削減され、キャラクターに永続的に影響を与えるアイテムを作成することもできます。

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

23
Mario

@DocBrownは良い答えを出しましたが、十分とは言えません、imho。答えの評価を始める前に、ニーズを評価する必要があります。あなたは何が本当に必要ですか

以下では、2つの可能なソリューションを示します。これらのソリューションは、さまざまなニーズに対してさまざまな利点を提供します。

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

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は、依然として仮想ディスパッチに依存するソリューションを提供しており、表示されなかったコードの一部のツールを何らかの形で専門化する場合、それは利点となります。ただし、他の動作も本当に必要または変更したい場合は、次の解決策をお勧めします。

継承を介した構成

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

私が示す2番目のソリューションは、コメントで先に示唆したものです-継承の代わりに構成を使用し、「変更のために閉じ、拡張のために開く*」の原則に従います。エンティティシステムの動作に慣れている場合、いくつかのことおなじみのように見えます(私はESの弟として構成を考えるのが好きです)。

以下に示すものは、JavaまたはC#などのランタイムタイプ情報を含む言語では非常にエレガントです。したがって、表示するC++コードには、「簿記」を含める必要があります。ここでコンポジションを機能させるために必要なのは、C++の経験が豊富な人なら、もっと良いアプローチを提案できるでしょう。

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

まず、新しいクラスを紹介します

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のソースに再び触れることはありません-open for extension-他のものを変更し、他のコンポーネントをそれに渡すだけで、ツールの動作を拡張できます。

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

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つのコンポーネントを持つprocessツールを使用します。他のことは何も気にしません。

あなたが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/3a29e1b106c6bb9c7308e89dd715ee2

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

編集

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

7
Polygnome

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

私はあなたのドメインを別の方法でモデリングします。

ユースケースでは、ToolにはAttackBonusDefenseBonusがあり、どちらも0羽のようなものやそのようなものと戦うのに役に立たない場合。

攻撃の場合、使用する武器からbaserate + bonusを手に入れます。防衛baserate + bonusについても同様です。

その結果、あなたのToolは、攻撃/防御ボニーを計算するためのvirtualメソッドを持つ必要があります。

tl; dr

優れたデザインを使用すると、ハッキーなifsを回避できます。

1
Thomas Junk

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

このような一般的なコンテナからデータをキャストするパターンの基本的な問題は、データのプロデューサとデータのコンシューマが連携する必要があることですが、そうであるかどうかは明らかではない場合がありますこのパターンのすべての例で、臭いか臭いかは、これが基本的な問題です。次の開発者があなたがこのパターンを行っていることを完全に認識していないために偶然にそれを壊すことは非常に可能です。開発者。彼が存在することを知らないかもしれない詳細のために、彼が意図せずにコードを壊さないようにする必要があります。

たとえば、プレーヤーをコピーしたい場合はどうなりますか?プレーヤーオブジェクトのコンテンツを見るだけでも、かなり簡単に見えます。 attackdefensetools変数をコピーするだけです。やさしい!まあ、ポインタを使用すると少し難しくなることがすぐにわかります(ある時点で、スマートポインタを見る価値がありますが、それは別のトピックです)。それは簡単に解決されます。各ツールの新しいコピーを作成し、それらを新しい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));

なぜこれが同じパターンなのですか? bindsockaddr_in*を受け入れないため、より一般的なsockaddr*を受け入れます。これらのクラスの定義を見ると、sockaddrには、sin_family *に割り当てたファミリのメンバーが1つだけあることがわかります。ファミリは、sockaddrをどのサブタイプにキャストする必要があるかを示します。 AF_INETは、アドレス構造体が実際にはsockaddr_inであることを示します。 AF_INET6の場合、アドレスはsockaddr_in6になり、より大きなIPv6アドレスをサポートするためにフィールドが大きくなります。

これは、std::stringではなく整数を使用してファミリを指定することを除いて、Toolの例と同じです。しかし、私はそれがにおいがないと主張し、「ソケットを行うためのその標準的な方法なので、それは「においがするべきではない」以外の理由でそうしようとします。明らかに同じパターンです。一般的なオブジェクトにデータを格納してキャストすることは自動的にコードの匂いではないと主張する理由ですが、その方法にはいくつかの違いがあり、より安全です。

このパターンを使用する場合、最も重要な情報は、プロデューサーからコンシューマーへのサブクラスに関する情報の伝達をキャプチャすることです。これはnameフィールドで行っていることであり、UNIXソケットはsin_familyフィールドで行っています。このフィールドは、消費者が生産者が実際に作成したものを理解するために必要な情報です。このパターンのすべての場合、それは列挙型(または少なくとも、列挙型のように機能する整数)である必要があります。どうして?消費者が情報をどうするかについて考えます。彼らはあなたがしたように、いくつかの大きなifステートメントまたはswitchステートメントを書き出して、正しいサブタイプを決定し、それをキャストし、データを使用する必要があります。定義により、これらのタイプはごく少数しか存在できません。あなたがしたように、あなたはそれを文字列に格納することができますが、それは多くの欠点を持っています:

  • 遅い-std::stringは通常、文字列を保持するために動的メモリを使用する必要があります。また、どのサブクラスを持っているのかを把握するたびに、名前と一致するフルテキスト比較を行う必要があります。
  • 多すぎる-あなたが非常に危険なことをしているときに自分に制約を課すために言われるべきことがある。このようなシステムで、substringを探して、それがどのタイプのオブジェクトを見ているのかを伝えてきました。これは、オブジェクトの名前が誤ってにその部分文字列を含み、ひどく不可解なエラーが発生するまで、うまく機能しました。上で述べたように、必要なのは少数のケースだけなので、文字列などの非常に強力なツールを使用する理由はありません。これはにつながります...
  • エラーが発生しやすい-ある消費者が誤って魔法の布の名前を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ケースだけです。あなたが物事を完璧に行うならば、あなたはそのような状況に決して乗るべきではありません。ただし、誰かが新しいツールタイプを追加し、それをサポートするようにコードを更新し忘れた場合は、エラーをキャッチする何かが必要になります。実際、必要がない場合でも追加するように、私はそれらを非常に推奨しています。

enumのもう1つの大きな利点は、次の開発者に有効なツールタイプの完全なリストを直接提供できることです。エピックボスバトルで使用するボブの特殊なフルートクラスを見つけるために、コードをたどる必要はありません。

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のように新しいプロトコルが登場した場合は、新しいフィールドを追加する必要があります。 union構造体を使用してこれを行った場合、同じ名前の構造体の新しいバージョンを効率的に作成し、互換性の問題が無限に続くことになります。これが、UNIXソケットがunion構造体ではなくキャストパターンの使用を選択した理由です。きっと彼らはそれを検討したと思います、そして彼らがそれについて考えたという事実は、彼らがそれを使用するときにそれがにおいがしない理由の一部です。

実際にunionを使用することもできます。組合は最大のメンバーと同じくらい大きくなるだけでメモリを節約しますが、組合には独自の問題があります。これはおそらくコードのオプションではありませんが、常に考慮すべきオプションです。

別の興味深い解決策は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を調べると、シールドがあることがわかっているため(この関数が呼び出されたため)、結局、AttackVisitorvoid ToolVisitor::visit(Shield* shield)を呼び出すことになります。これで正しいコードが実行され、防御が更新されます。

お客さんはかさばります。それはとても不格好で、私はそれがそれ自身の匂いを持っているとほとんど思います。悪いビジターパターンを書くのはとても簡単です。ただし、他のどれにもない1つの大きな利点があります。新しいツールタイプを追加する場合は、新しいToolVisitor::visit関数を追加する必要があります。これを行うと、プログラムのeveryToolVisitorは、仮想関数がないため、コンパイルを拒否します。これにより、何かを逃したすべてのケースを簡単にキャッチできます。 ifまたはswitchステートメントを使用して作業を行うことを保証することははるかに困難です。これらの利点は、ビジターが3Dグラフィックシーンジェネレーターにすてきなニッチ市場を見つけるのに十分です。彼らはたまたまビジターが提供するような行動を正確に必要とするので、それは素晴らしい働きをします!

全体として、これらのパターンは次の開発者にとって困難になることに注意してください。時間をかけて簡単に使用できるようにすれば、コードの臭いがなくなります。

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

1
Cort Ammon

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

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

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

0
Arthur Havlicek

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

たとえば、あなたが私たちに示したものを考えると、あなたは明らかに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_;}
};
0
Nicol Bolas

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
Paulo Amaral

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

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;
};

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

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

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

これはゲームのように見えるので、ある段階でパフォーマンスを心配し始め、それらの文字列比較をintまたはenumに交換することになるでしょう。項目のリストが長くなると、その_if-else_は扱いにくくなり始めるので、_switch-case_にリファクタリングすることを検討してください。この時点でかなりのテキストの壁もあるので、各case内のアクションを個別の関数に分割できます。

この時点に達すると、コードの構造はおなじみのように見え始めます-それは自作の手作業のvtable *のように見え始めます-仮想メソッドが通常実装される基本構造。例外として、これはvtableであり、アイテムタイプを追加または変更するたびに手動で更新して維持する必要があります。

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

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

*(ただし、一般的な実装に関連して置き換えられます)

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

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

ツールにはremove()メソッドも必要です。これにより、所有者に対する影響が逆になります。これは少しトリッキーですが(与えられたときにゼロ以外の効果を残してから削除されるツールになる可能性があります)、少なくとも各ツールにローカライズされています。

0
alexis