我在GUI设计中面临OOP问题,但让我用动物示例来说明它。让我们进行以下设置:
动物拥有牙齿是很自然的,但现在我需要像 has-a 关系的接口。例如,如果我有一个动物矢量,如果可以的话我怎么能做每个Bite()?
std::vector<Animal *> animals;
animals.push_back(new dog());
animals.push_back(new fly());
animals.push_back(new cat());
void Unleash_the_hounds(std::vector<Animal *> animals)
{
//bite if you can!
}
我想出了几个解决方案,但似乎没有一个完全合适:
1。)每个使用Teeth的类都实现了接口IBiting。然而,这个解决方案引入了大量的代码重复,我需要在每个类中“实现”Bite():
class Cat : public Animal, public IBiting {
Teeth teeth;
public:
virtual void Bite() { teeth.Bite(); }
}
2。)给每一只动物的牙齿,但只允许一些人使用它们。 注意:语法可能有误 - 它只是插图
class Animal{
static cosnt bool canBite = false;
Teeth teeth;
public:
void Bite() { this->canBite ? teeth.Bite() : return; }
}
class Cat {
static cosnt bool canBite = true;
}
3.)更多继承 - 创建类BitingAnimal并派生它。嗯,这可行,但如果我需要衍生(非)飞行动物,其中一些有牙齿。
class Animal{}
class BitingAnimal : public Animal {
Teeth teeth;
}
并用作BitingAnimal.teeth.Bite()
4。)多重继承。这通常是气馁的,而且在大多数语言中都是不可能的,而且Cat成为牙齿也是不合逻辑的。
class Cat : public Animal, public Teeth {
}
5。)可以咬人的类的枚举 - 只有它的声音很奇怪。
或者我只是过于复杂并错过了重要的事情?
答案 0 :(得分:2)
你没有提到的另一个选择是为牙齿提供抽象,但在基类中实现咬合。这减少了重复,因为派生类只需要指定如何访问牙齿而不是如何咬牙。通过返回指向牙齿的指针,我们可以允许空指针指示动物没有牙齿。这是一个例子:
#include <vector>
struct Teeth {
void bite() { }
};
struct Animal {
virtual Teeth *teethPtr() = 0;
void biteIfYouCan() { if (teethPtr()) teethPtr()->bite(); }
};
struct Dog : Animal {
Teeth teeth;
Teeth *teethPtr() override { return &teeth; }
};
struct Fish : Animal {
Teeth *teethPtr() override { return nullptr; }
};
int main()
{
Dog dog;
Fish fish;
std::vector<Animal *> animals {&dog,&fish};
for (auto animal_ptr : animals) {
animal_ptr->biteIfYouCan();
}
}
答案 1 :(得分:1)
以下是我对您的解决方案的评论:
canBite
标志是一种症状,可以做得更好。 BitingAnimal.teeth.Bite()
违反了Law of Demeter。此外,正如您所描述的 - 如果不是BittingAnimal
的动物想要咬什么呢?Cat
是 Teeth
。这不是完全正确。我建议采用以下方法:在mixin中创建IBiting
接口及其实现Biting
- 就像这样:
class Bitting : public IBiting {
Teeth teeth;
public:
virtual void Bite() { teeth.Bite(); }
}
然后,每个可以咬人的班级都会继承Biting
“mixin”:
class Cat : public Animal, public Biting {
}
当然,这将是一个多重继承,但是,由于Biting
只实现了“咬人”功能,所以它不会那么糟糕(没有Diamond Problem)。
答案 2 :(得分:1)
问题归结为如果你打电话给animal->bite()
会发生什么,而且那只动物没有牙齿而且咬不动。一个答案可能是所有动物都可以咬。对于猫来说它有效,因为它们有牙齿,而其他动物如蝴蝶可以咬,但没有任何反应。
class Animal{
public:
virtual void bite(){}
};
class Cat : public Animal{
Teeth teeth;
void bite() override{
teeth.bite();
}
};
class Butterfly : public Animal{
};
在这种方法中,您需要为每种动物类型编写额外的咬伤方式(如果可以的话)。如果您需要其他属性,例如scream()
和fly()
,则会变得有点棘手。是的,猫可以在这个模型中飞行,只是当他们这样做时什么也没发生,蝴蝶可以用零音量尖叫。
由于有很多动物都有牙齿,而且它们都咬一样,你可以添加一些专门的动物。
class BitingWithTeethAnimal : public Animal{
Teeth teeth;
void bite() override{
teeth.bite();
}
};
class Cat : public BitingWithTeethAnimal{
};
class Butterfly : public FlyingWithWingsAnimal{
};
理想情况下,你可以说class Pterodactyl : public BitingWithTeeth, FlyingWithWings, ScreamingWithVoice, Animal;
之类的内容而不会让Animal
膨胀到怪物类,但这在我的实现中是不可能的。好处是,你只需要实现你需要的东西,当动物咬牙齿时,你也不会感到困惑,因为实施是在一个功能中而不是在不同的类别中分开。
答案 3 :(得分:1)
1)界面很好,您可以通过以下方式添加默认实现:
class IBiting { public virtual void bite() = 0 };
class HasTeeth, public IBiting { Teeth teeth; public:
virtual void bite() override { teeth.bite(); } };
for(Animal* a: animals) {
IBiting* it = dynamic_cast<IBiting*>(a);
if(it) it->bite(); }
1b)...您也可以完全删除界面并仅使用HasTeeth
:
class HasTeeth { Teeth teeth; public:
void bite() { teeth.bite(); } };
for(Animal* a: animals) {
HasTeeth* it = dynamic_cast<HasTeeth*>(a);
if(it) it->bite(); }
2)如果您不想使用RTTI / Animal
,可以使用膨胀dynamic_cast
。您可以使virtual void bite()
空实施在Animal上并稍后覆盖它(一旦添加Teeth
)。如果您坚持不使用RTTI,那么编码不是很多,但是如果您可以使用dynamic_cast
,为什么不使用它呢?
编辑:来自 Vaughn Cato 的答案非常适合 - 虚拟/抽象teethPtr()
(或getTeeth()
)动物与快捷方式,如biteIfYouCan()
。适用于嵌入式世界(微芯片),但对于PC,我仍然更喜欢dynamic_cast
。
3)Virtual inheritance可以帮助我们BitingAnimal
与FlyingAnimal
:
class BitingAnimal: public virtual Animal {
Teeth teeth; public void bite() { teeth.bite(); } };
class FlyingAnimal: public virtual Animal {
Wings wings; public void fly() { wings.fly(); } };
class FlyingBitingAnimal: /*public virtual Animal, */
public FlyingAnimal, public BitingAnimal {};
4)加入Animal
和Teeth
毫无意义,除非您完全删除Teeth
并将其替换为HasTeeth
或CanBite
。然后它变成我的 1b 。
5)这个枚举是另一个版本的2 - 膨胀的动物。不好。
但这导致我选择不使用dynamic_cast:你可以通过功能(enum,或动物上的标志 - bool can_bite
)来模仿它,它可以告诉你,这是非常安全的。然后,您可以使用多个/虚拟继承来模拟dynamic_cast(首先检查功能,然后再执行下载)。
编辑: Vaughn Cato 的teethPtr()
也与此相符( 告诉我你可以咬牙的牙齿如果你有 )并且不需要演员。
回答评论:
简而言之: 尝试命名一项功能(能力,做某事,提供某些东西的能力)。
答案很长: 您需要Teeth
和HasTeeth
还是单CanBite
?你的第四个解决方案在原则上并不坏,但在命名和其他可能性方面。所有这些都是假设的。接口在其他语言中是众所周知的(单继承+接口),HasTeeth
类似于C#IListSource
,IList GetList()
和bool ContainsList
(用于擦除),其中{{1} }不是直接存在,但可以通过扩展方法添加:
bite()
在这里你可以看到,我已经使用C#扩展方法实现了与C ++多重继承相同的功能。名称是是-a 表单 - SourceOf 。你能和我们分享GUI中的真实姓名吗?
来自C ++的示例将是iostream = istream + ostream,具有来自ios的虚拟继承。再次 是-a 命名。
答案 4 :(得分:0)
每只带牙齿的动物都可以咬()&lt; =&gt;没有牙齿的动物不能咬()
这会减少分离。有更好的答案,但我已取消删除这个答案,以显示我对设计的看法:
<强>原始强>
class Animal { public: virtual ~Animal() {} /* for RTTI */ };
class IBite { virtual void Bite() = 0; }; // interface
class Teeth: public IBite { public: void Bite() { ... } }; // implementation
class HasTeeth { protected: Teeth teeth; }; // part of thought process
class BiteWithTeeth: public HasTeeth, public IBite { // interface
public: void Bite() { teeth.Bite(); } }; // +implementation
class Cat: public Animal, public BiteWithTeeth {}; // final class
class Unknown: public Animal, public HashTeeth {}; // but no IBite - not allowed
上面的顺序会让你想到原因。它涉及使用dynamic_cast
来查找界面(如果你坚持不使用它,解决方案会有所不同)。 我更喜欢将不相关的东西分开并找到真正相关的最小东西(类/接口)(否则,当你发现需要它时,你会在以后再做 - 如果你需要分开的话咬牙切齿。
class AnimalBitingWithTeeth: public Animal, public BiteWithTeeth {};
class AnimalWithTeethButNotBiting: public Animal, public HashTeeth {};
我们需要Animal
作为基类,IBite
(或IBiting
)来标记某些功能。其他类允许我们实现该功能(例如BiteWithTeeth
),但也允许其他实现。
重要的问题是:我们可以拥有一个包含IBite但我们想要禁用它的链吗?我们可以重新实现(虚拟)Bite with empty或添加virtual bool CanBite()
如果我们需要问这样的问题(而不是咬,如果你可以)。 在OOP中找到正确的问题(和功能)以创造良好的设计是非常重要的。
请参阅我的第二个答案 - 解决方案 - 多重/虚拟继承。
答案 5 :(得分:0)
将内容放入不共享内容的同一容器中会产生代码气味。 查询正确的容器而不是查询它的功能类。
如果您真的想要一个创建实例,请使用帮助程序类。但是尝试将接口用于具有 common 内容的类。想象一下,如果你有5000种动物类型,其中两种可以咬人。你真的想透过他们每个人去检查他们每个人的牙齿吗?你已经知道你获得它们时只有两只狗。
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
struct Animal { virtual ~Animal() {} }; // used to tag elements
struct Biting : public Animal { virtual void bite() = 0; };
struct Flying : public Animal { virtual void fly() = 0; };
struct Bird : public Flying {
virtual void fly() override { cout << "Fly\n"; }
};
struct Dog : public Biting {
virtual void bite() override { cout << "Bite\n"; }
};
struct World {
void populate(Animal *animal) {
Biting *b = dynamic_cast<Biting *>(animal);
if (b != nullptr) {
Biters.push_back(b);
return;
}
Flying *f = dynamic_cast<Flying *>(animal);
if (f != nullptr) {
Flyers.push_back(f);
return;
}
}
std::vector<Biting *> Biters;
std::vector<Flying *> Flyers;
};
class Schaefer : public Dog { };
class Pitbull : public Dog { };
class KingFisher : public Bird { };
class WoodPecker : public Bird { };
int main(int argc, char **argv) {
World w;
Schaefer s;
Pitbull p;
KingFisher k;
WoodPecker wp;
w.populate(&s);
w.populate(&p);
w.populate(&k);
w.populate(&wp);
for (auto &i : w.Biters) {
i->bite();
}
for (auto &i : w.Flyers) {
i->fly();
}
return 0;
}