2016-10-31 62 views
0

我想扩展一个类以包含额外的数据和功能(我想要多态行为)。使用继承和多继承似乎很明显。为扩展类功能提供最佳方法的建议

看了各种职位,继承(尤其是多重继承),是有问题的,我已经开始寻找其他的选择:

  • 把所有的数据和功能于一体的类,而不是使用继承
  • Composite模式
  • 混入

是否有下面的继承示例建议的方法呢?这是继承是否合理的情况? (但我不喜欢把默认功能的基类)

#include <iostream> 

//================================ 
class B { 

public: 
    virtual ~B() { } 

    void setVal(int val) { val_ = val; } 

    // I'd rather not have these at base class level but want to use 
    // polymorphism on type B: 

    virtual void setColor(int val) { std::cout << "setColor not implemented" << std::endl; } 
    virtual void setLength(int val) { std::cout << "setLength not implemented" << std::endl; } 

private: 
    int val_; 

}; 

//================================ 
class D1 : virtual public B { 

public: 
    void setColor(int color) { 
     std::cout << "D1::setColor to " << color << std::endl; 
     color_ = color; 
    } 

private: 
    int color_; 

}; 

//================================ 
class D2 : virtual public B { 

public: 
    void setLength(int length) { 
     std::cout << "D2::setLength to " << length << std::endl; 
     length_ = length; 
    } 

private: 
    int length_; 

}; 

//================================ 
// multi-inheritance diamond - have fiddled with mixin 
// but haven't solved using type B polymorphically with mixins 
class M1 : public D1, public D2 { 

}; 


//================================ 
int main() { 

    B* d1 = new D1; 

    d1->setVal(3); 
    d1->setColor(1); 


    B* m1 = new M1; 

    m1->setVal(4); 
    m1->setLength(2); 
    m1->setColor(4); 

    return 0; 
} 
+0

没有具体的例子就很难说出任何东西。继承代表的是一种关系。通常,当你开始谈论向对象添加“行为和更多数据”时,这是否意味着它总是使用行为和数据,或者它是可选的?如果这样查看组件和组合而不是继承和运行时查询附加行为。如果你真的觉得你会继续扩展你的基本类型,这种方法更加模块化。 –

+0

您可能想了解[纯虚方法](http://stackoverflow.com/questions/1306778/c-virtual-pure-virtual-explained) – wasthishelpful

+0

单继承是相当自然的,以确保整个层次结构将尊重共享概念;多重继承(没有虚拟)对于为同一个对象提供不同的视图可能很有用。应谨慎使用多个虚拟继承(dynamic_cast从基础派生到派生需要),正如您所提到的,复合或装饰器模式可能是有趣的替代方案。 – Franck

回答

1

与原来的示例代码

有许多与你的榜样问题怀疑的问题。

首先,您不必在基类中提供函数体。改用纯虚函数。其次,你的类D1和D2都没有功能,所以它们应该是抽象的(这会阻止你从它们中创建被剥夺的对象)。如果你真的为你的基类使用纯虚拟函数,这第二个问题将变得清晰。编译器将开始发出警告。

与D134一样,将D1例化为new D1,这是糟糕的设计,因为D1没有setLength方法的真正功能实现,即使您给它一个“虚拟”主体。给它一个'虚拟'身体(一个没有做任何有用的东西),所以掩盖了你的设计错误。

所以你的评论(但我不喜欢不得不把默认函数放在基类中)证明了一个适当的直觉。必须这样做的信号有缺陷的设计。 D1对象无法理解setLength,而其继承的公共接口承诺它可以。

并且:如果使用正确,多继承没有任何问题。它非常强大和优雅。但你必须在适当的地方使用它。 D1和D2是B的部分实现,所以抽象,并从两者继承将确实给你一个完整的实现,非常具体。

也许一个很好的规则是:只有在看到引人注目的需求时才使用多重继承。但是,如果你这样做,这是非常有用的。与例如相比,它可以防止相当丑陋的不对称和代码重复。像Java这样的语言已经禁止了它。

我不是树医生。当我使用链锯时,会危害我的腿。但这并不是说电锯没有用处。

在哪里放假人:无处请,不要取消...

[编辑OP的第一个注释后]

如果你得到的B类D1,将打印的“未实现setLength”如果你调用它的setLength方法,应该如何调用者作何反应?它不应该首先调用它,如果D1不是从具有这种方法的B派生的,那么调用者可能已经知道这个方法,纯粹是虚拟的。那么很明显,它不支持这种方法。拥有B基类让D1感到宾至如归的元素类型,B *或B &的多态数据结构承诺其用户正确支持getLength,而不支持getLength。

尽管在你的例子中情况并非如此(但也许你遗漏了一些东西),但当然可以有一个很好的理由从B中推导出D1和D2。B可以保存最终接口的一部分或实现它的派生类D1和D2都需要。

假设B有一个方法setAny(key,value)(设置字典中的一个值),D1和D2都使用,D1在setColor中调用它,D2在setLength中调用它。 在这种情况下,使用通用基类是有道理的。在这种情况下,B不应该有虚拟方法setColor或setLength,既不是虚拟方法也不是纯粹方法。你应该只在你的D1类中有一个setColor,在你的D2类中有一个setLength,但在你的B类中都没有。

有一个在面向对象设计的基本原则:

不要剥夺继承权

通过引入“方法,这是不适用”的具体类的概念,这正是你正在做。现在像这样的规则不是教条。但违反这条规则几乎总是指向一个设计缺陷。

所有B的一个数据结构是唯一有用的有他们这样做一招,他们都明白...

[EDIT2 OP的第二COMENT后]

OP希望有一个地图,可以容纳来自B的任何类别的对象。

这正是问题出现的地方。要了解如何存储指向我们对象的指针和引用,我们必须问:用于存储的是什么。如果一个地图,比如说mapB被用来存储指向B的指针,那么必须有一个点。数据存储的乐趣在于检索数据并做一些有用的事情。

让我们通过使用日常生活中的清单来简化这一点。假设我有一个personList 1000人,每个人都有他们的全名和电话号码。现在说我的厨房水槽有问题。我实际上可以通读清单,打电话给每个人,问:你能修理我的厨房水槽吗?换句话说:你是否支持repairKitchenSink方法。或者:你有没有可能成为水管工人的一个例子(你是水管工)。但是后来我花了很长时间打电话,或许经过500次电话会议后,我很幸运。

现在我personList上的所有1000人都支持talkToMe方法。所以,每当我感到孤独时,我都可以打电话给任何人,并调用该人的talkToMe方法。但他们不应该都有修理KidchenSink方法,即使不是纯粹的虚拟或虚拟变体也会做别的事情,因为如果我将这种方法称为类Burglar的人,他可能会响应这个呼叫,但是在一个意外的方式。

所以类人不应该包含一个方法repairKitchenSink,即使不是一个纯粹的虚拟人。因为它不应该被称为personList迭代的一部分。在迭代plumberList时应该调用它。该列表仅保存支持repairKitchenSink方法的对象。

使用纯虚函数只有在适当

他们可能会支持它以不同的方式,但。换句话说,在Plumber类中,方法repairKitchenSink可以例如是纯粹的虚拟。有可能例如是2派生类,PvcPlumber和CopperPlumber。 CopperPlumber将通过调用lightFlame实现(代码)repairKitchenSink方法,然后调用solderDrainToSink,而PvcPlumber将实现它作为applyGlueToPvcTube和glueTubeToSinkOutlet的连续调用。但是这两个水管工子类仅以不同的方式实施repairKitchenSink。这并且唯一证明在他们的基类水暖工中拥有纯虚函数repairKitchenSink。当然,一个班级可能来自水管工,不会实施该方法,比如说WannabePlumber类。但是由于它是抽象的,你不能从它实例化对象,这很好,除非你想湿脚。

Person可能有很多不同的子类。他们例如代表不同的职业,或不同的政治偏好或不同的宗教。如果一个人是民主党的Budhist Plumber,那么他(M/F)可能在继承民主党,Budhist和水管工阶级的衍生阶级。使用继承或者甚至为政治偏好或宗教信仰,甚至职业以及这些组合的无尽组合这样动荡的东西打字,在实践中并不方便,但这仅仅是一个例子。在现实中,职业,宗教和政治偏好可能是属性。但这并没有改变这里重要的一点。如果某个类是不支持某种操作的,那么它不应该在数据结构中暗示它的作用。

除了personList,拥有plumberList,animistList和democratList之外,您一定会打电话给理解您的方法inviteBillToPlayInMyJazzBand或worshipTheTreeInMyBackyard的人。

列表不包含对象,它们只包含指向对象的指针或引用。所以我们的民主Budhist管道工被包含在personList,democratList,budhistList和plumberList中没有任何问题。列表就像数据库索引。不包含记录,他们只是指它们。你可以在一个表上有许多索引,而且你应该这么做,因为索引很小,并且使你的数据库更快。

对于多态数据结构也是如此。目前,即使personList,democratList,budhistList和plumberList变得如此之大以至于内存不足,解决方案通常不会只有一个personList。因为那样你就会遇到一个性能问题和一个代码复杂性问题,而这个问题通常会变得更糟。

所以,回到你的评论:你说你想让你所有的派生类都在B的列表中。很好,但是B的接口应该只包含为列表中的所有内容实现的方法,所以没有虚拟方法。这就像是通过图书馆并浏览所有书籍,寻找一种支持教学关于浮萍的方法。老实说,告诉你所有这些,我一直犯下一个资本罪。我一直在推销一般真理。但在软件设计中这些不存在。我一直在试图将它们出售给你,因为我已经教了30年的面向对象设计,而且我认为我认识到了你卡在哪里。但是对于每一条规则,都有很多例外。尽管如此,如果我已经正确地解决了你的问题,在这种情况下,我认为你应该选择单独的数据结构,每个数据结构只保存对象的引用或指针,这些对象确实可以在你迭代特定数据结构时进行欺骗。

点是方形圆

部分的混乱中适当地使用多态数据结构(数据结构保持指针或引用不同的对象类型)来对关系数据库的世界。 RDB与平面记录表一起工作,每个记录具有相同的字段。由于某些领域可能不适用,所以发明了一种叫做“约束”的东西。在C++类中,Point将包含字段x和y。 Class Circle可以继承它,并且还包含字段'radius'。类Square也可以继承Point,但除x和y之外还包含字段“side”。在RDB世界约束中,不是字段,是继承的。因此,一个圆的约束半径将为== 0,而一个Square的约束点的值为== 0.一个点将继承两个约束,所以它将满足既是正方形又是圆的条件:一个点是一个正方形这在数学上确实是这样。请注意,与C++相比,约束继承层次结构是'颠倒'的。这可能会让人困惑。

无论是普遍认为继承与专业化并驾齐驱,两者都无济于事。虽然通常情况下并不总是如此。在许多情况下,C++继承是扩展而不是专业化。这两者往往是一致的,但点,方,圆的例子表明,这不是一个普遍的事实。

如果使用继承,在C++中Circle应该从Point派生,因为它有额外的字段。但是Circle肯定不是一种特殊类型的Point,反之亦然。在许多实际的图书馆中,Circle将包含一个类Point的对象,持有x和y,而不是从它继承,绕过了整个问题。

欢迎的设计选择

你碰到了什么,世界是一个真正的设计选择,一个重要问题。仔细思考这样的事情,就像你在做的一样,在实践中尝试所有这些事情,包括所谓的“错误”,都会使你成为程序员,而不是编码员。

+0

谢谢。在这种情况下,我确实想要实例化D1,D2或M1中的任何一个。如果我使用抽象基类,那么是否在派生类中为那些不适用的函数放置了一个“虚拟”占位符?我在想,如果我将这些虚拟例程放在顶层类中,我不必将它们放在派生类中。 – ark262

+0

我将编辑我的答案 –

+0

谢谢雅克。我将这些方法放在基类中的原因是因为我想使用一个可以容纳任何B或从B派生的类的映射。如果我然后尝试使用映射迭代器调用setLength,它会失败,因为setLength不是基类中的方法。我是C++和OOD的新手,知道这有味道,但不知道如何解决它。我一直试图使用装饰模式(每个人似乎都说继承是坏的),但我似乎遇到了同样的问题。如果你有更多的时间来看这个,我可以发布更多的代码来包含容器的使用。 – ark262

0

让我先状态你正在尝试做的是一个设计的气味:最有可能你是什么实际上试图实现可以以更好的方式来实现。不幸的是,我们无法知道您实际上想要达到的目标,因为您只告诉我们您想如何达到目标

但无论如何,你的实现是坏的,因为这些方法报告“未实现”以用户程序的,而不是向呼叫者。调用者无法对方法做出反应,而没有按照预期做出反应。更糟的是,你甚至不会将它输出到错误流中,而是输出到常规输出流中,所以如果你在任何产生常规输出的程序中使用这个类,那输出将会被你的错误信息中断,可能会使程序混淆进一步在管道中)。

这里有一个更好的方式来做到这一点:

#include <iostream> 
#include <cstdlib> // for EXIT_FAILURE 

//================================ 
class B { 

public: 
    virtual ~B() { } 

    void setVal(int val) { val_ = val; } 

    // note: No implementation of methods not making sense to a B  
private: 
    int val_; 

}; 

//================================ 
class D1 : virtual public B { 

public: 
    void setColor(int color) { 
     std::cout << "D1::setColor to " << color << std::endl; 
     color_ = color; 
    } 

private: 
    int color_; 

}; 

//================================ 
class D2 : virtual public B { 

public: 
    void setLength(int length) { 
     std::cout << "D2::setLength to " << length << std::endl; 
     length_ = length; 
    } 

private: 
    int length_; 

}; 

class M1 : public virtual D1, public virtual D2 { 

}; 


//================================ 
int main() { 

    B* d1 = new D1; 

    p->setVal(3); 
    if (D1* p = dynamic_cast<D1*>(d1)) 
    { 
    p->setColor(1); 
    } 
    else 
    { 
    // note: Use std::cerr, not std::cout, for error messages 
    std::cerr << "Oops, this wasn't a D1!\n"; 
    // Since this should not have happened to begin with, 
    // better exit immediately; *reporting* the failure 
    return EXIT_FAILURE; 
    } 

    B* m1 = new M1; 

    m1->setVal(4); 
    if (D2* p = dynamic_cast<D2*>(m1)) 
    { 
    p->setLength(2); 
    } 
    else 
    { 
    // note: Use std::cerr, not std::cout, for error messages 
    std::cerr << "Oops, this wasn't a D1!\n"; 
    // Since this should not have happened to begin with, 
    // better exit immediately; *reporting* the failure 
    return EXIT_FAILURE; 
    } 
    if (D1* p = dynamic_cast<D1*>(m1)) 
    { 
    p->setColor(4); 
    } 
    else 
    { 
    // note: Use std::cerr, not std::cout, for error messages 
    std::cerr << "Oops, this wasn't a D1!\n"; 
    // Since this should not have happened to begin with, 
    // better exit immediately; *reporting* the failure 
    return EXIT_FAILURE; 
    } 

    return 0; 
} 

或者,你可以利用的事实,你的方法共享一些一致性,并使用一个通用的方法来设置所有:

#include <iostream> 
#include <stdexcept> // for std::logic_error 
#include <cstdlib> 
#include <string> 

enum properties { propValue, propColour, propLength }; 

std::string property_name(property p) 
{ 
    switch(p) 
    { 
    case propValue: return "Value"; 
    case propColour: return "Colour"; 
    case propLength: return "Length"; 
    default: return "<invalid property>"; 
    } 
} 

class B 
{ 
public: 
    virtual ~B() {} 

    // allow the caller to determine which properties are supported 
    virtual bool supportsProperty(property p) 
    { 
    return p == propValue; 
    } 
    void setProperty(property p, int v) 
    { 
    bool succeeded = do_set_property(p,v); 
    // report problems to the _caller_ 
    if (!succeeded) 
     throw std::logic_error(property_name(p)+" not supported."); 
    } 
private: 
    virtual bool do_set_property(property p) 
    { 
    if (p == propValue) 
    { 
     value = v; 
     return true; 
    } 
    else 
     return false; 
    } 

    int value; 
}; 

class D1: public virtual B 
{ 
public: 
    virtual bool supportsProperty(property p) 
    { 
    return p == propColour || B::supportsProperty(p); 
    } 
private: 
    virtual bool do_set_property(property p, int v) 
    { 
    if (p == propColour) 
    { 
     colour = v; 
     return true; 
    } 
    else 
     return B::do_set_property(p, v); 
    } 

    int colour; 
}; 

class D2: public virtual B 
{ 
public: 
    virtual bool supportsProperty(property p) 
    { 
    return p == propLength || B::supportsProperty(p); 
    } 
private: 
    virtual bool do_set_property(property p, int v) 
    { 
    if (p == propLength) 
    { 
     length = v; 
     return true; 
    } 
    else 
     return B::do_set_property(p, v); 
    } 

    int length; 
}; 

class M1: public virtual D1, public virtual D2 
{ 
public: 
    virtual bool supportsProperty(property p) 
    { 
    return D1::supportsProperty(p) || D2::supportsProperty(p); 
    } 
private: 
    bool do_set_property(property p, int v) 
    { 
    return D1::do_set_property(p, v) || D2::do_set_property(p, v); 
    } 
}; 
+0

谢谢你的回复。你知道在基类中是否有一个特定的成语名称吗? (所以我可以进一步研究这个想法) – ark262

+0

@ ark262:我不知道一个成语的名字,对不起。 – celtschk

+0

Dewhurst的“C++ Common Knowledge”似乎称之为“能力查询” – ark262

相关问题