为什么更喜欢组合而不是继承?每种方法都有哪些权衡?什么时候应该选择继承而不是组合?
当前回答
继承带来了所有不可否认的好处,下面是它的一些缺点。
继承的缺点:
您不能在运行时更改从超级类继承的实现(显然是因为继承是在编译时定义的)。继承将子类暴露给其父类实现的细节,这就是为什么人们常说继承破坏了封装(从某种意义上讲,您真正需要关注的是接口而不是实现,所以通过子类重用并不总是首选的)。继承提供的紧密耦合使得子类的实现与超级类的实现紧密结合,父类实现中的任何更改都将迫使子类进行更改。子类的过度重用会使继承堆栈变得很深,也很混乱。
另一方面,通过对象获取对其他对象的引用,在运行时定义对象组合。在这种情况下,这些对象将永远无法访问彼此的受保护数据(没有封装中断),并将被迫尊重彼此的接口。在这种情况下,实现依赖性也会比继承的情况下少得多。
其他回答
当您想要“复制”/公开基类的API时,可以使用继承。当您只想“复制”功能时,请使用委派。
其中的一个例子是:您想要从列表中创建堆栈。堆栈只有pop、push和peek。考虑到堆栈中不需要push_back、push_front、removeAt等功能,您不应该使用继承。
更喜欢组合而不是继承,因为它更具延展性/更易于稍后修改,但不要使用组合始终方法。通过组合,使用依赖注入/设置器可以很容易地动态改变行为。继承更为严格,因为大多数语言不允许从多个类型派生。因此,一旦你从A型衍生出,鹅或多或少都是熟的。
我的上述酸性测试是:
TypeB是否希望公开TypeA的完整接口(所有公共方法都不少于),以便在需要TypeA的地方使用TypeB?表示继承。例如,赛斯纳双翼飞机将暴露出飞机的完整界面,如果不是更多的话。因此,它适合于从飞机衍生。TypeB是否只想要TypeA暴露的部分行为?表示需要合成。例如,鸟可能只需要飞机的飞行行为。在这种情况下,将其作为接口/类/两者提取出来并使其成为两个类的成员是有意义的。
更新:刚刚回到我的答案,现在看来,如果没有具体提到芭芭拉·里斯科夫的里斯科夫替代原则作为“我应该继承这种类型吗?”的测试,它似乎是不完整的
在可以枚举不变量的情况下,子类型更合适,功能更强大,否则使用函数组合实现可扩展性。
你需要看看Bob叔叔的SOLID类设计原则中的Liskov替换原则。:)
这里没有找到满意的答案,所以我写了一个新的。
为了理解为什么“更喜欢组合而不是继承”,我们需要首先找回这个缩短的习惯用法中省略的假设。
继承有两个好处:子类型化和子类化
子类型化意味着符合类型(接口)签名,即一组API,并且可以覆盖部分签名以实现子类型化多态性。子类化意味着方法实现的隐式重用。
这两个好处带来了两个不同的继承目的:面向子类型和面向代码重用。
如果代码重用是唯一的目的,那么子类化可能会给一个比他所需要的更多的东西,即父类的一些公共方法对于子类来说没有多大意义。在这种情况下,不赞成组合而不是继承,而是要求组合。这也是“is-a”与“has-a”概念的由来。
因此,只有当有了子类型,即以后以多态的方式使用新类时,我们才会面临选择继承或组合的问题。这是在所讨论的缩短成语中被省略的假设。
To子类型要符合类型签名,这意味着组合必须始终公开不少于该类型的API数量。现在开始进行权衡:
继承提供了直接的代码重用(如果不被重写),而组合必须对每个API重新编码,即使这只是一个简单的委托工作。继承通过内部多态站点this提供了直接的开放递归,即在另一个成员函数中调用重写方法(甚至类型),无论是公共的还是私有的(尽管不鼓励)。开放递归可以通过组合来模拟,但它需要额外的努力,并且可能并不总是可行的(?)。对重复问题的回答也有类似之处。继承公开受保护的成员。这打破了父类的封装,如果被子类使用,则会引入子类和父类之间的另一个依赖关系。组合具有控制反转的特性,其依赖性可以动态注入,如装饰器模式和代理模式所示。组合具有面向组合器编程的优点,即以类似于组合模式的方式工作。编程到接口后立即进行组合。组合具有易于多重继承的优点。
考虑到上述权衡,我们因此更喜欢组合而不是继承。然而,对于紧密相关的类,即当隐式代码重用真正带来好处,或者需要开放递归的魔力时,继承应该是选择。