合成复用原则又称为组合/聚合复用原则,即Composition/Aggregate Reuse Principle,CARP。
该原则定义如下:
优先使用对象组合而不是通过继承来达到复用的目的。
Favor composition of objects over inheritance as a reuse mechanism
合成复用原则指的是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用功能的目的。简而言之,在复用时要尽量使用组合/聚合关系(关联关系),而尽量少使用继承。
在面向对象设计中可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承。在使用继承时需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的某些内部细节对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变。
由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分。因此新对象可以调用已有对象的功能,这样做可以使成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作。
一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”的关系可以使用继承。“Is-A”是严格的分类学意义上的定义,意思是一个类是另一个类的“一种”;而“Has-A”则不同,它表示某一个角色具有某一项责任。
在此,以案例形式介绍合成复用原则。
在项目技术选型中采用MySQL数据库存储数据,并利用MySQL数据库连接对用户进行添加操作。
package com.carp01;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class DBConnection {
public String getConnection() {
return "获取MySQL数据库连接";
}
}
package com.carp01;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class UserDao extends DBConnection {
public void addUser() {
String connection = super.getConnection();
System.out.println(connection);
System.out.println("执行数据库操作");
// ......省略后续操作......
}
}
package com.carp01;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class Test {
public static void main(String[] args) {
UserDao userDao = new UserDao();
userDao.addUser();
}
}
在该示例中UserDao继承自DBConnection,getConnection( )方法只能返回MySQL数据库的连接。但是,随着业务的扩展功能增加了;又新增了Oracle数据库。此时,原本的UserDao就不能满足需求了。虽然,我们可在DBConnection中再新增一个方法用于获取 Oracle 数据库的连接;但是这样显然违反了开闭原则。
我们利用合成复用原则对示例代码进行重构。将数据库连接类设计为抽象类,其内部包含一个获取数据库连接的抽象方法;至于获取哪种数据库的连接则交于具体的子类去实现。另外,我们在UserDao中持有DBConnection类型的引用。
package com.carp02;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public abstract class DBConnection {
public abstract String getConnection();
}
package com.carp02;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "获取MySQL数据库连接";
}
}
package com.carp02;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class OracleConnection extends DBConnection {
@Override
public String getConnection() {
return "获取Oracle数据库连接";
}
}
package com.carp02;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class UserDao {
// 持有DBConnection类型的引用
private DBConnection connection;
public void setConnection(DBConnection connection) {
this.connection = connection;
}
public void addUser() {
System.out.println(connection);
System.out.println("执行数据库操作");
// ......省略后续操作......
}
}
package com.carp02;
/**
* 原创作者:谷哥的小弟
* 博客地址:http://blog.csdn.net/lfdfhl
*/
public class Test {
public static void main(String[] args) {
UserDao userDao = new UserDao();
MySQLConnection mySQLConnection = new MySQLConnection();
userDao.setConnection(mySQLConnection);
userDao.addUser();
OracleConnection oracleConnection = new OracleConnection();
userDao.setConnection(oracleConnection);
userDao.addUser();
}
}
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多;图示如下:
从上面类图我们可以看到:使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。在此,我们试着将继承复用改为聚合复用;图示如下: