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