软件修养 -- 组合/聚合复用原则(CARP:Composite/Aggregate Reuse Principle)

定义

组合/聚合复用原则(CARP:Composite/Aggregate Reuse Principle):在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的

简短表达就是:尽量使用组合/聚合,尽量不要使用继承

复用方式

在面向对象的设计里,有两种基本的方法可以在不同的环境中复用已有的设计和实现,即通过组合继承

组合

由于组合可以将已有的对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做有下面的好处:

  • 新对象存取成分对象的唯一方法是通过成分对象的接口
  • 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的
  • 这种复用所需的依赖较少
  • 每一个新的类可以将焦点集中在一个任务上
  • 这种复用可以在运行时间内动态进行,作为整体的新对象可以动态地引用与部分对象类型相同的对象。也就是说,组合/聚合是动态行为,即运行时行为。可以通过使用组合/聚合的方式在设计上获得更高的灵活性

组合复用的缺点就是用组合复用建造的系统会有较多的对象需要管理

继承

组合几乎可以用到任何环境中去,但是继承只能用到一些环境中

继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显的捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。

继承的优点:

  • 新的实现比较容易,因为基类的大部分功能都可以通过继承自动的进入子类。
  • 修改或扩展继承而来的实现较为容易。

继承的缺点:

  • 继承复用破坏了包装,因为继承超类的的实现细节暴露给子类。由于超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又称“白箱”复用。
  • 如果超类的实现发生改变,那么子类的实现也不得不发生改变。因此,当一个基类发生改变时,这种改变就会像水中投入石子引起的水波一样,将变化一圈又一圈的传导到一级又一级的子类,使设计师不得不相应地改变这些子类,以适应超类的变化。
  • 从超类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性

思考

  • 使用组合还是继承?

    满足 “Is-A” 的关系是才可以使用继承,而组合却是一种 “Has-A”(整体与部分)的关系

案例

雇员、销售员、经理,这三者可以设计为人是基类、雇员和经理都继承于人,代码如下:

1
2
3
4
5
class Employee {
  name: String;
}
class Manager extends Employee {}
class Sales extends Employee {}

这种设计的错误在于把角色的等级结构和雇员等级结构混淆了。经理、销售员是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是销售员的话,就不可能是经理,这显然不合理。

正确的设计是有个抽象类角色,人可以拥有多个角色(聚合),销售员、经理是角色的子类,代码如下:

1
2
3
4
5
6
7
8
class Employee {
  name: String;
  roles: [Role];
}

class Role {}
class Manager extends Role {}
class Sales extends Role {}

这样做降低了类与类之间的耦合度,Employee 类的变化对其它类造成的影响相对较少。

从这个例子可以看出,当一个类是另一个类的角色时,不应该使用继承描述这种关系

注意点

  • 首选组合,然后才是继承

    按照组合复用原则应该首选组合,然后才是继承,使用继承时应该严格的遵守里氏替换原则必须满足 “Is-A” 的关系是才能使用继承。

延伸阅读


CatchZeng
Written by CatchZeng Follow
AI (Machine Learning) and DevOps enthusiast.