界面与组合
interface vs composition
我想我理解界面和抽象之间的区别。 抽象设置默认行为,在纯抽象的情况下,行为需要由派生类设置。 接口是获取您需要的东西,而无需基类的开销。 那么界面相对于构图的优势是什么?我能想到的唯一优点是在基类中使用受保护的字段。 我错过了什么?
你的标题没有意义,你的解释有点模糊,所以让我们定义术语(并介绍缺少的键(。
这里有两件不同的事情:
- 抽象类与接口
- 继承与组合
让我们从接口和抽象类开始。
- 抽象类(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++它们可以被描述为只有纯虚函数的类。薄是好的,因为
- 它减少了使用或实现接口的学习曲线
- 它减少了用户和接口实现者之间的耦合(依赖性(。因此,用户确实很好地与他们正在使用的接口的实现中的更改隔离开来。
这与动态库链接相结合,有助于促进即插即用,这是近年来默默无闻但伟大的软件创新之一。这带来了更大的软件互操作性、可扩展性等。
接口可以做更多的工作。当您有一个重要的子系统,并且有一天可能有多个可能的实现时,请证明采用它们的合理性。在这种情况下,应通过接口使用子系统。
通过继承权重用需要对所覆盖的实现的行为有更多的了解,因此有更大的"耦合"。也就是说,在接口矫枉过正的情况下,这也是一种有效的方法。
继承自类型 X 的,那么在大多数情况下,知道如何处理 X 类型的对象的代码将自动能够处理 Y 类型的对象。 同样,如果类型 Z 实现接口 I,则知道如何使用有关实现 I 的对象的代码,而不必了解它们的任何信息,将自动能够使用 Z 类型的对象。 继承和接口的主要目的是允许此类替换。
相反,如果 P 类型的对象包含 Q 类型的对象,则期望处理 Q 类型的对象的代码将无法处理 P 类型的对象之一(除非 P 除了保存该类型的对象之外还继承自 Q(。 期望操作 Q 类型的对象的代码将能够在 P 中包含的 Q 实例上运行,但前提是 P 的代码明确地将其直接提供给该代码,或者使其可供这样做的外部代码使用。
- 如何在OMNET++中指定与命令行参数组合的输出文件名
- 可组合的lambda/std::函数与std::可选
- 如何将两个不同矢量的同一位置的两个元素组合在一起
- 混合组合和继承的C++问题
- 我需要将多个函数组合为一个函数
- 构建可组合有向图(扫描仪生成器的汤普森构造算法)
- 通过组合不同的类型来创建唯一的id
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 模板元编程:如何将参数包组合成新的参数包
- 检查向量是否具有所有可能的字符组合
- 如何在加密++中将两个源组合成新的源
- 根中的组合
- 更改 C++ 中的组合分类变量
- 错误:(-210:不支持的格式或格式组合)功能'create'中的硬件视频解码器不支持视频源
- 组合字符串不适用于 libCurl,C++
- 测试两个类型列表中的所有组合
- 将多个 for 循环组合成单个迭代器
- 如何从组合指数中找到仓位
- 防止组合框被关闭
- 界面与组合