在c ++中的has-a关系中是否有良好的接口替换

时间:2014-08-16 19:15:25

标签: c++ oop

我在GUI设计中面临OOP问题,但让我用动物示例来说明它。让我们进行以下设置:

  • 有一个基类Animal
  • 任何派生类都可以拥有-a 牙齿
  • 每个有牙齿的动物都可以咬()< =>没有牙齿的动物不能咬()
  • 每个动物Bite()的方式相同(在Teeth类中有默认实现)

动物拥有牙齿是很自然的,但现在我需要像 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。)可以咬人的类的枚举 - 只有它的声音很奇怪。


或者我只是过于复杂并错过了重要的事情?

6 个答案:

答案 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)

以下是我对您的解决方案的评论:

  1. 我同意你的观点,这会导致很多代码重复。不酷。
  2. 这显然不是最好的方法。使用canBite标志是一种症状,可以做得更好。
  3. 同样,糟糕的设计 - 调用BitingAnimal.teeth.Bite()违反了Law of Demeter。此外,正如您所描述的 - 如果不是BittingAnimal的动物想要咬什么呢?
  4. 正如Mare Infinitus评论的那样,在这种方法中,您的Cat Teeth。这不是完全正确。
  5. 可以咬人的课程 - 你是对的。这很奇怪:)
  6. 我建议采用以下方法:在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膨胀到怪物类,但这在我的实现中是不可能的。好处是,你只需要实现你需要的东西,当动物咬牙齿时,你也不会感到困惑,因为实施是在一个功能中而不是在不同的类别中分开。

Pterodactyl

答案 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可以帮助我们BitingAnimalFlyingAnimal

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)加入AnimalTeeth毫无意义,除非您完全删除Teeth并将其替换为HasTeethCanBite。然后它变成我的 1b

5)这个枚举是另一个版本的2 - 膨胀的动物。不好。 但这导致我选择不使用dynamic_cast:你可以通过功能(enum,或动物上的标志 - bool can_bite)来模仿它,它可以告诉你,这是非常安全的。然后,您可以使用多个/虚拟继承来模拟dynamic_cast(首先检查功能,然后再执行下载)。

编辑: Vaughn Cato teethPtr()也与此相符( 告诉我你可以咬牙的牙齿如果你有 )并且不需要演员。


回答评论:

简而言之: 尝试命名一项功能(能力,做某事,提供某些东西的能力)。

答案很长: 您需要TeethHasTeeth还是单CanBite?你的第四个解决方案在原则上并不坏,但在命名和其他可能性方面。所有这些都是假设的。接口在其他语言中是众所周知的(单继承+接口),HasTeeth类似于C#IListSourceIList 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;
}