जैसा कि लिखा गया है, यह "बदबू आ रही है", लेकिन यह सिर्फ आपके द्वारा दिए गए उदाहरण हो सकते हैं। सामान्य वस्तु कंटेनरों में डेटा संग्रहीत है, तो कास्टिंग यह डेटा तक पहुँच प्राप्त करने के लिए है नहीं स्वचालित रूप से कोड गंध। आप इसे कई स्थितियों में उपयोग करते देखेंगे। हालाँकि, जब आप इसका उपयोग करते हैं, तो आपको पता होना चाहिए कि आप क्या कर रहे हैं, आप इसे कैसे कर रहे हैं और क्यों कर रहे हैं। जब मैं उदाहरण को देखता हूं, तो स्ट्रिंग आधारित तुलना का उपयोग मुझे यह बताने के लिए करता है कि वस्तु क्या है जो मेरी व्यक्तिगत गंध मीटर की यात्रा करती है। यह सुझाव देता है कि आप पूरी तरह से निश्चित नहीं हैं कि आप यहाँ क्या कर रहे हैं (जो ठीक है, क्योंकि आपको प्रोग्रामर के यहाँ आने की बुद्धि थी। वह कहे और "अरे, मुझे नहीं लगता कि मुझे पसंद है कि मैं क्या कर रहा हूँ, मदद करो" मै बाहर!")।
जेनेरिक कंटेनरों से डेटा कास्टिंग के पैटर्न के साथ मूल मुद्दा यह है कि डेटा के निर्माता और डेटा के उपभोक्ता को एक साथ काम करना होगा, लेकिन यह स्पष्ट नहीं हो सकता है कि वे पहली नज़र में ऐसा करते हैं। इस पैटर्न के प्रत्येक उदाहरण में, बदबूदार या बदबूदार नहीं है, यह मौलिक मुद्दा है। यह है बहुत , संभव अगले डेवलपर पूरी तरह से अनजान होने के लिए है कि आप इस पैटर्न कर रहे हैं और दुर्घटना से इसे तोड़ने के लिए, इसलिए यदि आप इस पद्धति का उपयोग आप अगले डेवलपर मदद करने के लिए ध्यान रखना चाहिए। आपको उसके लिए यह आसान बनाना होगा कि वह अनजाने में कोड को न तोड़ें क्योंकि कुछ विस्तार से वह मौजूद नहीं हो सकता है।
उदाहरण के लिए, क्या होगा अगर मैं एक खिलाड़ी की नकल करना चाहता हूं? अगर मैं सिर्फ खिलाड़ी की सामग्री को देखता हूं, तो यह बहुत आसान लगता है। मैं सिर्फ नकल करने के लिए है 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
आपको बताता है कि पता संरचना वास्तव में एक है 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
और अन्य पूरी तरह से एक दूसरे का उपयोग कर रहे हैं, जो भी गद्य के लिए सबसे अधिक सहज है, यह विश्वास करते हुए कि सी चालबाज महत्वहीन विवरणों का ध्यान रखता है।