在本书中,把UML中的关联关系和聚集关系统称为组合关系。组合与继承都是提高代码可重用性的手段。在设计对象模型时,可以按照语义来识别类之间的组合关系和继承关系。在有些情况下,采用组合关系或者继承关系能完成同样的任务,组合和继承存在着对应关系:组合中的整体类和继承中的子类对应,组合中的局部类和继承中的父类对应,参见表6-1。本章6.9节(小结)中的表6-2总结了组合与继承的优缺点。
表6-1 组合与继承的对应关系
组 合 关 系 |
继 承 关 系 |
局部类 |
父类 |
整体类 |
子类 |
从整体类到局部类的分解过程 |
从子类到父类的抽象过程 |
从局部类到整体类的组合过程 |
从父类到子类的扩展过程 |
值得注意的是,本章所说的整体类和局部类比UML的聚集关系中的整体类和局部类具有更广泛的含义。在本章中,如果在类A中包含类C类型的属性,那么就把类A称为整体类或者包装类,把类C称为局部类或者被包装类。
组合关系的分解过程对应继承关系的抽象过程
图6-11 具有相同行为的类A和类B
下面的例子未涉及具体的业务领域,该例子分别用组合关系与继承关系来建立一个对象模型。如图6-11所示,类A和类B有相同方法method1()、method2()和method3(),此外类A和类B还分别拥有methodA()和methodB()方法。在method3()方法中访问method1()方法,在mehodA()和methodB()方法中都会访问method2()方法。以下是类A和类B的源程序。
- public class A{
- private void method1(){System.out.println("method1");}
- private void method2(){System.out.println("method2");}
- public void method3(){method1();System.out.println("method3");}
- public void methodA(){method2();System.out.println("methodA");}
- }
- public class B{
- private void method1(){System.out.println("method1");}
- private void method2(){System.out.println("method2");}
- public void method3(){method1();System.out.println("method3");}
- public void methodB(){method2();System.out.println("methodB");}
- }
图6-12 从类A和类B中抽象出父类C
1.使用继承关系在图6-12中,从类A和类B中抽象出父类C,它包含method1()、method2()和method3()方法。由于在类A和类B中都会访问method2()方法,因此把method2()方法声明为protected类型。
- public class C{
- private void method1(){System.out.println("method1");}
- protected void method2(){System.out.println("method2");}
- public void method3(){method1();System.out.println("method3");}
- }
- public class A extends C{
- public void methodA(){method2(); System.out.println("methodA");}
- }
- public class B extends C{
- public void methodB(){method2(); System.out.println("methodB");}
- }
2.使用组合关系
在图6-13中,类A与类C,以及类B与类C之间为组合关系。在类A中定义了C类型的引用变量c,类A的method3()方法直接调用类C的method3()方法。类A对类C进行了封装,类A被称为包装类,同样,类B也是包装类。由于在类A和类B中都会访问private类型的method2()方法,因此不能把method2()方法放在类C中定义,因为如果这样做,就必须在类C中把method2()方法定义为public类型,而这彻底破坏了封装。以下是类C、类A和类B的源程序。
- public class C{
- private void method1(){System.out.println("method1");}
- public void method3(){method1();System.out.println("method3");}
- }
- public class A {
- private C c;
- public A(C c){this.c=c;}
- private void method2(){System.out.println("method2");}
- public void method3(){c.method3();}
- public void methodA(){method2(); System.out.println("methodA");}
- }
- public class B {
- private C c;
- public B(C c){this.c=c;}
- private void method2(){System.out.println("method2");}
- public void method3(){c.method3();}
- public void methodB(){method2(); System.out.println("methodB");}
- }
图6-13 从类A与类B中分解出局部类C
组合关系和继承关系相比,前者的最主要优势是不会破坏封装,当类A与类C之间为组合关系时,类C封装实现,仅向类A提供接口;而当类A与类C之间为继承关系时,类C会向类A暴露部分实现细节。在软件开发阶段,组合关系虽然不会比继承关系减少编码量,但是到了软件维护阶段,由于组合关系使系统具有较好的松耦合性,因此使得系统更加容易维护。
组合关系的缺点是比继承关系要创建更多的对象。以下程序演示在两种关系下创建类A的实例并且调用其methodA()方法。
- //类A与类C为组合关系
- C c=new C();
- A a=new A(c);
- a.methodA();
- // 类A与类C为继承关系
- A a=new A();
- a.methodA();
从以上程序看出,对于组合关系,创建整体类的实例时,必须创建其所有局部类的实例;而对于继承关系,创建子类的实例时,无须创建父类的实例。继承关系最大的弱点是打破了封装,子类能够访问父类的实现细节,子类与父类之间紧密耦合,子类缺乏独立性,从而影响了子类的可维护性。为了尽可能地克服继承的这一缺陷,应该遵循以下原则:
·精心设计专门用于被继承的类,继承树的抽象层应该比较稳定。
·对于父类中不允许覆盖的方法,采用final修饰符来禁止其被子类覆盖。
·对于不是专门用于被继承的类,禁止其被继承。
·优先考虑用组合关系来提高代码的可重用性。
本章对组合关系和继承关系进行了比较,表6-2对这两种关系的优缺点做了总结。
表6-2 比较组合关系与继承关系
组 合 关 系 |
继 承 关 系 |
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 |
缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 |
优点:具有较好的可扩展性 |
缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 |
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 |
缺点:不支持动态继承。在运行时,子类无法选择不同的父类 |
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 |
缺点:子类不能改变父类的接口 |
缺点:整体类不能自动获得和局部类同样的接口 |
优点:子类能自动继承父类的接口 |
缺点:创建整体类的对象时,需要创建所有局部类的对象 |
优点:创建子类的对象时,无须创建父类的对象 |