接口 vs 组合

4

我认为我理解了接口和抽象类的区别。抽象类设置默认行为,对于纯抽象类,行为需要由派生类设置。接口是从基类中获取所需内容而无需附加开销。那么,与组合相比,接口的优势在哪里?我唯一能想到的优点是在基类中使用受保护字段。我漏掉了什么吗?


2
你考虑使用哪种编程语言? - Chetter Hummin
5个回答

10
您的标题没有意义,解释也有些模糊,因此让我们定义这些术语(并介绍其中缺失的关键术语)。
这里发生了两件不同的事情:
- 抽象类与接口 - 继承与组合
让我们从接口和抽象类开始:
- 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);
}

现在,如果你决定用WaterEngine替换DieselEngine,上述函数就无法使用。编译失败。而让WaterEngine继承自DieselEngine显然感觉很奇怪... 那么解决方案是什么呢?组合。
class Car {
public:
    void start() { engine.start(); }

private:
    DieselEngine engine;
};

这样做能避免无意义的代码,例如假设一个汽车是发动机(天哪!)。因此,更换发动机变得简单易行,绝对不会影响客户
这意味着你的实现与使用它的代码之间的依赖性更小;或者通常所说的耦合度更低
一般来说,从具有数据或实现行为的类继承应该被反对。这样做可以是合法的,但通常有更好的方法。当然,像所有经验法则一样,要适度考虑;小心过度设计。

6

接口定义了你的使用方式。

继承是为了重用代码。这意味着你想要适应某个框架。如果你不需要适应任何框架,甚至包括自己创建的框架,就不要继承。

组合是一个实现细节。不要为了获取基类的实现而继承它,应该使用组合。只有在需要适应框架的情况下才应该继承。


5
一个接口定义行为,而抽象类则有助于实现行为。
理论上,一个没有任何实现的纯抽象类和一个接口之间的区别并不大。两者都定义了未实现的API。然而,在不支持接口的语言中,纯抽象类通常用于提供类似于接口的语义(例如C ++)。
当您有选择时,通常抽象基类将提供一定级别的功能,即使它不完整也如此。它有助于实现共同的行为。缺点是您被迫从中派生。当您仅定义使用时,请使用接口。(没有任何限制阻止您创建一个实现接口的抽象基类)。

0

接口是轻量级的,用C++可以描述为只有纯虚函数的类。轻量级是好的,因为

  • 它减少了使用或实现接口的学习曲线
  • 它减少了用户和接口实现者之间的耦合(依赖)。因此,用户真正地与他们正在使用的接口的实现变化隔离开来。

这个特点与动态库链接结合使用,有助于促进“即插即用”,这是近年来未被赞扬但伟大的软件创新之一。这导致更大的软件互操作性、可扩展性等。

接口可能需要更多的工作来实现。当您有一个重要的子系统可能有多个可能的实现时,通过接口使用该子系统应该被采用。

通过继承进行重用需要更多关于您正在覆盖的实现行为的知识,因此存在更大的“耦合”。尽管如此,在接口过度的情况下,这也是一种有效的方法。


1
在接口与组合的特定上下文中,接口引入的耦合性比组合更大。 - Matthieu M.
我没有看到那个。请随意详细说明。 - ScrollerBlaster
1
问题涉及继承行为。问题在于继承是可见的,因此其他人可能会使用它,引入不必要的耦合。另一方面,由于您的属性是隐藏的,没有人依赖它们,因此组合不会引入任何耦合。 - Matthieu M.
@Matthieu 可能我们在谈论不同的事情。当我提到实现一个接口时,实现者从一个具有纯虚函数和没有实现的结构体继承。他们没有继承任何行为。实现者的类对接口的使用者是不可见的。还能有更少的耦合吗! - ScrollerBlaster
1
是的,恐怕这个问题是错误的。接口与组合根本没有意义。我添加了一个答案,探讨了接口与抽象类(在C++中没有太大区别)以及继承与组合之间的区别。 - Matthieu M.
在我那个年代,我们称组合为“包含”,我将接口解释为一个有效的C++构造,与抽象类不同。 - ScrollerBlaster

0
如果类型Y继承自类型X,那么知道如何处理类型X对象的代码,在大多数情况下,都可以自动处理类型Y对象。同样地,如果类型Z实现接口I,那么知道如何使用实现I的对象的代码,而不需要了解它们的任何信息,就可以自动使用类型Z的对象。继承和接口的主要目的是允许这种替换。
相比之下,如果类型P的对象包含类型Q的对象,则期望使用类型Q对象的代码将无法在类型P对象上工作(除非P除了持有该类型的对象外还继承自Q)。期望操作类型Q对象的代码将能够在P中包含的Q实例上操作,但前提是P的代码明确地直接向其提供它,或者使其可用于为之提供它的外部代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接