如所写,它“闻起来”,但这可能仅仅是您所举的例子。将数据存储在通用对象容器中,然后进行强制转换以获取对数据的访问不会自动编码气味。您会看到它在许多情况下都使用过。但是,在使用它时,您应该知道自己在做什么,如何做以及为什么。当我看示例时,使用基于字符串的比较来告诉我什么对象是使我的个人气味计绊倒的东西。这表明您不确定自己在这里做什么(这很好,因为您有足够的智慧来向程序员咨询。SE并说:“嘿,我不认为我喜欢我在做什么,请帮助我出去!”)。
像这样从通用容器中投射数据的模式的根本问题在于,数据的生产者和数据的消费者必须一起工作,但是乍一看,这样做似乎并不明显。 在这种模式的每个示例中,无论有臭还是无臭,这都是根本问题。这是非常有可能在未来开发者完全不知道自己在做这种模式并造成破坏,所以如果你使用这个模式,你必须小心,以帮助在未来开发出来。由于他可能不知道存在的某些细节,您必须使他更容易避免无意间破坏代码。
例如,如果我想复制播放器怎么办?如果我只看播放器对象的内容,它看起来很简单。我只是复制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
不接受a sockaddr_in*
,所以接受更通用的sockaddr*
。如果您查看这些类的定义,我们将看到sockaddr
我们分配给sin_family
* 的族只有一个成员。家人说您应该将哪个子类型转换sockaddr
为。 AF_INET
告诉您地址结构实际上是一个sockaddr_in
。如果为AF_INET6
,则地址将为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
涉及枚举的语句,该语句具有该模式最重要的部分: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个工具,那么您可能会懒于内存。当您需要制作5亿个工具时,您将开始关心比特和字节。联合结构总是比需要的大。
UNIX套接字不使用该解决方案,这是因为API的开放性加剧了差异性。UNIX套接字的目的是创建各种UNIX风格都可以使用的东西。每种口味都可以定义他们支持的家庭的列表,例如AF_INET
,并且每个列表都有一个简短列表。但是,如果出现了新的协议(例如这样AF_INET6
做),则可能需要添加新字段。如果使用并集结构执行此操作,最终将有效地创建具有相同名称的结构新版本,从而产生无休止的不兼容问题。这就是UNIX套接字选择使用强制转换模式而不是并集结构的原因。我敢肯定,他们考虑过了,而他们想到的事实就是为什么使用时它不会闻起来的一部分。
您也可以将并集用于真实。工会仅与最大的会员一样大,从而节省了内存,但是工会也有自己的问题。这可能不是代码的选项,但它始终是您应该考虑的选项。
另一个有趣的解决方案是boost::variant
。 Boost是一个很棒的库,里面充满了可重用的跨平台解决方案。它可能是有史以来最好的C ++代码。 Boost.Variant基本上是联合的C ++版本。它是一个容器,可以包含许多不同的类型,但一次只能包含一个。您可以将您的Sword
,Shield
和MagicCloth
类设为make,然后将tool设为boost::variant<Sword, Shield, MagicCloth>
,这意味着它包含这三种类型之一。将来仍然存在相同的问题,即将来的兼容性会阻止UNIX套接字使用它(更不用说UNIX套接字是C,早于boost
相当多!),但是这种模式非常有用。变体经常用在例如解析树中,这些树接受一串文本并使用语法规则将其分解。
我建议在尝试并使用通用对象转换方法之前要考虑的最终解决方案是Visitor设计模式。访客是一种强大的设计模式,它充分利用了以下观点:调用虚拟函数可以有效地完成您所需的转换,并且可以为您完成。因为编译器会这样做,所以它永远不会出错。因此,Visitor使用抽象的基类,而不是存储枚举,该基类具有一个vtable,该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
语句来完成工作,要保证这一点要困难得多。这些优势足够好,以至于Visitor在3D图形场景生成器中找到了一个不错的小众市场。他们恰好需要访客提供的行为,因此效果很好!
总而言之,请记住,这些模式使下一个开发人员难以承受。花时间让他们更轻松,代码也不会闻起来!
*从技术上讲,如果您查看规格,sockaddr有一个名为的成员sa_family
。在C级别上有一些棘手的事情对我们来说无关紧要。欢迎您来看看实际实现,但是对于这个答案,我将使用它sa_family
sin_family
和其他答案完全互换,使用散文中最直观的一种,并相信C技巧可以处理不重要的细节。