面试中被面试官问到组合模式和继承有什么区别,给我问懵了,今天又仔细看了下,这不就是UE里的组件吗 >_<
文章目录
- 问题提出
- 概述
- 问题解决
- 总结
- 组合模式的优缺点
- 继承的优缺点
问题提出
考虑这样一个场景,我们有一个敌人的基类,其包含一些公有的属性和方法,比如他有一个Mesh(Static Mesh/Skeletal Mesh),他在世界空间中有一个位置,他可以攻击我们的玩家。然而对于不同类型的敌人,他可能有自己独特的属性和方法,比如有些怪可以给己方加血,有些怪会飞,有些怪会上buff…
一种常见的设计方法是通过继承实现。
从上面的UML图可以看出,EnemyClassA 可以飞,可以上buff,EnemyClassB 可以上 buff,可以治疗队友,EnemyClassC 可以治疗队友,可以飞。想象一下,如果再有一个新的方法,比如可以隐身,那么可能又会产生三个新的基类(考虑两种独有的方法),但其实完全不必要,因为fly,buff,treat明显是重复实现了好多次。
那么有什么办法可以解决这个问题吗。
概述
这时候就需要组合模式闪亮登场了,组合模式通过少继承多组合的方式,可以提高代码复用,且我们想给一个类增加一个新功能,直接给他加一个组件就行了,很方便。
在UE/Unity里也是用了组件,比如UE里的移动组件,网格组件,摄像机组件,一个Actor可以说是一个组件的容器,组件给Actor提供了各种技能。
且组件也支持嵌套,即组件可以包含很多的子组件,比如UE里的USceneComponent,其有一个AttachParent为父节点,还有一个AttachChildren保存多个子节点,明显是一个树形结构。这种设计哲学,感觉更像是这些组件组合起来,成为了一个我们想要的类,而不是像继承一样,通过分类的方式,子类相较于父类多了什么。
这种设计模式也很适合用来设计UI,UI本身也可以看作一个树形结构,每一个组件提供了一些功能,其下也能包含其它的一些小的组件。
问题解决
我们用组合的方式来解决一开始敌人的场景问题,我们可以有飞行组件、治疗组件、加buff组件,那么对于一个具体的敌人,我们只需要添加对应的组件即可。
这个场景比较简单,且有明显的继承关系,个人感觉其实用继承更合适。但其实好多类他们没有继承关系,比如玩家和敌人,没有明显的继承关系,但都可以行走,那么完全可以独立出一个移动组件,而UE本身也是这么做的。
而且组合模式,可以动态地增减节点,对应的就是增加/减少功能,更加灵活(更低的耦合)。比如玩家捡到一把枪,那么玩家就获得了开火射击的技能。枪也相当于组件挂载到玩家这个整体组件的树形结构上。
总结
组合模式的优缺点
优点:
-
灵活性高: 组合模式允许在运行时动态地改变对象的行为,比继承这种在编译时确定的机制更为灵活。
-
减少耦合: 通过组合,类与类之间的关系可以保持松散耦合,有利于系统的可维护性和可扩展性。
-
更好的代码复用: 对象可以通过组合不同的小对象来获得新的功能,而不需要从基类继承所有的实现。这有助于代码模块化和复用。
-
更容易进行单元测试: 由于组合的各个组件是分离的,这些组件可以单独进行测试,简化了测试过程。
-
避免继承层次复杂: 大量使用继承可能导致复杂的继承层次结构,使得系统难以理解和维护。组合模式通过简单组合对象,避免了这个问题。
缺点:
-
复杂性增加: 组合模式可能会导致系统设计复杂化,因为需要管理更多的对象和对象关系。
-
接口定义可能复杂: 为了支持组合,各个组件之间需要定义良好的接口。这些接口的定义和维护可能会比较复杂。
继承的优缺点
优点:
-
直接性: 继承是直观的、简单的,特别是在机制已经被明确时。它允许一个类自动获得另一个类的属性和方法。
-
基础实现共享: 继承允许子类共享从基类继承的代码,因此可以较容易实现代码复用。
-
一致性: 通过继承可以确保子类与父类之间的一致性,子类可以覆盖或扩展父类的方法。
缺点:
-
灵活性较差: 继承的关系在编译时已经确定,因此运行时无法进行动态行为改变。
-
高耦合: 继承会导致子类和父类之间的紧耦合关系,增加了系统变化的难度。如果父类发生变化,可能会影响所有子类。
-
继承层次复杂难以管理: 继承层次过深时,会导致代码维护困难,理解和管理的成本增加。
-
单继承的限制: 在不支持多重继承的语言中,继承会限制一个类只能从一个父类继承,限制了类之间的关系构建。