क्या किसी कंटेनर में जेनेरिक ऑब्जेक्ट को स्टोर करने के लिए एक कोड गंध है और फिर ऑब्जेक्ट प्राप्त करें और कंटेनर से ऑब्जेक्ट को डाउन करें?


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
एक विशिष्ट उपवर्ग (और बाद के डाउनकास्ट) के लिए परीक्षण को हटाने के लिए एक मानक ओओपी तकनीक बेस श्रृंखला में एक या, इस मामले में शायद दो, आभासी विधि (ओं) को बनाने के लिए है अगर इसके बजाय चेन और कास्ट का उपयोग करें। इसका उपयोग इफ की पूरी तरह से हटाने और उपवर्गों को लागू करने के लिए संचालन को सौंपने के लिए किया जा सकता है। यदि आप हर बार एक नया उपवर्ग जोड़ते हैं तो आपको हर बार स्टेटमेंट को संपादित नहीं करना पड़ेगा।
एरिक इद्दत

2
डबल डिस्पैच पर भी विचार करें।
बोरिस द स्पाइडर

अपने टूल क्लास में एक गुण क्यों न जोड़ें, जो विशेषता प्रकारों (यानी हमला, रक्षा) का एक शब्दकोश और इसके लिए निर्दिष्ट मान रखता है। हमले, रक्षा मूल्यों की गणना की जा सकती है। तब आप केवल एन्यूमरेटेड कंटेंट द्वारा टूल से वैल्यू को कॉल कर सकते हैं।
user1740075

8
मैं इसे यहीं छोड़ दूंगा
You

1
विज़िटर पैटर्न भी देखें।
जुल्लुगोज़

जवाबों:


63

हां, यह एक कोड गंध (बहुत सारे मामलों में) है।

मुझे लगता है कि उपकरण में वर्चुअल विधियों के साथ-और यदि इसे बदलना मुश्किल है

आपके उदाहरण में, वर्चुअल विधियों द्वारा 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 पर
क्रॉम्स्टर का कहना है कि मोनिका

7
आपको Swordअपने कोडबेस में इस तरह की अवधारणा की आवश्यकता भी नहीं होगी । आप बस new Tool("sword", swordAttack, swordDefense)एक JSON फ़ाइल उदा से ले सकते हैं ।
अमेजिंगड्र्स

7
@AmazingDreams: यह सही है (कोड के उन हिस्सों के लिए जो हम यहां देख रहे हैं), लेकिन मुझे लगता है कि ओपी ने अपने प्रश्न के लिए अपने वास्तविक कोड को सरल बनाया है जिस पहलू पर वह चर्चा करना चाहता था।
डॉक ब्राउन

3
यह इतना बेहतर नहीं है तो मूल कोड (ठीक है, यह एक सा है)। कोई भी उपकरण जिसमें अतिरिक्त गुण हैं, अतिरिक्त तरीकों को जोड़े बिना नहीं बनाया जा सकता है। मुझे लगता है कि इस मामले में एक को विरासत पर रचना का पक्ष लेना चाहिए। हाँ, वर्तमान में केवल हमला और बचाव है, लेकिन उस तरह से रहने की आवश्यकता नहीं है।
पॉलिग्नोम

1
@DocBrown हां यह सच है, हालांकि यह एक आरपीजी की तरह दिखता है जहां एक चरित्र में कुछ आँकड़े होते हैं जो उपकरण या बल्कि सुसज्जित वस्तुओं द्वारा संशोधित होते हैं। मैं Toolसभी संभावित संशोधक के साथ एक सामान्य बनाऊंगा, कुछ vector<Tool*>को डेटा फ़ाइल से पढ़े गए सामान के साथ भर दूंगा, फिर बस उन पर लूप करूंगा और जैसे आप अभी करते हैं, उन आंकड़ों को संशोधित करें। आप मुसीबत में पड़ना चाहते हैं जब आप एक आइटम देना चाहते हैं, उदाहरण के लिए 10% बोनस। शायद एक tool->modify(playerStats)और विकल्प है।
अमेजिंगडर्म्स

23

आपके कोड के साथ प्रमुख समस्या यह है कि जब भी आप किसी भी नए आइटम को पेश करते हैं, तो आपको न केवल आइटम के कोड को लिखना और अपडेट करना होगा, आपको अपने खिलाड़ी (या जहां भी आइटम का उपयोग किया जाता है) को संशोधित करना होगा, जो पूरी बात को एक बनाता है बहुत अधिक जटिल।

अंगूठे के एक सामान्य नियम के रूप में, मुझे लगता है कि यह हमेशा थोड़े गड़बड़ है, जब आप सामान्य उपवर्ग / विरासत पर भरोसा नहीं कर सकते हैं और खुद को ऊपर उठाना है।

मैं दो संभव दृष्टिकोणों के बारे में सोच सकता था जिससे पूरी बात और अधिक लचीली हो जाएगी:

  • जैसा कि दूसरों ने उल्लेख किया है, 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;
    }
}

यह बहुत आसान क्रमांकन / औजारों के deserialization (जैसे बचत या नेटवर्किंग के लिए) की अनुमति देता है, और इसके लिए आभासी प्रेषण की आवश्यकता नहीं होती है। यदि आपका कोड आप सभी को दिखाया गया है, और आप यह उम्मीद नहीं करते हैं कि यह बहुत अधिक विकसित हो सकता है, तो अलग-अलग नामों और उन आँकड़ों के साथ अधिक अलग-अलग उपकरण हैं, केवल अलग-अलग मात्रा में, तो यह जाने का तरीका है।

@DocBrown ने एक समाधान पेश किया है जो अभी भी आभासी प्रेषण पर निर्भर करता है, और यह एक फायदा हो सकता है यदि आप किसी तरह अपने कोड के उन हिस्सों के लिए उपकरण बनाते हैं जो दिखाए नहीं गए थे। हालाँकि, अगर आपको वास्तव में आवश्यकता है या अन्य व्यवहार को भी बदलना चाहते हैं, तो मैं निम्नलिखित समाधान सुझाऊंगा:

वंशानुक्रम पर रचना

क्या होगा यदि आप बाद में एक उपकरण चाहते हैं जो चपलता को संशोधित करता है ? या गति को चलाने ? मेरे लिए, ऐसा लगता है कि आप एक आरपीजी बना रहे हैं। आरपीजी के लिए एक चीज जो महत्वपूर्ण है, वह है विस्तार के लिए खुला होना । अब तक दिखाए गए समाधानों की पेशकश नहीं है। आपको Toolहर बार एक नई विशेषता के लिए कक्षा में परिवर्तन करना होगा और इसमें नए वर्चुअल तरीके जोड़ने होंगे।

दूसरा समाधान जो मैं दिखा रहा हूं वह है जिसे मैंने पहले एक टिप्पणी में संकेत दिया था - यह विरासत के बजाय रचना का उपयोग करता है और "संशोधन के लिए बंद", विस्तार * सिद्धांत के लिए खुला है। यदि आप इस बात से परिचित हैं कि इकाई सिस्टम कैसे काम करता है, तो कुछ चीजें। परिचित लगेगा (मैं ईएस के छोटे भाई के रूप में रचना के बारे में सोचना पसंद करता हूं)।

ध्यान दें कि जो मैं नीचे दिखा रहा हूं वह उन भाषाओं में काफी अधिक सुरुचिपूर्ण है जिनके पास रनटाइम प्रकार की जानकारी है, जैसे जावा या सी #। इसलिए, जो 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;
    }
}

ध्यान दें कि हमने टूल क्लास को संशोधित किए बिना अपने टूल्स में कुछ व्यवहार जोड़ा है।

आप स्ट्रिंग्स को मैक्रो या स्टैटिक कॉन्स्ट वैरिएबल में ले जा सकते हैं, इसलिए आपको इसे बार-बार टाइप करने की जरूरत नहीं है।

यदि आप इस दृष्टिकोण को और आगे ले जाते हैं - उदाहरण के लिए ऐसे घटक बनाएं जो खिलाड़ी में जोड़े जा सकते हैं, और एक ऐसा Combatघटक बनाते हैं जो खिलाड़ी को मुकाबले में भाग लेने में सक्षम बनाता है , तो आप attackविधि से भी छुटकारा पा सकते हैं , और उसे संभाला जा सकता है। घटक द्वारा या कहीं और संसाधित किया जाना चाहिए।

खिलाड़ी को अवयवों को प्राप्त करने में सक्षम बनाने का लाभ यह होगा कि तब, आपको उसे अलग व्यवहार देने के लिए खिलाड़ी को बदलने की भी आवश्यकता नहीं होगी। मेरे उदाहरण में, आप एक Movableघटक बना सकते हैं , इस तरह से आपको walkToउसे स्थानांतरित करने के लिए खिलाड़ी पर विधि को लागू करने की आवश्यकता नहीं है । आप बस घटक बनाएंगे, इसे खिलाड़ी से जोड़ेंगे और किसी और को इसे संसाधित करने देंगे।

आप इस उदाहरण में एक पूरा उदाहरण पा सकते हैं: https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20

यह समाधान स्पष्ट रूप से थोड़ा अधिक जटिल है, फिर जो अन्य पोस्ट किए गए हैं। लेकिन आप कितना लचीला होना चाहते हैं, इस पर निर्भर करता है कि आप इसे कितनी दूर तक ले जाना चाहते हैं, यह एक बहुत शक्तिशाली दृष्टिकोण हो सकता है।

संपादित करें

कुछ अन्य उत्तर सीधे वंशानुक्रम का प्रस्ताव देते हैं (तलवारें उपकरण का विस्तार करती हैं, जिससे शील्ड का विस्तार उपकरण होता है)। मुझे नहीं लगता कि यह एक ऐसा परिदृश्य है जहाँ वंशानुक्रम बहुत अच्छी तरह से काम करता है। क्या होगा अगर आप तय करते हैं कि एक निश्चित तरीके से ढाल के साथ अवरुद्ध करना हमलावर को भी नुकसान पहुंचा सकता है? मेरे समाधान के साथ, आप बस एक ढाल में एक हमला घटक जोड़ सकते हैं और महसूस कर सकते हैं कि आपके कोड में कोई बदलाव किए बिना। विरासत के साथ, आपको एक समस्या होगी। आरपीजी में आइटम / उपकरण शुरू से ही इकाई प्रणालियों का उपयोग करते हुए सीधे रचना के लिए अभ्यर्थी या सीधे उम्मीदवार हैं।


1

सामान्यतया, यदि आपको कभी ifकिसी ओओपी भाषा में (उदाहरण के प्रकार की आवश्यकता के साथ संयोजन में) का उपयोग करने की आवश्यकता होती है, तो यह एक संकेत है, कि कुछ बदबूदार चल रहा है। कम से कम, आपको अपने मॉडलों पर करीब से नज़र डालनी चाहिए।

मैं आपके डोमेन को अलग तरीके से मॉडल करूँगा।

अपने USECASE के लिए एक Toolएक है AttackBonusऔर एक DefenseBonusहै जो दोनों हो सकता है - 0मामला यह पंख या ऐसा ही कुछ तरह से लड़ने के लिए बेकार है में।

एक हमले के लिए, आपके पास उपयोग किए गए हथियार से आपका baserate+ bonusहै। वही रक्षा baserate+ के लिए जाता है bonus

परिणाम में आपके Toolपास virtualआक्रमण / रक्षा बोनी की गणना के लिए एक विधि होनी चाहिए ।

tl; डॉ

एक बेहतर डिजाइन के साथ, आप हैकी ifएस से बच सकते हैं ।


कभी-कभी यदि आवश्यक हो, उदाहरण के लिए जब अदिश मानों की तुलना की जाती है। ऑब्जेक्ट प्रकार स्विचिंग के लिए, इतना नहीं।
एंडी

हाहा, अगर एक बहुत ही आवश्यक ऑपरेटर है और आप सिर्फ यह नहीं कह सकते कि कोड गंध का उपयोग करना है।
tymtam

1
@ तुत्सी कुछ सम्मान के लिए आप सही हैं। मैंने खुद को और अधिक स्पष्ट किया। मैं ifकम प्रोग्रामिंग का बचाव नहीं कर रहा हूं । ज्यादातर कॉम्बिनेशन में अगर instanceofऐसा है या ऐसा कुछ है। लेकिन एक स्थिति है, जो दावा करती है कि ifएक कोडमेल है और इसके चारों ओर पाने के तरीके हैं। और आप सही हैं, यह एक आवश्यक ऑपरेटर है जिसका अपना अधिकार है।
थॉमस जंक

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* को सौंपा है । परिवार कहता है कि आपको कौन सा उपप्रकार करना चाहिए sockaddrAF_INETआपको बताता है कि पता संरचना वास्तव में एक है sockaddr_in। यदि यह AF_INET6होता है, तो पता एक होगा sockaddr_in6, जिसमें बड़े आईपीवी 6 पते का समर्थन करने के लिए बड़े क्षेत्र हैं।

यह आपके 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ऐसा मामला जो फेंकता है। अगर आप चीजों को पूरी तरह से करते हैं तो आपको उस स्थिति में कभी नहीं आना चाहिए । हालाँकि, अगर कोई नया टूल टाइप करता है, और आप इसे सपोर्ट करने के लिए अपने कोड को अपडेट करना भूल जाते हैं, तो आप त्रुटि पकड़ना चाहते हैं। वास्तव में, मैं उन्हें बहुत सलाह देता हूं कि आपको उनकी आवश्यकता होने पर भी उन्हें जोड़ना चाहिए।

इसका दूसरा बहुत बड़ा फायदा 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
    }
}

हां, मैं एक "खाली" डिफ़ॉल्ट स्टेटमेंट में रखता हूं, बस अगले डेवलपर को यह स्पष्ट करने के लिए कि मैं क्या होने की उम्मीद करता हूं अगर कुछ नया अप्रत्याशित प्रकार मेरे रास्ते में आता है।

यदि आप ऐसा करते हैं, तो पैटर्न कम गंध होगा। हालाँकि, अंतिम चीज़ को सूँघने-मुक्त करने के लिए आपको अन्य विकल्पों पर विचार करना होगा। ये कास्ट कुछ अधिक शक्तिशाली और खतरनाक उपकरण हैं जो आपके पास सी ++ प्रदर्शनों की सूची में हैं। जब तक आपके पास कोई अच्छा कारण न हो, आपको उनका उपयोग नहीं करना चाहिए।

एक बहुत लोकप्रिय विकल्प है जिसे मैं "संघ संरचना" या "संघ वर्ग" कहता हूं। आपके उदाहरण के लिए, यह वास्तव में एक बहुत अच्छा होगा। इनमें से एक बनाने के लिए, आप एक Toolकक्षा बनाते हैं , पहले की तरह एक संलयन के साथ, लेकिन उपवर्ग के बजाय Tool, हम बस उस पर प्रत्येक उपप्रकार से सभी फ़ील्ड्स डालते हैं।

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

    int   attack;
    int   defense;
};

अब आपको उपवर्गों की आवश्यकता नहीं है। आपको केवल यह देखने के लिए typeफ़ील्ड देखना होगा कि कौन से अन्य फ़ील्ड वास्तव में मान्य हैं। यह बहुत सुरक्षित और समझने में आसान है। हालांकि, इसमें कमियां हैं। कई बार आप इसका उपयोग नहीं करना चाहते हैं:

  • जब ऑब्जेक्ट बहुत अधिक भिन्न होते हैं - आप फ़ील्ड्स की लॉन्ड्री सूची के साथ समाप्त हो सकते हैं, और यह स्पष्ट नहीं हो सकता है कि कौन से प्रत्येक ऑब्जेक्ट प्रकार पर लागू होते हैं।
  • मेमोरी-क्रिटिकल स्थिति में संचालन करते समय - यदि आपको 10 टूल बनाने की आवश्यकता है, तो आप मेमोरी के साथ आलसी हो सकते हैं। जब आपको 500 मिलियन उपकरण बनाने की आवश्यकता होती है, तो आप बिट्स और बाइट्स के बारे में देखभाल शुरू करने जा रहे हैं। संघ की संरचनाएं हमेशा से बड़ी होती हैं, जिनकी उन्हें आवश्यकता होती है।

यह समाधान UNIX सॉकेट्स द्वारा उपयोग नहीं किया जाता है, क्योंकि एपीआई की खुली समाप्ति से जटिल असमानता मुद्दा है। UNIX सॉकेट्स के साथ इरादा कुछ ऐसा बनाना था जो UNIX के हर स्वाद के साथ काम कर सके। प्रत्येक स्वाद उन परिवारों की सूची को परिभाषित कर सकता है, जिन्हें वे पसंद करते हैं, जैसे AF_INET, और प्रत्येक के लिए एक छोटी सूची होगी। हालांकि, अगर एक नया प्रोटोकॉल साथ आता है, जैसेAF_INET6 किया, तो आपको नए फ़ील्ड जोड़ने होंगे। यदि आपने संघ संरचना के साथ ऐसा किया है, तो आप अंतत: असंगतता के मुद्दों को बनाते हुए प्रभावी रूप से एक ही नाम के साथ संरचना का एक नया संस्करण तैयार करेंगे। यही कारण है कि UNIX सॉकेट्स ने संघ संरचना के बजाय कास्टिंग पैटर्न का उपयोग करने के लिए चुना। मुझे यकीन है कि उन्होंने इस पर विचार किया था, और इस तथ्य के बारे में उन्होंने सोचा कि इसका एक हिस्सा यह क्यों गंध नहीं करता है जब वे इसका उपयोग करते हैं।

आप वास्तविक के लिए एक संघ का उपयोग भी कर सकते हैं। यूनियनों ने स्मृति को बचाया, केवल सबसे बड़े सदस्य के रूप में बड़ा होने के नाते, लेकिन वे मुद्दों के अपने सेट के साथ आते हैं। यह शायद आपके कोड के लिए एक विकल्प नहीं है, लेकिन इसका हमेशा एक विकल्प जिस पर आपको विचार करना चाहिए।

एक और दिलचस्प उपाय है boost::variantबूस्ट पुन: प्रयोज्य क्रॉस-प्लेटफॉर्म समाधानों से भरा एक महान पुस्तकालय है। इसका शायद कुछ सबसे अच्छा C ++ कोड कभी लिखा है। Boost.Variant मूल रूप से यूनियनों का C ++ संस्करण है। यह एक कंटेनर है जिसमें कई अलग-अलग प्रकार हो सकते हैं, लेकिन एक समय में केवल एक ही। आप अपने Sword, Shieldऔर MagicClothकक्षाओं को बना सकते हैं, फिर उपकरण को थोड़ा-थोड़ा करके बनाएं !), लेकिन यह पैटर्न अविश्वसनीय रूप से उपयोगी हो सकता है। वेरिएंट का उपयोग अक्सर किया जाता है, उदाहरण के लिए, पार्स पेड़ों में, जो पाठ की एक स्ट्रिंग लेते हैं और नियमों के लिए एक व्याकरण का उपयोग करके इसे तोड़ते हैं।boost::variant<Sword, Shield, MagicCloth> , जिसका अर्थ यह उन तीन प्रकारों में से एक होता है। यह अभी भी भविष्य की संगतता के साथ एक ही समस्या से ग्रस्त है जो UNIX सॉकेट्स को इसका उपयोग करने से रोकता है (UNIX सॉकेट्स का उल्लेख नहीं करने के लिए)boost

अंतिम समाधान जो मैं एक डुबकी लेने और जेनेरिक ऑब्जेक्ट कास्टिंग दृष्टिकोण का उपयोग करने से पहले देख रहा हूं, वह विज़िटर डिज़ाइन पैटर्न है। विज़िटर एक शक्तिशाली डिज़ाइन पैटर्न है जो अवलोकन का लाभ उठाता है कि वर्चुअल फ़ंक्शन को कॉल करना प्रभावी रूप से आपके लिए आवश्यक कास्टिंग करता है, और यह आपके लिए करता है। क्योंकि कंपाइलर ऐसा करता है, यह कभी गलत नहीं हो सकता। इस प्रकार, एक Enum को संचय करने के बजाय, विज़िटर एक अमूर्त आधार वर्ग का उपयोग करता है, जिसमें एक व्यवहार्य है जो जानता है कि ऑब्जेक्ट किस प्रकार का है। फिर हम एक साफ-सुथरी डबल-इनडायरेक्शन कॉल बनाते हैं जो काम करती है:

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। यहाँ सी स्तर पर कुछ मुश्किल काम किया जा रहा है जो हमारे लिए मायने नहीं रखता है। वास्तविक क्रियान्वयन को देखने के लिए आपका स्वागत है , लेकिन इस उत्तर के लिए मैं उपयोग करने जा रहा हूं sa_family sin_familyऔर अन्य पूरी तरह से एक दूसरे का उपयोग कर रहे हैं, जो भी गद्य के लिए सबसे अधिक सहज है, यह विश्वास करते हुए कि सी चालबाज महत्वहीन विवरणों का ध्यान रखता है।


लगातार हमला करने से खिलाड़ी आपके उदाहरण में असीम रूप से मजबूत हो जाएगा। और आप ToolVisitor के स्रोत को संशोधित किए बिना अपने दृष्टिकोण का विस्तार नहीं कर सकते। यह एक महान समाधान है, हालांकि।
पॉलिग्नोमेन

@Polygnome आप उदाहरण के बारे में सही हैं। मैंने सोचा था कि कोड अजीब लग रहा था, लेकिन पाठ के उन सभी पृष्ठों को स्क्रॉल करते हुए जो मैंने त्रुटि को याद किया। अब इसे ठीक कर रहे हैं। ToolVisitor के स्रोत को संशोधित करने की आवश्यकता के रूप में, यह विज़िटर पैटर्न का एक डिज़ाइन विशेषता है। यह एक आशीर्वाद है (जैसा मैंने लिखा) और एक अभिशाप (जैसा कि आपने लिखा)। उस मामले को संभालना जहां आप मनमाने ढंग से एक्स्टेंसिबल संस्करण चाहते हैं, यह कहीं अधिक कठिन है, और केवल उनके मूल्य के बजाय चर के अर्थ में खुदाई करना शुरू करता है , और अन्य पैटर्न को खोलता है, जैसे कि कमजोर टाइप किए गए चर और शब्दकोशों और JSON।
कॉर्ट अमोन -

1
हाँ, दुख की बात है कि हम वास्तव में सूचित निर्णय लेने के लिए ओपी प्राथमिकताओं और लक्ष्यों के बारे में पर्याप्त नहीं जानते हैं। और हाँ, एक पूरी तरह से लचीला समाधान को लागू करने के लिए कठिन है, मैं लगभग 3h के लिए मेरे जवाब पर काम किया, क्योंकि मेरी सी ++ बहुत जंग
खा रही है

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: यह आज "एक क्षेत्र" है। कल क्या होगा? यह डिज़ाइन अनुमति देता है Attackऔर Defenseइंटरफ़ेस को बदले बिना अधिक जटिल हो सकता है Tool
निकोल बोलस

1
आप अभी भी इसके साथ बहुत अच्छी तरह से टूल का विस्तार नहीं कर सकते हैं - यकीन है, हमले और रक्षा को अधिक जटिल हो सकता है, लेकिन यह है। यदि आप इसकी पूर्ण शक्ति के लिए रचना का उपयोग करते हैं, तो आप Toolसंशोधन के लिए पूर्ण बंद कर सकते हैं जबकि अभी भी इसे विस्तार के लिए खुला है।
पॉलिग्नोम

@ पॉलीग्नोम: यदि आप इस तरह के एक तुच्छ मामले के लिए पूरी मनमानी घटक प्रणाली बनाने की परेशानी से गुजरना चाहते हैं, तो यह आपके ऊपर है। मैं व्यक्तिगत रूप से कोई कारण नहीं देखता कि मैं इसे संशोधितTool किए बिना आगे बढ़ाना चाहता हूं । और अगर मुझे इसे संशोधित करने का अधिकार है, तो मुझे मनमाने घटकों की आवश्यकता नहीं है।
निकोल बोलस

जब तक टूल आपके नियंत्रण में है, आप इसे संशोधित कर सकते हैं। लेकिन "संशोधन के लिए बंद, विस्तार के लिए खुला" सिद्धांत अच्छे कारण के लिए मौजूद है (यहां विस्तार के लिए बहुत लंबा है)। मुझे नहीं लगता कि यह वह तुच्छ है, यद्यपि। यदि आप एक आरपीजी के लिए एक लचीली घटक प्रणाली की योजना बना उचित समय बिताते हैं, तो आप लंबे समय में जबरदस्त पुरस्कार प्राप्त करते हैं। मैं इस तरह की रचना में जोड़े गए लाभ को केवल सादे क्षेत्रों का उपयोग करके नहीं देखता । आगे हमले और रक्षा के लिए सक्षम होना एक बहुत ही सैद्धांतिक परिदृश्य लगता है। लेकिन जैसा कि मैंने लिखा है, यह ओपी की सटीक आवश्यकताओं पर निर्भर करता है।
पॉलिग्नोम

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

मुझे लगता है कि इस दृष्टिकोण की खामियों को पहचानने का एक तरीका यह है कि आप अपने विचार को उसके तार्किक निष्कर्ष तक पहुंचाएं।

यह एक खेल की तरह दिखता है, इसलिए कुछ स्तर पर आप शायद प्रदर्शन के बारे में चिंता करना शुरू कर देंगे intया उन स्ट्रिंग तुलनाओं को स्वैप करेंगे enum। जैसे-जैसे वस्तुओं की सूची लंबी if-elseहोती जाती है, वैसे-वैसे बहुत सुंदर दिखने लगती है, इसलिए आप इसे रिफैक्ट करने पर विचार कर सकते हैं switch-case। आपको इस बिंदु पर पाठ की एक दीवार भी मिल गई है ताकि आप प्रत्येक के भीतर कार्रवाई को शांत कर सकेंcase एक अलग फ़ंक्शन में ।

एक बार जब आप इस बिंदु पर पहुंच जाते हैं, तो आपके कोड की संरचना परिचित लगने लगती है - इसकी शुरुआत होमब्रेव, हैंड-रोलेड वाइबेट * जैसी दिखने लगती है - मूल संरचना जिस पर वर्चुअल तरीके आमतौर पर लागू होते हैं। को छोड़कर, यह एक व्यवहार्य है जिसे आपको मैन्युअल रूप से अद्यतन और बनाए रखना होगा, प्रत्येक बार जब आप किसी आइटम प्रकार को जोड़ते या संशोधित करते हैं।

"वास्तविक" वर्चुअल फ़ंक्शंस से चिपककर आप आइटम के भीतर प्रत्येक आइटम के व्यवहार को लागू करने में सक्षम हैं। आप अधिक स्व-निहित और सुसंगत तरीके से अतिरिक्त आइटम जोड़ने में सक्षम हैं। और जैसा कि आप यह सब करते हैं, यह संकलक है जो आपके बजाय आपके गतिशील प्रेषण के कार्यान्वयन का ख्याल रखेगा।

अपनी विशिष्ट समस्या को हल करने के लिए: आप हमले और रक्षा को अद्यतन करने के लिए आभासी कार्यों की एक सरल जोड़ी लिखने के लिए संघर्ष कर रहे हैं क्योंकि कुछ आइटम केवल हमले को प्रभावित करते हैं और कुछ आइटम केवल रक्षा को प्रभावित करते हैं। इस तरह एक सरल मामले में चाल दोनों व्यवहार को वैसे भी लागू करने के लिए, लेकिन कुछ मामलों में कोई प्रभाव नहीं। GetDefenseBonus()वापस आ सकता है 0या ApplyDefenseBonus(int& defence)बस defenceअपरिवर्तित छोड़ सकता है । आप इसके बारे में कैसे जाते हैं यह इस बात पर निर्भर करेगा कि आप उन अन्य कार्यों को कैसे संभालना चाहते हैं जिनका प्रभाव पड़ता है। अधिक जटिल मामलों में, जहां अधिक विविध व्यवहार होता है, आप बस गतिविधि को एक एकल विधि में जोड़ सकते हैं।

* (Albeit, ठेठ कार्यान्वयन के सापेक्ष स्थानांतरित)


0

कोड का एक ब्लॉक होना जो सभी संभावित "टूल्स" के बारे में जानता है, वह महान डिज़ाइन नहीं है (विशेषकर जब से आप अपने कोड में ऐसे कई ब्लॉक समाप्त करेंगे ); लेकिन न तो Toolसभी संभव उपकरण गुणों के लिए स्टब्स के साथ एक मूल है : अब Toolकक्षा को सभी संभावित उपयोगों के बारे में पता होना चाहिए।

क्या प्रत्येक उपकरण जानता है क्या यह चरित्र यह का उपयोग करता है करने के लिए योगदान कर सकते हैं। इसलिए सभी उपकरणों के लिए एक विधि प्रदान करें giveto(*Character owner)। यह खिलाड़ी के आँकड़ों को बिना उपयुक्त समझे समायोजित कर सकता है कि अन्य उपकरण क्या कर सकते हैं और सबसे अच्छा क्या है, इसके लिए चरित्र के अप्रासंगिक गुणों के बारे में जानने की आवश्यकता नहीं है। उदाहरण के लिए, एक ढाल भी विशेषताओं के बारे में पता करने की जरूरत नहीं है attack, invisibility, healthआदि सभी कि लागू करने के लिए चरित्र गुण का समर्थन करने के लिए उस वस्तु की आवश्यकता है एक उपकरण है आवश्यक है। यदि आप गधे को तलवार देने की कोशिश करते हैं, और गधे के पास कोई attackआँकड़े नहीं हैं , तो आपको एक त्रुटि मिलेगी।

उपकरण में एक remove()विधि भी होनी चाहिए , जो मालिक पर उनके प्रभाव को उलट देती है। यह थोड़ा मुश्किल है (यह उन उपकरणों के साथ समाप्त होने के लिए संभव है जो दिए जाने पर एक गैर-शून्य प्रभाव छोड़ते हैं और फिर ले जाते हैं), लेकिन कम से कम यह प्रत्येक उपकरण के लिए स्थानीयकृत है।


-4

वहाँ कोई जवाब नहीं है जो कहता है कि यह गंध नहीं करता है, इसलिए मैं वह होगा जो उस राय के लिए खड़ा हो; यह कोड पूरी तरह से ठीक है! मेरी राय इस तथ्य पर आधारित है कि कभी-कभी आगे बढ़ना आसान होता है और अपने कौशल को धीरे-धीरे बढ़ाने की अनुमति देता है क्योंकि आप अधिक नया सामान बनाते हैं। आप एक आदर्श वास्तुकला बनाने पर दिनों के लिए फंस सकते हैं, लेकिन शायद कोई भी इसे कभी भी कार्रवाई में नहीं देखेगा, क्योंकि आपने परियोजना को कभी पूरा नहीं किया। चीयर्स!


4
व्यक्तिगत अनुभव के साथ कौशल में सुधार अच्छा है, निश्चित है। लेकिन उन लोगों से पूछकर कौशल में सुधार करना, जिनके पास पहले से ही व्यक्तिगत अनुभव है, इसलिए आपको अपने आप को छेद में पड़ने की ज़रूरत नहीं है, होशियार है। निश्चित रूप से यही कारण है कि लोग यहाँ पहले सवाल पूछते हैं, है ना?
ग्राहम

मैं सहमत नहीं हूँ। लेकिन मैं समझता हूं कि यह साइट गहरी जाने वाली है। कभी-कभी इसका मतलब है कि अत्यधिक पांडित्यपूर्ण होना। यही कारण है कि मैं इस राय को पोस्ट करना चाहता था, क्योंकि यह वास्तविकता में लंगर डाले हुए है और यदि आप बेहतर बनने के लिए युक्तियों की खोज कर रहे हैं और एक शुरुआत के लिए मदद करते हैं तो आप इस पूरे अध्याय को "अच्छे से पर्याप्त" के बारे में याद करते हैं जो कि एक शुरुआत के लिए बहुत उपयोगी है।
ओस्टमेइस्त्रो
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.