界面与组合

interface vs composition

本文关键字:组合 界面      更新时间:2023-10-16

我想我理解界面和抽象之间的区别。 抽象设置默认行为,在纯抽象的情况下,行为需要由派生类设置。 接口是获取您需要的东西,而无需基类的开销。 那么界面相对于构图的优势是什么?我能想到的唯一优点是在基类中使用受保护的字段。 我错过了什么?

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

这里有两件不同的事情:

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

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

  • 抽象类(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);
}

现在,如果您决定将DieselEngine替换为 WaterEngine ,上述功能不起作用。编译失败。而WaterEngine继承DieselEngine当然感觉很奇怪......

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

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

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

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


经验法则是,一般来说,从具有数据或实现行为的类继承应该不受欢迎。它可以是合法的,但通常有更好的方法。当然,像所有经验法则一样,要持保留态度;小心过度设计。

接口定义如何使用您。

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

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

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

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

当你有选择时,通常抽象基础会提供一定程度的功能,即使它不完整。它有助于实施共同行为。缺点是你被迫从中得出。当您只是定义用法时,请使用接口。(没有什么能阻止你创建一个实现接口的抽象基础(。

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

  • 它减少了使用或实现接口的学习曲线
  • 它减少了用户和接口实现者之间的耦合(依赖性(。因此,用户确实很好地与他们正在使用的接口的实现中的更改隔离开来。

这与动态库链接相结合,有助于促进即插即用,这是近年来默默无闻但伟大的软件创新之一。这带来了更大的软件互操作性、可扩展性等。

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

通过继承权重用需要对所覆盖的实现的行为有更多的了解,因此有更大的"耦合"。也就是说,在接口矫枉过正的情况下,这也是一种有效的方法。

如果类型 Y

继承自类型 X 的,那么在大多数情况下,知道如何处理 X 类型的对象的代码将自动能够处理 Y 类型的对象。 同样,如果类型 Z 实现接口 I,则知道如何使用有关实现 I 的对象的代码,而不必了解它们的任何信息,将自动能够使用 Z 类型的对象。 继承和接口的主要目的是允许此类替换。

相反,如果 P 类型的对象包含 Q 类型的对象

,则期望处理 Q 类型的对象的代码将无法处理 P 类型的对象之一(除非 P 除了保存该类型的对象之外还继承自 Q(。 期望操作 Q 类型的对象的代码将能够在 P 中包含的 Q 实例上运行,但前提是 P 的代码明确地将其直接提供给该代码,或者使其可供这样做的外部代码使用。