【问题标题】:Does this example warrant the use of public inheritance这个例子是否保证使用公共继承
【发布时间】:2020-08-24 17:15:54
【问题描述】:

我正在尝试实现一个库,其中 Class1 提供了大约五个公共方法 Method1 到 Method5。 Class2 提供了两种方法——Methods6 和 Method7。 Class3 提供了一种方法——Method8。现在,对于最终用户,我想从这些类的组合中公开方法。例如。如果最终用户实例化一个名为 Class1Class2 的类,他们应该有权访问 Method1 到 Method7,如果他们实例化一个名为 Class1Class3 的类,他们应该有权访问 Method1 到 Method5 和 Method8。

我能想到 3 种不同的方法(也请提出其他方法):

  1. 多重继承:保持 Class1、Class2 和 Class3 原样。然后,创建一个从 Class1 和 Class2 公开多重继承的新类 Class1Class2。同样,我可以创建一个类 Class1Class3,它公开多个继承自 Class1 和 Class3。

  2. 多级继承:我可以从 Class1 派生 Class2,并将其称为 Class1Class2。和 Class1 中的 Class3 并称之为 Class1Class3。如果我们需要 Class1Class2Class3,我们从 Class2 和 Class3 继承该类,它们都派生自 Class1。在这里,我们将使用虚拟继承来解决菱形问题。我不希望使用 Class2Class3,所以这应该不是问题。

  3. 组成:保持 Class1、Class2 和 Class3 的原样。创建实现 Method1 到 Method7 的每个方法的 Class1Class2,并在内部将它们相应地委托给 Class1 和 Class2 的对象。类似地,Class1Class3 将组成 Class1 和 Class3 的对象。使用这种方法,我们需要为所有方法提供实现并将它们委托给组合对象。

虽然“组合优于继承”指南通常非常适合类的松散耦合等,但在上述情况下,我们必须从单独的具体实现中进行代码重用,方法 1 或 2 似乎是更好的选择。

【问题讨论】:

  • 你看过 Liscov 替换原则吗?与遵守可替换性相比,正确的继承与代码重用的关系要小得多。
  • 方法 1 看起来更可行。去吧。
  • @StephenNewell 是的,但是在这种情况下,对于选项 (2),从 Class1 派生的 Class2 不会改变 Class1 的任何方法,因此在技术上是 IS-A Class1。所以本质上,只要用户用 Class2 代替 Class1,什么都不会破坏。这不满足 LSP 吗?
  • 如果函数采用Class1,是否可以安全地传递Class1Class2Class1Class3Class1Class2Class3对于该函数执行的任何操作。如果是这样,就满足了LSP,这就是继承的合适使用。
  • 另外两个需要考虑的想法:静态多态性和 CRTP。

标签: c++ oop design-patterns


【解决方案1】:

您在这里只谈论代码重用。我认为那是因为您实际上不需要也不想要实际的多态性。如果确实是这样,那么考虑私有继承和using 暴露父方法。

例如:

class Class1Class2 : Class1, Class2 {
public:
    using Class1::Method1;
    // ...
    using Class2::Method6;
    // ...
};

私有继承虽然在技术上称为继承,但与公共继承有很大不同,公共继承本身在概念上与子类型不同。

在 C++(以及许多其他支持 OOP 的语言)中,公共继承通常提供子类型化和代码重用。但是,完全有可能在期望父类的地方派生一个行为不正确的类。这可能会破坏子类型化。也完全有可能派生一个不重用父类的任何实现的类。这可能会破坏代码重用。

从概念上讲,子类型化是通过接口继承来表达的,而实现继承只是重用代码的一种方式。据我了解,“组合优于继承”的说法是关于使用其他工具重用代码,因为实现继承通常会导致代码错误。但是,除了继承之外,没有其他方法可以实现真正的子类型化,因此它可能仍然有用1

另一方面,私有继承只是一种奇怪的组合形式。它只是将成员替换为私有基类。这样做的一个优点是能够使用using 轻松公开您想要公开的“成员”的部分。


1 我个人不喜欢任何一种形式的(公共)继承,更喜欢静态多态性和编译时鸭子类型。但是,我可以愉快地使用接口继承,而我通常远离实现继承。

【讨论】:

  • 你说得对,我只是指代码重用,子类型不适用于我拥有的类。那么,您会说私有继承和using 是比公共继承更好的选择(例如选项1)吗?你能详细说明原因吗?
  • @Chandan 那里,我详细说明了。
【解决方案2】:

由于您想要简单的组合,您可以使用模板作为您的第一个提案的变体:

template <typename ... Bases>
struct Derived : Bases...
{
};

using Class1Class2 = Derived<Class1, Class2>;
using Class1Class2Class3 = Derived<Class1, Class2, Class3>;

【讨论】:

  • 谢谢,这很优雅。唯一的后续问题是,考虑到上述用例,是否推荐选项 1。或者公共继承通常被认为是不好的做法(正如网上很多文章所暗示的那样)?
  • 我同意继承不应该是默认选择,而是更喜欢组合。但有时,继承更合适或更方便。由您来做(适当的)交易。 (在目前的情况下,组合很冗长,有一些重复:/继承很方便......)
【解决方案3】:

为了让事情变得更有趣,您可以使用 CRTP。这是一个例子:

template<typename Base>
class ClassA {
public:
    void MethodA1() {static_cast<Base*>(this)->MethodA1_Impl();}
    void MethodA2() {static_cast<Base*>(this)->MethodA2_Impl();}
};

template<typename Base>
class ClassB {
public:
    void MethodB1() {static_cast<Base*>(this)->MethodB1_Impl();}
    void MethodB2() {static_cast<Base*>(this)->MethodB2_Impl();}
};

template<typename Base>
class ClassC {
public:
    void MethodC1() {static_cast<Base*>(this)->MethodC1_Impl();}
    void MethodC2() {static_cast<Base*>(this)->MethodC2_Impl();}
};

class ClassABC: public ClassA<ClassABC>, public ClassB<ClassABC>, public ClassC<ClassABC> {
public:
    //void MethodA1_Impl();
    //void MethodA2_Impl();
    //void MethodB1_Impl();
    //void MethodB2_Impl();
    //void MethodC1_Impl();
    //void MethodC2_Impl();
};

您可以取消注释并实现MethodXY_Impl() 的任何子集,这样就可以编译。客户端代码可以调用来自MethodXY() 的任何方法。如果没有相应的实现 - 编译器会产生错误。

【讨论】:

  • 我在不同的具体类中有实现,因为它们各自负责那一件事。使用您的解决方案,您将所有实现方法移动到 ABC 类中。不确定 CRTP 解决方案如何解决重用/公开 3 个不同类的具体实现的问题。
  • @Chandan CRTP 解决了暴露不同接口的问题,无需实现整套方法。此外,它避免了虚拟继承和动态多态(两者都有明显的缺点)。无论如何,你还没有从业务逻辑的角度表达你的问题,所以你的模糊问题没有正确/错误的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-05
  • 2010-12-06
  • 2015-07-14
  • 2019-08-24
  • 2011-03-11
  • 1970-01-01
相关资源
最近更新 更多