聚合,组合与IoC

为什么我们说多用组合,少用继承?


继承具有强耦合(strog coupling)特点。根据里氏替换原则(Liskov Substitution Principle LSP),基类出现的地方,子类一定能透明地替代。本质上来讲,LSP的这种特性致使基类具有很强的病毒蔓延性(invasive),因为如何基类设计的不好,会侵蚀所有的子类。举个例子,基类是鸟,同时具有fly这样的方法,子类中如果有鸵鸟或企鹅,就必须继承fly方法,然而这显然是没有意义的。

继承因为强耦合,所以不好测试。举个例子,有一个RemoteXMLFile<Res>基类,它内部有个Res to()方法,旨在把远程的XML文件转换成具体的Response。在我们准备测试to方法的转换逻辑的时候,每个对应子类的测试类里都不得不准备丑陋冗余的XML字面值字符串,尤其是在Java里。之所以这样,是因为无法隔离出一个专门读取XML的抽象屏障,然后使用Mock隔离这层抽象。

组合的好处自然是松耦合(loose coupling)。如果严格遵循依赖倒置原则(Dependency Inversion Principle),那么类本身一定是高能聚,类之间是松耦合的。除此之外,测试起来,各层之间的逻辑清晰明了,易于隔离开来做单元测试(Unit test)。

谈谈聚合和组合的区别


UML里有很多表示关联的线段,有两个关系十分相近,它们是聚合(aggregation)和组合(composition)

聚合:表示两个对象之间是整体和部分的弱(弱拥有)关系,部分的生命周期可以超越整体。如电脑和鼠标。

组合:表示两个对象之间是整体和部分的强(强拥有)关系,部分的生命周期不能超越整体,或者说不能脱离整体而存在。组合关系的“部分”,是不能在整体之间进行共享的。

以上是网络上的标准解释,应该是没错。但是实践中依然让人困惑不已。汽车和引擎,这个还比较好理解,汽车报废了,引擎还是可以换给别的汽车用的嘛。这就实现了整体之间的共享——聚合无疑。但是值得注意的一点是:从未说过部分的生命周期小于整体,也就是一定是大于或者等于整体的生命周期的。所以千万别和别人争论,引擎报废了,汽车就没用了,因为在程序中一定是某个第三方供应商维护引擎的生命周期,所以引擎和汽车没有相互维持对方生命周期的联系。

但是比较困惑就是这个生命周期理论!IoC容器提供给我们使用的注入方法无非3种,接口注入、构造函数注入、setter方法注入。每一种无不是聚合,因为产生和管理对象的控制权移交到IoC容器了。那我们口口声声的说组合优于继承其实一直都是聚合优于继承。

那是不是说,有了IoC之后,组合就应该消失呢,当然不是的。例如,某个场景里,一个类确实需要分离出一部分职责交给另外一个类,比如Builder,这部分构建实例的职责就可以通过new Builder()的方式组合应用起来。

Builder_Pattern
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
public class Person {
private String name;
private int age;

public static builder() {
return new Builder();
}

public static Builder {
private String name;
private int age;

public Builder withName(String name) {
this.name = name;
}

public Builder withAge(int age) {
this.age = age;
}

public Person build(){
Person person = new Person();
person.name = name;
person.age = age;
return person;
}
}
}
>>>
Person person = Person.builder().withName('Ryan').withAge(18).build();

结论


  • 组合优于继承
  • 因为IoC的存在,上句话得改成聚合优于继承