合成/聚合的理解

聚合(Aggregation)表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象但B对象不是A对象的一部分。比如:大雁是群居动物,每个大雁都是属于一个雁群,一个雁群可以有多只大雁,所以大雁和雁群是聚合关系。

合成(Composition)则是一种强的’拥有’关系,体现了严格的部分和整体关系,部分和整体的生命周期一样。比如:大雁有两个翅膀,翅膀和大雁是部分和整体的关系,并且他们有相同的生命周期,所以大雁和翅膀是合成关系。

定义

合成/聚合复用原则或者合成复用原则(Composition/Aggregate Reuse Principle,CARP),在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。

复用时要尽量使用组合/聚合关系(关联关系),少用继承

实现方式

方式一:组合复用

组合可以将已有的对象纳入到新的对象中,使之成为新对象的一部分,因此新对象就可以调用已有对象的功能。

1、优点:

  • 新对象存取成分对象的唯一方法是通过成分对象的接口。
  • 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
  • 这种复用支持包装。
  • 这种复用所需要的依赖较少。
  • 每一个新的类可以将焦点集中到一个任务上。
  • 这种复用可以在运行时间动态进行,新对象可以使用合成/聚合关系将新的责任委派到合适的对象。

2、缺点:

用组合复用建造的系统会有较多的对象需要管理。

方式二:继承复用

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

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

1、优点:

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

2、缺点:

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

如何选择复用方式,继承还是组合?

在复用时,应优先考虑使用组合而不是继承,判定的条件有以下:

  1. 子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has-A”和“Is-A”。满足”Is-A”的关系才能继承,而组合却是“Has-A”的关系。
  2. 永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。
    3.子类具有扩展超类的责任,而不是具有置换调(override)或注销掉(Nullify)超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。
  3. 只有在分类学角度有意义时,才可以使用继承。不要从工具类继承。

Is-A顾名思义,就是“是一个”,意思是一个类是另一个类的一种;Has-A是“有一个”,意思是一个类是另一个类的一部分。错误的使用继承而不是使用组合,就是把“Has-A”当做“Is-A”。

下面看一个例子:

人被继承到雇员,学生,经理子类。而实际上,雇员、学生和经理分别描述一种角色,而人可以同时有几种不同的角色。比如,一个人既然是经理了就一定是雇员,使用继承来实现角色,则只能使用每一个人具有一种角色,这显然是不合理的。错误的原因就是把角色的等级结构和人的等级结构混淆起来,把Has-A的关系误认为是Is-A的关系,通过下面的改正就可以正确的做到这一点。

从上图可以看出,每一个人都可以有一个以上的角色,所以一个人可以同时是雇员又是经理。从这个例子可以看出,当一个类是另一个类的角色时,不应该使用继承描述这种关系。

应用举例

我们每个人都有一个身份,比如我是老师,你是学生,他是运动员。如果要为这一的情况设计,可能会出现下面的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Person
{
public Person()
{
}
}
//老师的继承
class Teacher extends Person
{
public Teacher()
{
Syetem.out.printIn("I am a teacher");
}
}
//学生的继承
class Student extends Person
{
public Student()
{
System.out.printIn("I am a student");
}
}
//运动员的继承
class Athlete extends Person
{
public Athlete()
{
System.out.printIn("I am a athlete");
}
}

假如这样设计有什么问题呢?事实上,现实生活中我们不只有一种身份,可能多种身份,比如我既是一个学生又是一名运动员,这并不矛盾。但是,如果按照上面的设计,就忽略了这个因素,导致每个人只有一种身份,这显然是不合理的。

下面是我们遵循合成复用原则重新设计的,由聚合关系变成组合关系,一个人现在可以有多重角色,在不同的情况下,我们可以给人物设置不同的角色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Person
{
privite Role role;
public Person()
{
}
public void setRole(Role role)
{
this.role = role;
}
public Role getRole()
{
return role;
}
}
interface Role
{
}
class Teacher implements Role
{
public Teacher()
{
System.out.println("Iam a teacher");
}
}
class Student implements Role
{
public Student()
{
System.out.println("Iam a student");
}
}

class Athlete implements Role
{
public Athlete()
{
System.out.println("Iam a athlete");
}
}

作者:Diandian