避免了OO设计中的RTTI

avoiding RTTI in OO design

本文关键字:RTTI OO      更新时间:2023-10-16

我最近在某个论坛上看到了一个OO设计问题,并开始考虑使用RTTI。然而,这一定是一个糟糕的设计,但我想不出其他选择。这是一个简单的问题:

使用OO概念为以下场景创建一个C++程序-

我的狗名叫巴迪,住在后院。晚上,当他看到猫或松鼠来访时,他会吠叫。如果他看到一只青蛙,他饿了,他就吃它。如果他看到青蛙,他不饿,他就和它玩。如果他已经吃了两只青蛙,但仍然饿,他就会让它走。如果他看到一只郊狼,他就会大声呼救。有时他的朋友斯波特路过,他们互相追逐。如果他看到任何其他动物,他只是看着它。我希望你会有一个动物类,还有一个从动物类继承的猫、狗、松鼠、郊狼类。

我开始考虑在dog类中使用see()方法,该方法接受Animal参数,然后检查对象的实际类型(青蛙、猫等),并根据实际类型采取所需的动作-玩耍、追逐等。然而,这将需要RTTI,这一定是糟糕的设计。有人能提出一个更好的设计方案吗?这样既可以避免RTTI,又可以指出我想法中的错误?

根据您想要强调的内容,使用"OO概念"可以满足这个问题的方法多得离谱。

以下是我能想到的最简单的解决方案:

class Animal {
public:
    virtual void seenBy(Buddy&) = 0;
};
class Buddy {
public:
    void see(Cat&)      { /* ... */ }
    void see(Squirrel&) { /* ... */ }
    // ...
};
class Cat : public Animal {
public:
    virtual seenBy(Buddy& b) { b.see(*this); }
};
class Squirrel : public Animal {
public:
    virtual seenBy(Buddy& b) { b.see(*this); }
};
// classes for Frog, Coyote, Spot...

如果你需要多种"感知"动物,那么很容易为see制作一个虚拟包装器(产生双重调度的形式):

// On a parent class
virtual void see(Animal&) = 0;
// On Buddy
virtual void see(Animal& a) { a.seenBy(*this); }

以上要求Animal类对Buddy类有所了解。如果你不喜欢你的方法是被动动词,并且想将AnimalBuddy解耦,你可以使用访问者模式:

class Animal {
public:
    virtual void visit(Visitor&) = 0;
};
class Cat : public Animal {
public:
    virtual void visit(Visitor& v) { v.visit(*this); }
};
class Squirrel : public Animal {
public:
    virtual void visit(Visitor& v) { v.visit(*this); }
};
// classes for Frog, Coyote, Spot...
class Visitor {
public:
    virtual void visit(Cat&) = 0;
    virtual void visit(Squirrel&) = 0;
    // ...
};
class BuddyVision : public Visitor {
public:
    virtual void visit(Cat&)      { /* ... */ }
    virtual void visit(Squirrel&) { /* ... */ }
    // ...
};
class Buddy {
public:
    void see(Animal& a) {
        BuddyVision visitor;
        a.visit(visitor);
    }
};

第二种机制可以用于除巴迪看到动物之外的其他目的(可能用于动物看到巴迪)。然而,情况更为复杂。


注意OO绝对不是解决这个问题的唯一方法。对于这个问题,还存在其他更实用的解决方案,例如存储导致巴迪吠叫、进食、玩耍等的各种动物的属性。这还将Buddy类与Animal类解耦(即使是访问者模式也需要巴迪所能感知的所有内容的详尽列表)。

设计特别要求识别某些实体,以便对它们执行某些操作。因为对于某些操作与某些实体配合的原因(即:这都是任意的),没有任何韵律或理由,所以您所看到的要么是基于类型的调度,要么是基于属性的调度。我会选择后者。

给每个实体一些属性集。因此,狗会根据这些特性做出反应。猫和松鼠会有一个属性,"狗应该对我吠叫。"当狗遇到一个有这样属性的实体时,它会采取适当的行动。

在这种情况下,实体只不过是其属性以及基于遇到具有各种属性的其他实体的行为的总和。实体也可能有一些相关的状态。不会有特定的Dog或Cat类。只会有一个实体具有类似猫的属性和行为,还有一个实体拥有类似狗的属性和行动。

提示:使用虚拟函数(在目标动物上)而不是RTTI。

大多数时候,您可以通过消息传递来取代RTTI。

类型

Id id = object->send(WHO_ARE_YOU); 
switch(id)
{
  case ID_FROG: ...; break;
  case ID_CAT: ...; break;
}

原则上,消息传递比RTTI更灵活:

other_object->send(IS_SCARRY_OF, this); 

因为它允许设计目前未知的关系。假设明天你的狗会看到racoon,它在其他DLL中定义,但在Pascal中。