มันเป็นกลิ่นรหัสในการจัดเก็บวัตถุทั่วไปในภาชนะและจากนั้นรับวัตถุและทำให้วัตถุตกต่ำจากคอนเทนเนอร์?


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 มาตรฐานในการลบการทดสอบสำหรับคลาสย่อยที่เฉพาะเจาะจง (และดาวน์ไลท์ที่ตามมา) คือการสร้าง a หรือในกรณีนี้อาจจะมีสองวิธีเสมือนในคลาสพื้นฐานที่จะใช้แทนการใช้ chain และ casts สิ่งนี้สามารถใช้ลบถ้าของทั้งหมดและมอบหมายการดำเนินการไปยังคลาสย่อยที่จะใช้ คุณไม่จำเป็นต้องแก้ไขคำสั่ง if ทุกครั้งที่คุณเพิ่มคลาสย่อยใหม่
Erik Eidt

2
ให้พิจารณา Double Dispatch ด้วย
Boris the Spider

ทำไมไม่เพิ่มคุณสมบัติให้กับคลาสเครื่องมือของคุณที่เก็บพจนานุกรมประเภทแอททริบิว (เช่นการโจมตีการป้องกัน) และค่าที่กำหนดให้ การโจมตีการป้องกันสามารถระบุค่าได้ จากนั้นคุณสามารถเรียกค่าจาก Tool เองโดยค่าคงที่ที่ระบุ
user1740075

8
ฉันจะออกจากที่นี่: ericlippert.com/2015/04/27/wizards-and-warriors-part-one
คุณ

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วิธีนี้ใน codebase ของคุณ คุณสามารถทำได้new Tool("sword", swordAttack, swordDefense)จากไฟล์เช่น JSON
AmazingDreams

7
@AmazingDreams: นั่นถูกต้อง (สำหรับส่วนของรหัสที่เราเห็นที่นี่) แต่ฉันเดาว่า OP ได้ทำให้รหัสจริงของเขาง่ายขึ้นสำหรับคำถามของเขาที่จะมุ่งเน้นในแง่มุมที่เขาต้องการพูดคุย
Doc Brown

3
นี่ไม่ได้ดีไปกว่ารหัสเดิม (ก็เป็นบิต) เครื่องมือใด ๆ ที่มีคุณสมบัติเพิ่มเติมไม่สามารถสร้างได้โดยไม่ต้องเพิ่มวิธีการเพิ่มเติม ฉันคิดว่าในกรณีนี้เราควรสนับสนุนการแต่งเรื่องการสืบทอด ใช่ปัจจุบันมีเพียงการโจมตีและการป้องกัน แต่ก็ไม่จำเป็นต้องอยู่ต่อไป
Polygnome

1
@DocBrown ใช่ว่าเป็นเรื่องจริงแม้ว่ามันจะดูเหมือนว่าเป็นเกม RPG ที่ตัวละครมีสถิติบางอย่างที่ถูกดัดแปลงโดยเครื่องมือหรือไอเท็มที่ติดตั้งไว้มากกว่า ฉันจะสร้างToolตัวดัดแปลงทั่วไปที่เป็นไปได้ทั้งหมดเติมบางvector<Tool*>สิ่งที่อ่านจากไฟล์ข้อมูลจากนั้นก็วนรอบพวกเขาและแก้ไขสถิติแบบที่คุณทำอยู่ตอนนี้ คุณจะมีปัญหาเมื่อคุณต้องการให้ไอเท็มมอบเช่นโบนัส 10% สำหรับการโจมตี อาจจะtool->modify(playerStats)เป็นตัวเลือกอื่น
AmazingDreams

23

ปัญหาหลักของรหัสคือเมื่อใดก็ตามที่คุณแนะนำรายการใหม่คุณไม่เพียง แต่ต้องเขียนและอัปเดตรหัสของรายการคุณยังต้องแก้ไขโปรแกรมเล่นของคุณ (หรือที่ใดก็ตามที่มีการใช้รายการ) ซึ่งทำให้สิ่งทั้งหมด ซับซ้อนมากขึ้น

ตามกฎทั่วไปของหัวแม่มือฉันคิดว่ามันเป็นเรื่องที่น่ารังเกียจเสมอเมื่อคุณไม่สามารถพึ่งพา subclassing / การสืบทอดตามปกติได้และต้องทำด้วยตัวคุณเอง

ฉันนึกถึงสองแนวทางที่เป็นไปได้ที่ทำให้สิ่งทั้งหมดมีความยืดหยุ่นมากขึ้น:

  • ในฐานะที่เป็นคนพูดถึงย้าย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); }
    • สถิติสามารถเข้าถึงได้ด้วยวิธีการบางอย่างเช่นการใช้สตริงสั้น ๆ หรือค่าคงที่เป็นคีย์ดังนั้นคุณสามารถแนะนำค่าหรือโบนัสใหม่ ๆ ที่คุณไม่จำเป็นต้องจัดการในเครื่องเล่นหรือ "เจ้าของ" โดยเฉพาะ
    • เมื่อเปรียบเทียบกับการร้องขอค่าการโจมตี / การป้องกันเพียงทันเวลาสิ่งนี้ไม่เพียงทำให้ทุกอย่างมีชีวิตชีวายิ่งขึ้น แต่ยังช่วยให้คุณประหยัดการโทรที่ไม่จำเป็นและยังช่วยให้คุณสร้างรายการที่จะส่งผลต่อตัวละครของคุณอย่างถาวร

      ตัวอย่างเช่นลองนึกภาพแหวนสาปที่เพิ่งตั้งค่าสถิติที่ซ่อนอยู่เมื่อติดตั้งแล้วทำเครื่องหมายตัวละครของคุณว่าสาปอย่างถาวร


7

ในขณะที่ @DocBrown ให้คำตอบที่ดีก็ไม่ได้ไปไกลพอ ก่อนที่คุณจะเริ่มประเมินคำตอบคุณควรประเมินความต้องการของคุณ คุณต้องการอะไรจริงๆ

ด้านล่างฉันจะแสดงโซลูชันที่เป็นไปได้สองแบบซึ่งมีข้อดีแตกต่างกันสำหรับความต้องการที่แตกต่างกัน

วิธีแรกนั้นง่ายมากและปรับให้เหมาะกับสิ่งที่คุณแสดง:

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 อยู่ หนึ่งในสิ่งที่เป็นสิ่งสำคัญสำหรับ RPGs คือจะต้องเปิดให้บริการสำหรับส่วนขยาย วิธีแก้ปัญหาที่แสดงจนถึงตอนนี้ไม่ได้นำเสนอ คุณจะต้องเปลี่ยนToolคลาสและเพิ่มวิธีการเสมือนใหม่ทุกครั้งที่คุณต้องการแอตทริบิวต์ใหม่

ทางออกที่สองที่ฉันแสดงคือคำตอบที่ฉันได้กล่าวไว้ก่อนหน้านี้ - ใช้การประพันธ์แทนการสืบทอดและทำตาม "การปิดเพื่อการแก้ไขเปิดสำหรับหลักการขยาย * หากคุณคุ้นเคยกับการทำงานของระบบเอนทิตี้ จะดูคุ้นเคย (ฉันชอบคิดว่าองค์ประกอบเป็นน้องชายของ ES)

โปรดทราบว่าสิ่งที่ฉันแสดงด้านล่างมีความหรูหรามากกว่าในภาษาที่มีข้อมูลประเภทรันไทม์เช่น Java หรือ C # ดังนั้นรหัส C ++ ที่ฉันแสดงจะต้องมี "การทำบัญชี" บางอย่างที่จำเป็นสำหรับการแต่งเพลงที่นี่ บางทีคนที่มีประสบการณ์ C ++ มากขึ้นสามารถแนะนำวิธีที่ดีกว่า

ครั้งแรกที่เราดูอีกครั้งที่ด้านข้างของผู้โทร ในตัวอย่างของคุณคุณในฐานะผู้โทรภายในattackวิธีการไม่สนใจเครื่องมือเลย สิ่งที่คุณสนใจคือคุณสมบัติสองประการ - จุดโจมตีและป้องกัน คุณไม่ได้จริงๆดูแลที่ผู้ที่มาจากและคุณไม่สนใจเกี่ยวกับคุณสมบัติอื่น ๆ (เช่นความเร็ววิ่งคล่องตัว)

ดังนั้นก่อนอื่นเราขอแนะนำคลาสใหม่

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

จากนั้นเราสร้างสององค์ประกอบแรกของเรา

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

โปรดทราบว่าในตัวอย่างนี้ฉันสนับสนุนเฉพาะการมีองค์ประกอบเดียวของแต่ละประเภท - สิ่งนี้ทำให้ง่ายขึ้น ในทางทฤษฎีคุณสามารถอนุญาตให้มีองค์ประกอบหลาย ๆ ประเภทที่เหมือนกันได้ แต่มันก็น่าเกลียดอย่างรวดเร็ว สิ่งสำคัญอย่างหนึ่ง: Toolขณะนี้ถูกปิดเพื่อการปรับเปลี่ยน - เราจะไม่แตะต้องแหล่งที่มาของToolอีกต่อไป แต่เปิดเพื่อขยาย - เราสามารถขยายพฤติกรรมของเครื่องมือโดยดัดแปลงสิ่งอื่น ๆ และเพียงแค่ส่งส่วนประกอบอื่น ๆ เข้ามา

ตอนนี้เราต้องการวิธีการดึงเครื่องมือตามประเภทส่วนประกอบ คุณยังคงสามารถใช้เวกเตอร์สำหรับเครื่องมือได้เหมือนในตัวอย่างโค้ดของคุณ:

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คุณประมวลผลเครื่องมือที่มีสององค์ประกอบ - คุณไม่สนใจสิ่งอื่นใด

สมมติว่าคุณมี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;
    }
}

โปรดทราบว่าเราเพิ่มพฤติกรรมบางอย่างในเครื่องมือของเราโดยไม่ต้องแก้ไขคลาสเครื่องมือเลย

คุณสามารถ (และควร) ย้ายสตริงไปยังตัวแปรแมโครหรือสแตติก const ดังนั้นคุณไม่จำเป็นต้องพิมพ์ซ้ำแล้วซ้ำอีก

หากคุณใช้แนวทางนี้เพิ่มเติมเช่นสร้างส่วนประกอบที่สามารถเพิ่มเข้าไปในเครื่องเล่นและสร้างCombatส่วนประกอบที่ตั้งค่าสถานะผู้เล่นว่าสามารถมีส่วนร่วมในการต่อสู้คุณสามารถกำจัดattackวิธีนี้ได้เช่นกันและให้จัดการได้ โดย Component หรือดำเนินการที่อื่น

ข้อได้เปรียบของการทำให้ผู้เล่นสามารถรับคอมโพเนนต์ได้ด้วยเช่นนั้นคุณไม่จำเป็นต้องเปลี่ยนผู้เล่นเพื่อให้เขามีพฤติกรรมที่แตกต่าง ในตัวอย่างของฉันคุณสามารถสร้างMovableส่วนประกอบวิธีที่คุณไม่จำเป็นต้องใช้walkToวิธีการในโปรแกรมเล่นเพื่อให้เขาย้าย คุณเพียงแค่สร้างส่วนประกอบเชื่อมต่อกับเครื่องเล่นและปล่อยให้คนอื่นประมวลผล

คุณสามารถหาตัวอย่างที่สมบูรณ์ได้ในส่วนสำคัญนี้: https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20

เห็นได้ชัดว่าวิธีการแก้ปัญหานี้มีความซับซ้อนมากขึ้นเล็กน้อยจากนั้นส่วนอื่น ๆ ที่โพสต์ แต่ขึ้นอยู่กับว่าคุณต้องการความยืดหยุ่นแค่ไหนคุณต้องการที่จะใช้มันนานแค่ไหนนี่อาจเป็นวิธีที่ทรงพลังมาก

แก้ไข

คำตอบอื่น ๆ เสนอการถ่ายทอดมรดกโดยตรง (การสร้างดาบขยายเครื่องมือ, สร้างโล่ขยายเครื่องมือ) ฉันไม่คิดว่านี่เป็นสถานการณ์ที่มรดกทำงานได้ดีมาก จะเป็นอย่างไรถ้าคุณตัดสินใจว่าการปิดกั้นด้วยเกราะในบางวิธีสามารถสร้างความเสียหายให้กับผู้โจมตีได้? ด้วยวิธีการแก้ปัญหาของฉันคุณสามารถเพิ่มองค์ประกอบโจมตีลงในเกราะและตระหนักว่าไม่มีการเปลี่ยนแปลงรหัสของคุณ ด้วยการสืบทอดคุณจะมีปัญหา รายการ / เครื่องมือในเกม RPG เป็นตัวเลือกอันดับต้นสำหรับการแต่งเพลงหรือแม้กระทั่งตรงไปตรงมาโดยใช้ระบบนิติบุคคลตั้งแต่เริ่มต้น


1

โดยทั่วไปถ้าคุณต้องการใช้if(ร่วมกับการกำหนดประเภทของอินสแตนซ์) ในภาษา OOP ใด ๆ นั่นเป็นสัญญาณบ่งบอกว่ามีบางอย่างที่เกิดขึ้น อย่างน้อยคุณควรมองใกล้รุ่นของคุณ

ฉันจะสร้างแบบจำลองโดเมนของคุณแตกต่างกัน

สำหรับผู้ใช้ของคุณเอToolมีAttackBonusและDefenseBonus- ซึ่งอาจเป็นได้ทั้ง0ในกรณีที่มันไร้ประโยชน์สำหรับการต่อสู้เหมือนขนนกหรืออะไรทำนองนั้น

สำหรับการโจมตีคุณมีbaserate+ ของคุณbonusจากอาวุธที่ใช้ เดียวกันจะไปสำหรับการป้องกัน+baseratebonus

ดังนั้นคุณToolต้องมีvirtualวิธีคำนวณหาค่า boni โจมตี / ป้องกัน

TL; DR

ด้วยการออกแบบที่ดีกว่าคุณสามารถหลีกเลี่ยงการแฮ็if


บางครั้งถ้าจำเป็นจำเป็นเช่นเมื่อเปรียบเทียบค่าสเกลาร์ สำหรับการสลับประเภทวัตถุไม่มาก
Andy

ฮ่าฮ่าถ้าเป็นผู้ประกอบการที่สำคัญและคุณไม่สามารถพูดได้ว่าการใช้เป็นกลิ่นรหัส
tymtam

1
@Tymski ให้ความเคารพคุณอย่างถูกต้อง ฉันทำให้ตัวเองชัดเจนยิ่งขึ้น ฉันไม่ได้ป้องกันifการเขียนโปรแกรมน้อยลง ส่วนใหญ่ในชุดค่าผสมเช่นถ้าinstanceofหรืออะไรเช่นนั้น แต่มีตำแหน่งซึ่งอ้างว่าifs เป็นรหัสและมีวิธีที่จะได้รับรอบ และคุณพูดถูกนั่นเป็นตัวดำเนินการที่จำเป็นซึ่งมีสิทธิ์ของตัวเอง
โทมัสขยะ

1

ตามที่เขียนไว้มัน "มีกลิ่น" แต่นั่นอาจเป็นเพียงตัวอย่างที่คุณให้ การจัดเก็บข้อมูลในภาชนะบรรจุวัตถุทั่วไปจากนั้นนำไปหล่อเพื่อให้สามารถเข้าถึงข้อมูลได้โดยอัตโนมัติไม่มีกลิ่นรหัส คุณจะเห็นมันใช้ในหลาย ๆ สถานการณ์ อย่างไรก็ตามเมื่อคุณใช้งานคุณควรตระหนักถึงสิ่งที่คุณกำลังทำวิธีที่คุณทำและสาเหตุ เมื่อฉันดูตัวอย่างการใช้การเปรียบเทียบแบบอิงสตริงเพื่อบอกฉันว่าวัตถุคืออะไรสิ่งที่เดินทางไปยังเครื่องวัดกลิ่นส่วนตัวของฉัน มันแสดงให้เห็นว่าคุณไม่แน่ใจว่าสิ่งที่คุณทำอยู่ที่นี่ (ซึ่งก็ดีเพราะคุณมีปัญญาที่จะมาที่นี่เพื่อโปรแกรมเมอร์และพูดว่า "เฮ้ฉันไม่คิดว่าฉันชอบสิ่งที่ฉันทำช่วย ฉันออกไป! ")

ปัญหาพื้นฐานที่มีรูปแบบการส่งข้อมูลจากตู้คอนเทนเนอร์ทั่วไปเช่นนี้คือผู้ผลิตข้อมูลและผู้บริโภคข้อมูลต้องทำงานร่วมกัน แต่อาจไม่ชัดเจนว่าพวกเขาทำอย่างรวดเร็วในตอนแรก ในทุกตัวอย่างของรูปแบบนี้ส่งกลิ่นหรือไม่ส่งกลิ่นนี่เป็นปัญหาพื้นฐาน เป็นไปได้มากที่ผู้พัฒนารายต่อไปจะไม่รู้ตัวว่าคุณกำลังทำรูปแบบนี้และทำลายมันโดยไม่ตั้งใจดังนั้นถ้าคุณใช้รูปแบบนี้คุณจะต้องระมัดระวังเพื่อช่วยนักพัฒนารายต่อไป คุณต้องทำให้ง่ายขึ้นสำหรับเขาที่จะไม่ทำลายรหัสโดยไม่ตั้งใจเนื่องจากรายละเอียดบางอย่างที่เขาอาจไม่ทราบว่ามีอยู่จริง

ตัวอย่างเช่นถ้าฉันต้องการคัดลอกผู้เล่น ถ้าฉันแค่ดูเนื้อหาของวัตถุผู้เล่นมันดูง่ายมาก ฉันเพียงแค่ต้องคัดลอกattack, defenseและtoolsตัวแปร ง่ายเหมือนพาย! ฉันจะรู้ได้อย่างรวดเร็วว่าการใช้พอยน์เตอร์ของคุณทำให้มันยากขึ้นนิดหน่อย (ในบางครั้งมันก็คุ้มค่าที่จะดูพอยน์เตอร์ที่ฉลาด แต่นั่นเป็นหัวข้ออื่น) นั่นคือการแก้ไขอย่างง่ายดาย ฉันจะสร้างสำเนาใหม่ของเครื่องมือแต่ละตัวแล้วใส่ลงในtoolsรายการใหม่ของฉัน หลังจากที่ทุกคนToolเป็นชั้นง่ายจริงๆมีเพียงหนึ่งในสมาชิก ดังนั้นผมจึงสร้างพวงของสำเนารวมทั้งสำเนาได้แต่ผมไม่ทราบว่ามันเป็นดาบดังนั้นผมคัดลอกเพียง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* ครอบครัวบอกว่าคุณควรเลือกsockaddrย่อย AF_INETจะบอกคุณว่า struct sockaddr_inอยู่เป็นจริง ถ้าเป็นAF_INET6เช่นนั้นที่อยู่จะเป็น a 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คำสั่งที่เกี่ยวข้องกับ enums ด้วยส่วนที่สำคัญที่สุดเดียวของรูปแบบนี้: defaultกรณีที่พ่น คุณไม่ควรอยู่ในสถานการณ์เช่นนั้นหากคุณทำสิ่งต่าง ๆ อย่างสมบูรณ์แบบ อย่างไรก็ตามหากมีคนเพิ่มประเภทเครื่องมือใหม่และคุณลืมที่จะอัปเดตรหัสของคุณเพื่อสนับสนุนคุณจะต้องมีบางสิ่งที่จะจับข้อผิดพลาด ในความเป็นจริงฉันแนะนำพวกเขามากจนคุณควรเพิ่มพวกเขาแม้ว่าคุณไม่ต้องการ

ข้อดีอีกอย่างของ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 ++ คุณไม่ควรใช้มันหากคุณไม่มีเหตุผลอันสมควร

ทางเลือกหนึ่งที่นิยมมากคือสิ่งที่ฉันเรียกว่า "โครงสร้างสหภาพ" หรือ "สหภาพชั้น" สำหรับตัวอย่างของคุณนี่จะเป็นแบบที่ดีมาก ในการสร้างหนึ่งในนั้นคุณต้องสร้างToolคลาสโดยมีการแจงนับเหมือนก่อน แต่แทนที่จะเป็นคลาสย่อยToolเราเพียงแค่ใส่ฟิลด์ทั้งหมดจากทุกประเภทย่อย

class Tool {
    public:
        enum TypeE {
            kSword,
            kShield,
            kMagicCloth
        };
    TypeE type;

    int   attack;
    int   defense;
};

ตอนนี้คุณไม่ต้องการคลาสย่อยเลย คุณเพียงแค่ต้องดูที่typeเขตข้อมูลเพื่อดูว่าเขตข้อมูลอื่นถูกต้องจริง ปลอดภัยและเข้าใจง่ายกว่ามาก อย่างไรก็ตามมันมีข้อเสีย มีหลายครั้งที่คุณไม่ต้องการใช้สิ่งนี้:

  • เมื่อวัตถุนั้นแตกต่างกันมากเกินไป - คุณสามารถจบลงด้วยรายการของฟิลด์การซักและอาจไม่ชัดเจนว่าจะใช้กับวัตถุชนิดใดในแต่ละประเภท
  • เมื่อทำงานในสถานการณ์ที่มีความสำคัญหน่วยความจำ - หากคุณต้องการสร้างเครื่องมือ 10 ตัวคุณอาจขี้เกียจกับหน่วยความจำ เมื่อคุณต้องการสร้างเครื่องมือ 500 ล้านคุณจะเริ่มสนใจบิตและไบต์ โครงสร้างยูเนี่ยนใหญ่กว่าที่พวกเขาต้องการเสมอ

โซลูชันนี้ไม่ได้ถูกใช้โดยซ็อกเก็ต UNIX เนื่องจากปัญหาที่แตกต่างกันประกอบไปด้วยความสิ้นสุดแบบเปิดของ API จุดประสงค์ของซ็อกเก็ต UNIX คือการสร้างบางสิ่งที่ยูนิกซ์ทุกรสชาติสามารถใช้งานได้ แต่ละรสสามารถกำหนดรายชื่อของครอบครัวที่พวกเขาสนับสนุนเช่นAF_INETและจะมีรายการสั้น ๆ สำหรับแต่ละคน อย่างไรก็ตามหากมีโปรโตคอลใหม่เข้ามาเช่นAF_INET6เคยคุณอาจต้องเพิ่มฟิลด์ใหม่ หากคุณทำสิ่งนี้ด้วยโครงสร้างยูเนี่ยนคุณจะต้องสร้างโครงสร้างเวอร์ชันใหม่ด้วยชื่อเดียวกันอย่างมีประสิทธิภาพเพื่อสร้างปัญหาความไม่ลงรอยกันที่ไม่สิ้นสุด นี่คือเหตุผลที่ซ็อกเก็ต UNIX เลือกใช้รูปแบบการหล่อมากกว่าโครงสร้างยูเนี่ยน ฉันแน่ใจว่าพวกเขาพิจารณามันและความจริงที่ว่าพวกเขาคิดเกี่ยวกับมันเป็นส่วนหนึ่งของสาเหตุที่ไม่มีกลิ่นเมื่อใช้

คุณสามารถใช้สหภาพเป็นของจริงได้ สหภาพแรงงานจะบันทึกความทรงจำโดยมีขนาดใหญ่กว่าสมาชิกที่ใหญ่ที่สุดเท่านั้น นี่อาจไม่ใช่ตัวเลือกสำหรับรหัสของคุณ แต่เป็นตัวเลือกที่คุณควรพิจารณา

boost::variantอีกวิธีที่น่าสนใจคือ Boostเป็นห้องสมุดที่ยอดเยี่ยมเต็มไปด้วยโซลูชันข้ามแพลตฟอร์มที่ใช้ซ้ำได้ อาจเป็นโค้ด C ++ ที่ดีที่สุดที่เคยเขียนมา Boost.Variantนั้นเป็นรุ่น C ++ ของสหภาพ มันเป็นภาชนะที่สามารถมีหลายประเภท แต่เพียงครั้งเดียว คุณสามารถทำให้คุณSword, ShieldและMagicClothชั้นเรียนแล้วให้เครื่องมือที่จะเป็นboost::variant<Sword, Shield, MagicCloth>ความหมายมันมีหนึ่งในบรรดาสามประเภท ปัญหานี้ยังคงมีปัญหาเดียวกันกับความเข้ากันได้ในอนาคตที่ป้องกันไม่ให้ซ็อกเก็ต UNIX ใช้งานได้ (ไม่ต้องพูดถึงซ็อกเก็ต UNIX คือ C, กำลังดำเนินการboostค่อนข้างน้อย!) แต่รูปแบบนี้มีประโยชน์อย่างเหลือเชื่อ บ่อยครั้งที่ใช้ตัวแปรเช่นในการแยกวิเคราะห์ต้นไม้ซึ่งใช้สตริงของข้อความและแยกย่อยโดยใช้ไวยากรณ์สำหรับกฎ

คำตอบสุดท้ายที่ฉันขอแนะนำให้ดูก่อนที่จะกระโดดและใช้วิธีการคัดเลือกวัตถุทั่วไปคือรูปแบบการออกแบบของผู้เข้าชม ผู้เยี่ยมชมเป็นรูปแบบการออกแบบที่ทรงพลังที่ใช้ประโยชน์จากการสังเกตว่าการเรียกฟังก์ชั่นเสมือนจริงนั้นช่วยให้คุณหล่อได้อย่างมีประสิทธิภาพ เนื่องจากคอมไพเลอร์ทำมันจึงไม่มีทางผิด ดังนั้นแทนที่จะเก็บ enum ผู้เยี่ยมชมจะใช้คลาสฐานนามธรรมซึ่งมี 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ตอนนี้เรียกใช้รหัสที่ถูกต้องเพื่ออัปเดตการป้องกันของเรา

ผู้เข้าชมมีขนาดใหญ่ มันเป็นเรื่องที่ดุร้ายจนฉันคิดว่ามันมีกลิ่นของมันเอง มันง่ายมากที่จะเขียนรูปแบบผู้เยี่ยมชมที่ไม่ดี อย่างไรก็ตามมันมีข้อได้เปรียบอย่างใหญ่หลวงที่คนอื่นไม่มี หากเราเพิ่มประเภทเครื่องมือใหม่เราต้องเพิ่มToolVisitor::visitฟังก์ชั่นใหม่สำหรับมัน ทันทีที่เราทำสิ่งนี้ทุก ToolVisitorโปรแกรมจะปฏิเสธที่จะรวบรวมเพราะมันขาดฟังก์ชั่นเสมือน สิ่งนี้ทำให้ง่ายต่อการติดตามทุกกรณีที่เราพลาดบางสิ่งบางอย่าง มันยากมากที่จะรับประกันว่าถ้าคุณใช้ifหรือswitchคำสั่งเพื่อทำงาน ข้อดีเหล่านี้ดีพอที่ผู้เยี่ยมชมพบว่ามีช่องเล็ก ๆ ในตัวสร้างฉากกราฟิก 3 มิติ พวกเขาต้องการชนิดของพฤติกรรมที่ผู้เข้าชมเสนออย่างดีเพื่อให้ใช้งานได้ดี!

ในทุกครั้งโปรดจำไว้ว่ารูปแบบเหล่านี้ทำให้นักพัฒนารายต่อไปยาก ใช้เวลาทำให้มันง่ายขึ้นสำหรับพวกเขาและรหัสจะไม่มีกลิ่น!

* เทคนิคหากดูข้อมูลจำเพาะ sockaddr sa_familyมีสมาชิกคนหนึ่งชื่อ มีเรื่องยุ่งยากเกิดขึ้นที่นี่ในระดับ C ซึ่งไม่สำคัญสำหรับเรา คุณสามารถดูการใช้งานจริงได้แต่สำหรับคำตอบนี้ฉันจะใช้sa_family sin_familyและคนอื่น ๆ ใช้แทนกันได้อย่างสมบูรณ์โดยใช้ข้อใดก็ตามที่ง่ายที่สุดสำหรับงานประพันธ์โดยเชื่อว่า C trickery จะดูแลรายละเอียดที่ไม่สำคัญ


การโจมตีอย่างต่อเนื่องจะทำให้ผู้เล่นแข็งแกร่งในตัวอย่างของคุณ และคุณไม่สามารถขยายแนวทางของคุณได้โดยไม่ต้องดัดแปลงแหล่งที่มาของ ToolVisitor มันเป็นทางออกที่ดีแม้ว่า
Polygnome

@Polygnome คุณพูดถูกเกี่ยวกับตัวอย่าง ฉันคิดว่ารหัสดูแปลก แต่การเลื่อนผ่านหน้าข้อความเหล่านั้นทั้งหมดฉันพลาดข้อผิดพลาด แก้ไขได้ทันที สำหรับความต้องการในการปรับเปลี่ยนแหล่งที่มาของ ToolVisitor นั่นเป็นลักษณะการออกแบบของรูปแบบผู้เข้าชม มันเป็นคำอวยพร (ตามที่ฉันเขียน) และคำสาป (ตามที่คุณเขียน) การจัดการกรณีที่คุณต้องการเวอร์ชั่นที่ขยายได้โดยพลการนั้นยากกว่ามากและเริ่มขุดด้วยความหมายของตัวแปรมากกว่าเพียงแค่ค่าของมันและเปิดรูปแบบอื่น ๆ เช่นตัวแปรที่พิมพ์อย่างอ่อนและพจนานุกรมและ JSON
Cort Ammon

1
ใช่น่าเศร้าที่เราไม่มีความรู้เพียงพอเกี่ยวกับการตั้งค่าและเป้าหมายของ OPs ในการตัดสินใจอย่างชาญฉลาด และใช่ทางออกที่ยืดหยุ่นอย่างสมบูรณ์นั้นยากที่จะนำไปใช้จริงฉันทำงานกับคำตอบเกือบ 3 ชั่วโมงเนื่องจาก C ++ ของฉันค่อนข้างจะเป็นสนิม :(
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 แนวคิดอย่างชัดเจน:

  • ชิ้น
  • ไอเท็มที่สามารถโจมตีได้
  • ไอเท็มที่สามารถป้องกันได้

โปรดทราบว่าคลาสที่สี่ของคุณเป็นเพียงการรวมกันของแนวคิดสองประการสุดท้าย ดังนั้นฉันขอแนะนำให้ใช้องค์ประกอบสำหรับสิ่งนี้

คุณต้องการโครงสร้างข้อมูลเพื่อแสดงข้อมูลที่จำเป็นสำหรับการโจมตี และคุณต้องการโครงสร้างข้อมูลที่แสดงถึงข้อมูลที่คุณต้องการสำหรับการป้องกัน สุดท้ายคุณต้องมีโครงสร้างข้อมูลเพื่อแสดงสิ่งต่าง ๆ ที่อาจมีหรือไม่มีคุณสมบัติเหล่านี้อย่างใดอย่างหนึ่งหรือทั้งสองอย่าง:

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: วันนี้เป็น "ฟิลด์" พรุ่งนี้จะเป็นอย่างไร การออกแบบนี้จะช่วยให้Attackและจะกลายเป็นความซับซ้อนมากขึ้นโดยไม่ต้องเปลี่ยนอินเตอร์เฟซของDefense Tool
Nicol Bolas

1
คุณยังคงไม่สามารถขยายเครื่องมือได้เป็นอย่างดีด้วยสิ่งนี้แน่นอนว่าการโจมตีและการป้องกันอาจมีความซับซ้อนมากขึ้น แต่นั่นก็เป็นเช่นนั้น หากคุณใช้การจัดวางองค์ประกอบอย่างเต็มประสิทธิภาพคุณสามารถToolปิดการปรับแต่งให้เรียบร้อยในขณะที่ยังคงเปิดให้ขยายได้
Polygnome

@Polygnome: ถ้าคุณต้องการที่จะผ่านปัญหาในการสร้างระบบส่วนประกอบโดยพลการสำหรับกรณีเล็ก ๆ น้อย ๆ เช่นนี้นั่นก็ขึ้นอยู่กับคุณแล้ว ผมเองเห็นเหตุผลว่าทำไมฉันจะต้องการที่จะขยายไม่Toolได้โดยไม่ต้องปรับเปลี่ยนมัน และถ้าฉันมีสิทธิ์ที่จะแก้ไขมันฉันก็ไม่เห็นความต้องการส่วนประกอบโดยพลการ
Nicol Bolas

ตราบใดที่ Tool อยู่ภายใต้การควบคุมของคุณเองคุณสามารถแก้ไขได้ แต่หลักการ "ปิดเพื่อแก้ไขเปิดสำหรับส่วนขยาย" มีอยู่ด้วยเหตุผลที่ดี (ยาวเกินกว่าที่จะอธิบายอย่างละเอียดที่นี่) ฉันไม่คิดว่ามันเป็นเรื่องเล็กน้อย หากคุณใช้เวลาที่เหมาะสมการวางแผนระบบที่มีความยืดหยุ่นสำหรับ RPG ที่คุณจะได้รับอย่างมากผลตอบแทนในระยะยาว ฉันไม่เห็นประโยชน์ที่เพิ่มขึ้นในการแต่งเพลงประเภทนี้มากกว่าแค่ใช้ทุ่งโล่ง ความสามารถในการโจมตีและป้องกันเพิ่มเติมนั้นน่าจะเป็นสถานการณ์เชิงทฤษฎี แต่อย่างที่ฉันเขียนมันขึ้นอยู่กับข้อกำหนดที่แน่นอนของ OP
Polygnome

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

ถ้าใครใช้ polymorphism มันจะดีที่สุดถ้ารหัสทั้งหมดที่เกี่ยวกับคลาสที่ใช้อยู่ภายในคลาสนั้น นี่คือวิธีที่ฉันจะรหัสมัน:

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 () ที่จะลบโบนัสออกจากโปรแกรมเล่น
Polygnome

0

ฉันคิดว่าวิธีหนึ่งที่จะรับรู้ข้อบกพร่องในวิธีนี้คือการพัฒนาความคิดของคุณไปสู่ข้อสรุปเชิงตรรกะ

ลักษณะเช่นเดียวกับเกมนี้ดังนั้นในขั้นตอนบางอย่างที่คุณอาจจะเริ่มกังวลเกี่ยวกับประสิทธิภาพการทำงานและสลับการเปรียบเทียบสตริงผู้หาหรือint enumในฐานะที่เป็นรายชื่อของรายการที่ได้รับอีกต่อไปที่if-elseจะเริ่มต้นที่จะได้รับเทอะทะสวยดังนั้นคุณอาจพิจารณา refactoring switch-caseมันกลายเป็น ในตอนนี้คุณยังมีกำแพงข้อความอยู่พอสมควรดังนั้นคุณสามารถแยกการกระทำในแต่ละส่วนcaseออกเป็นฟังก์ชันแยกต่างหาก

เมื่อคุณมาถึงจุดนี้โครงสร้างของรหัสของคุณจะเริ่มคุ้นเคยซึ่งเริ่มต้นที่จะมีลักษณะเหมือนโฮมเบกต์ vtable ที่เขียนด้วยมือ * - โครงสร้างพื้นฐานที่ใช้วิธีเสมือนจริง ยกเว้นนี่เป็น vtable ที่คุณต้องอัพเดตและบำรุงรักษาด้วยตนเองทุกครั้งที่คุณเพิ่มหรือแก้ไขประเภทรายการ

ด้วยการผสานกับฟังก์ชั่นเสมือน "ของจริง" คุณจะสามารถใช้งานพฤติกรรมของแต่ละรายการในรายการได้ คุณสามารถเพิ่มรายการเพิ่มเติมในแบบที่มีอยู่ในตัวเองและสอดคล้องกันมากขึ้น และในขณะที่คุณทำสิ่งนี้มันเป็นคอมไพเลอร์ที่จะดูแลการใช้งานการจัดส่งแบบไดนามิกของคุณมากกว่าที่คุณ

ในการแก้ปัญหาเฉพาะของคุณ: คุณกำลังดิ้นรนที่จะเขียนคู่ของฟังก์ชั่นเสมือนจริงสำหรับการปรับปรุงการโจมตีและการป้องกันเพราะบางรายการมีผลต่อการโจมตีและบางรายการมีผลต่อการป้องกัน เคล็ดลับในกรณีง่าย ๆ เช่นนี้เพื่อใช้งานพฤติกรรมทั้งสองอย่างต่อไป แต่ไม่มีผลในบางกรณี GetDefenseBonus()อาจกลับมา0หรือApplyDefenseBonus(int& defence)อาจจะdefenceไม่เปลี่ยนแปลง วิธีที่คุณใช้เกี่ยวกับมันจะขึ้นอยู่กับว่าคุณต้องการจัดการกับการกระทำอื่น ๆ ที่มีผลกระทบอย่างไร ในกรณีที่ซับซ้อนมากขึ้นซึ่งมีพฤติกรรมที่หลากหลายมากขึ้นคุณสามารถรวมกิจกรรมเป็นวิธีเดียว

* (แม้ว่าจะถูกย้ายที่เกี่ยวข้องกับการใช้งานทั่วไป)


0

การมีกลุ่มของรหัสที่รู้เกี่ยวกับ "เครื่องมือ" ที่เป็นไปได้ทั้งหมดนั้นไม่ใช่การออกแบบที่ยอดเยี่ยม (โดยเฉพาะเมื่อคุณต้องปิดท้ายด้วยรหัสดังกล่าวจำนวนมาก ) แต่ไม่ได้มีพื้นฐานToolกับ stubs สำหรับคุณสมบัติของเครื่องมือที่เป็นไปได้ทั้งหมด: ตอนนี้Toolคลาสต้องรู้เกี่ยวกับการใช้ที่เป็นไปได้ทั้งหมด

สิ่งที่แต่ละเครื่องมือรู้คือสิ่งที่มันสามารถมีส่วนร่วมกับตัวละครที่ใช้มัน giveto(*Character owner)ดังนั้นให้วิธีการหนึ่งสำหรับเครื่องมือทั้งหมด มันจะปรับสถิติของผู้เล่นตามความเหมาะสมโดยไม่ทราบว่าเครื่องมืออื่นสามารถทำอะไรได้บ้างและสิ่งที่ดีที่สุดคือไม่จำเป็นต้องรู้คุณสมบัติของตัวละครที่ไม่เกี่ยวข้องด้วย ยกตัวอย่างเช่นโล่ไม่จำเป็นแม้จะรู้เกี่ยวกับคุณลักษณะattack, invisibility, healthฯลฯ ทั้งหมดที่จำเป็นในการใช้เครื่องมือที่เป็นตัวละครที่จะสนับสนุนแอตทริบิวต์ที่วัตถุต้อง หากคุณพยายามให้ดาบแก่ลาและลาไม่มีattackสถิติคุณจะได้รับข้อผิดพลาด

เครื่องมือควรมีremove()วิธีการซึ่งกลับผลกระทบต่อเจ้าของ นี่เป็นเรื่องยุ่งยากเล็กน้อย (เป็นไปได้ที่จะจบลงด้วยเครื่องมือที่ปล่อยเอฟเฟ็กต์ที่ไม่เป็นศูนย์เมื่อได้รับและถูกนำออกไปแล้ว) แต่อย่างน้อยก็มีการแปลเป็นภาษาท้องถิ่นสำหรับแต่ละเครื่องมือ


-4

ไม่มีคำตอบที่บอกว่ามันไม่ได้มีกลิ่นเลยฉันจะเป็นคนที่ย่อมาจากความคิดเห็นนั้น รหัสนี้ดีมาก! ความคิดเห็นของฉันขึ้นอยู่กับความจริงที่ว่าบางครั้งมันง่ายกว่าที่จะเดินหน้าต่อไปและอนุญาตให้ทักษะของคุณค่อยๆเพิ่มขึ้นเมื่อคุณสร้างสิ่งใหม่เพิ่มเติม คุณอาจติดขัดอยู่หลายวันในการสร้างสถาปัตยกรรมที่สมบูรณ์แบบ แต่อาจจะไม่มีใครเคยเห็นมันมาก่อนเพราะคุณไม่เคยทำโครงการเสร็จ ไชโย!


4
การพัฒนาทักษะพร้อมประสบการณ์ส่วนตัวเป็นสิ่งที่ดีอย่างแน่นอน แต่การพัฒนาทักษะโดยการถามคนที่มีประสบการณ์ส่วนตัวอยู่แล้วดังนั้นคุณไม่จำเป็นต้องตกหลุมรักตัวเองอย่างชาญฉลาด แน่นอนว่าเป็นเหตุผลที่ผู้คนถามคำถามที่นี่ตั้งแต่แรกใช่มั้ย
เกรแฮม

ฉันไม่เห็นด้วย แต่ฉันเข้าใจว่าเว็บไซต์นี้เกี่ยวกับการไปลึก บางครั้งนั่นหมายถึงการมีความเชื่องช้ามากเกินไป นี่คือเหตุผลที่ฉันต้องการโพสต์ความคิดเห็นนี้เพราะมันยึดในความเป็นจริงและหากคุณกำลังมองหาเคล็ดลับที่จะกลายเป็นดีขึ้นและช่วยให้ผู้เริ่มต้นคุณพลาดทั้งบทเกี่ยวกับ "ดีพอ" ที่ค่อนข้างมีประโยชน์สำหรับผู้เริ่มต้นรู้
Ostmeistro
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.