合成复用原则:类之间尽量使用合成/集合的方式,其次是使用继承
含义
-
合成复用原则(
Composite/Aggregate Reuse Principle
,CARP
)是指尽量使用对象组合(has-a)、聚合(contanis-a) ,而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少 -
继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合也称之为黑箱复用,对类以外的对象是无法获取到实现细节的
核心思想
- 优先使用聚合、组合的方式,而不是使用继承
实现方式
合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用
复用的基本方式
通常类的复用可分为继承复用(使用继承实现代码的复用性)和合成复用(使用组合或者聚合实现代码的复用性)两种
-
组合/聚合(合成复用)
-
聚合与组合的区别:
- 聚合是用来表示拥有关系或者整体与部分的关系
- 组合则是表示一种强得多的拥有关系,在组合里,部分与整体的生命周期是一样的。一个组合的新的对象完全拥有对其组成部分的支配权,包括它们的创建和销毁等。一个组合关系中的成分对象是不能与另一个组合关系共享的。一个组成部分在同一个时间内只能属于一个组合关系
-
-
继承(继承复用)
复用两种方式的区别
-
组合/聚合:是将已有的对象纳入到新对象中,使之成为新对象的一部分
-
优点
- 对象间的耦合度低,组合或者聚合本身就比继承的耦合度低。当我们真正去使用组合或者聚合复用时,我们可以在类的成员位置声明抽象父类或者父接口,这样,我们就能动态地去传递该抽象类或者父接口的子类对象了
- 维持了类的封装性,因为成员对象的内部细节是对新对象不可见的,也就是说新对象不知道成员对象里面的具体的实现,但是可以调用其功能,所以这种复用又被称为黑箱复用
- 复用的灵活性高,这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象,也就是说如果我们要给成员变量进行赋值,那么我们就可以在程序运行的时候才对其进行赋值了。若成员位置声明的是抽象的类或者接口,则我们就可以传递该接口或者该类的子类对象了
-
缺点
- 通过使用这种方式复用建造的系统会有较多的对象需要管理
-
-
继承:是面向对象特有的复用工具,而且也最容易被滥用。继承复用通过扩展一个已有对象的实现来得到新的功能
-
优点
- 简单、易实现,因为父类的大部分功能都可以通过继承关系自动进入子类、且修改或扩展继承而来的实现也较为容易
-
缺点
- 破坏了类的封装性,因为继承会将父类的实现细节暴露给子类,也就是说子类可以直接去继承父类中的功能,这样,子类就可以将父类中的功能给覆盖掉了,所以,父类对子类是透明的。其实,这种复用我们又可称为白箱复用
- 子类与父类的耦合度高,父类的实现的任何改变都会导致子类的实现发生变化,修改父类就会形成连锁反应,这不利于类的扩展与维护
- 限制了复用的灵活性,从父类继承而来的实现是静态的,这是因为在编译时就已经定义好了,所以在运行时是不可能发生变化的。我们总不能说,在程序运行过程中,来解除子类和父类的继承关系吧!况且,这也是无法实现的,所以我们才说继承限制了复用的灵活性
-
说明:如果使用继承,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
-
案例
汽车分类
汽车按照动力源可分为电动车、汽油车…;按颜色可以分为黑色、白色、红色…;同时考虑上面两种方式,可组合的汽车就有很多了
如果使用继承复用的话,那么设计出来的UML类图就是
从以上类图中可以看到,首先定义了一个顶层父类,即汽车类,而该类下面又有两个子类,一个是汽油汽车类,一个是电动汽车类。汽油汽车类下面又有三个子类,黑、红、白颜色的汽油汽车类;电动汽车类也是一样,黑、红、白颜色的电动汽车类。
使用继承复用产生了很多子类。如果现在又有一个新的动力源了,例如氢能汽车,那么我们又要在以上类图中添加一个新的子类,而且,我们还得添加更多的子类,因为氢能汽车也得按颜色来划分。
代码
public abstract class Car {
public abstract void run();
}
// 汽油、氢能都类似
public class ElectricCar extends Car {
@Override
public void run() {
System.out.println(" - 电动车");
//System.out.println(" - 汽油车");
}
}
// 红、黑、白色电动车类似
public class RedElectricCar extends ElectricCar {
public void go() {
System.out.print("红色");
run();
}
}
// 红、黑、白色汽油车类似
public class RedPetrolCar extends PetrolCar {
public void go() {
System.out.print("红色");
run();
}
}
Main
public static void main(String[] args) {
BlackPetrolCar blackPetrolCar = new BlackPetrolCar();
blackPetrolCar.go();
}
案例改进:
继承复用改为合成复用
首先,来看上面类图中右边的部分,在这一部分,我们定义了一个颜色接口,而且该接口有三个子实现类,一个是Black类、White类、Red类;在以上类图中左边的部分,我们定义了一个顶层父类,即汽车类,而且该类还组合进了Color接口,也就是说,到时候我们在去创建汽车对象的时候,就需要给它传递具体的颜色的子类对象了。此外,该汽车类还有两个子类,一个是汽油汽车类,一个是电动汽车类。
代码
public interface Color {
void colorKind();
}
public abstract class Car {
public abstract void run();
Color color;
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
}
public class ElectricCar extends Car {
@Override
public void run() {
System.out.println(" - 电动车");
}
}
public class White implements Color{
@Override
public void colorKind() {
System.out.println("白色");
}
}
Main
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
White white = new White();
electricCar.setColor(white);
electricCar.getColor().colorKind();//白色
electricCar.run();//电动汽车
}
如果要是现在多了一种动力源,例如氢能汽车,UML类图就是下面这样
只需要在汽车类下新增子类即可
案例二
假设有A、B两个类。B类需要复用A类的方法
通过合成复用原则来实现上述案例,不再展示继承复用
方式一
使用简单依赖。在B类中添加方法,方法入参使用A类
// 测试类
public class Test {
public static void main(String[] args) {
B b = new B();
b.methodB(new A());
}
}
// A类
class A {
public void methodA() {
System.out.println("A类的方法执行了。");
}
}
// B类
class B {
public void methodB(A a) {
System.out.println("B类的方法执行了。");
a.methodA();
}
}
方式二
把A类聚合到B类中。在B类中添加属性,类型为A类,通过set方法进行初始化
// 测试类
public class Test2 {
public static void main(String[] args) {
B b = new B();
System.out.println("使用聚合的执行结果:");
b.setA(new A());
b.methodB();
}
}
// A类
class A {
public void methodA() {
System.out.println("A类的方法执行了。");
}
}
// B类
class B {
private A a;
public A getA() {
return a;
}
public void setA(A a) {
this.a = a;
}
public void methodB() {
System.out.println("B类的方法执行了。");
this.a.methodA();
}
}
方式三
把A类组合到B类中。在B类中添加属性,类型为A类,并直接new出来
public class Test3 {
public static void main(String[] args) {
B b = new B();
System.out.println("使用组合的执行结果:");
b.methodB();
}
}
class A {
public void methodA() {
System.out.println("A类的方法执行了。");
}
}
class B {
private A a = new A();
public void methodB() {
System.out.println("B类的方法执行了。");
this.a.methodA();
}
}