4

我想我理解接口和抽象之间的区别。抽象设置默认行为,在纯抽象的情况下,行为需要由派生类设置。接口是你所需要的,没有基类的开销。那么接口相对于组合的优势是什么?我能想到的唯一优点是在基类中使用受保护的字段。我错过了什么?

4

5 回答 5

8

您的标题没有意义,并且您的解释有点模糊,所以让我们定义术语(并介绍缺少的关键)。

这里有两件不同的事情:

  • 抽象类与接口
  • 继承与组合

让我们从接口和抽象类开始。

  • 抽象类(在 C++ 中)是一个无法实例化的类,因为它的至少一个方法是纯虚方法。
  • 接口,在类 Java 语言中,是一组没有实现的方法,在 C++ 中,它是用只有纯虚拟方法的抽象类来模拟的。

因此,在 C++ 的上下文中,两者之间没有太大区别。特别是因为这种区别从未考虑到自由功能。

例如,考虑以下“接口”:

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

    virtual bool less(LessThanComparable const& other) const = 0;
};

即使使用免费功能,您也可以轻松地扩充它:

inline bool operator<(LessThanComparable const& left, LessThanComparable const& right) {
    return left.less(right);
}

inline bool operator>(LessThanComparable const& left, LessThanComparable const& right) {
    return right.less(left);
}

inline bool operator<=(LessThanComparable const& left, LessThanComparable const& right) {
    return not right.less(left);
}

inline bool operator>=(LessThanComparable const& left, LessThanComparable const& right) {
    return not left.less(right);
}

在这种情况下,我们提供行为......但类本身仍然是一个接口......哦,好吧。


因此,真正的争论是在继承组合之间。

继承经常被滥用来继承行为。这是不好的。应该使用继承来建模is-a关系。否则,您可能需要组合。

考虑一个简单的用例:

class DieselEngine { public: void start(); };

现在,我们如何Car用这个构建一个?

如果您继承,它将起作用。然而,突然你得到这样的代码:

void start(DieselEngine& e) { e.start(); }

int main() {
    Car car;
    start(car);
}

现在,如果您决定替换DieselEngineWaterEngine,则上述功能不起作用。编译失败。并且WaterEngine继承自DieselEngine肯定感觉很恶心......

那么解决方案是什么?组成

class Car {
public:
    void start() { engine.start(); }

private:
    DieselEngine engine;
};

这样,没有人可以编写假设汽车是发动机的荒谬代码(doh!)。因此,更换发动机很容易,绝对不会影响客户

这意味着您的实现和使用它的代码之间的一致性较低;或者通常所说的:少耦合


经验法则是,一般来说,从具有数据或实现行为的类继承应该不被接受。它可以是合法的,但通常有更好的方法。当然,就像所有的经验法则一样,它是带着一粒盐的。小心过度工程。

于 2012-04-26T15:26:30.027 回答
6

界面定义了您将如何使用。

你继承是为了被重用。这意味着您想适应某个框架。如果您不需要适应框架,即使是您自己制作的框架,也不要继承。

组合是一个实现细节。不要为了得到基类的实现而继承,组合它。仅当它允许您适应框架时才继承。

于 2012-04-26T14:05:32.520 回答
5

接口定义行为。抽象类有助于实现行为。

理论上,完全没有实现的纯抽象类和接口之间没有太大区别。两者都定义了一个未实现的 API。但是,纯抽象类通常用于不支持接口的语言中以提供类似语义的接口(例如 C++)。

当您有选择时,通常抽象基础将提供某种级别的功能,即使它不完整。它有助于实现常见行为。缺点是你被迫从中获得。当您只是定义用法时,请使用接口。(没有什么能阻止你创建一个实现接口的抽象基础)。

于 2012-04-26T14:18:58.297 回答
0

如果类型 Y 继承自类型 X,那么知道如何处理类型 X 的对象的代码在大多数情况下将自动能够处理类型 Y 的对象。同样,如果类型 Z 实现接口 I,那么知道如何处理类型 X 的代码如何使用关于哪些实现的对象 我无需了解它们,就可以自动使用 Z 类型的对象。继承和接口的主要目的是允许这样的替换。

相反,如果 P 类型的对象包含 Q 类型的对象,那么期望与 Q 类型的对象一起工作的代码将无法在 P 类型之一上工作(除非 P 除了持有 Q 的对象之外还继承自 Q那种)。期望操作 Q 类型对象的代码将能够对包含在 P 中的 Q 实例进行操作,但前提是 P 的代码直接将其提供给该代码,或使其可供外部代码使用。

于 2012-04-26T19:43:01.480 回答
0

接口很薄,在 C++ 中,它们可以被描述为只有纯虚函数的类。薄是好的,因为

  • 它减少了使用或实现界面的学习曲线
  • 它减少了用户和接口实现者之间的耦合(依赖)。因此,用户可以很好地避免他们正在使用的接口的实现发生变化。

这与动态库链接相结合,有助于促进即插即用,这是最近不为人知但伟大的软件创新之一。这导致更大的软件互操作性、可扩展性等。

接口可以做更多的工作来落实。当你有一个重要的子系统,有一天可能有多个可能的实现时,证明它们的采用是合理的。在这种情况下,应通过接口使用子系统。

通过继承重用需要更多地了解您正在覆盖的实现的行为,因此存在更大的“耦合”。也就是说,在接口过度使用的情况下,这也是一种有效的方法。

于 2012-04-26T14:23:39.670 回答