继承是一种提高程序代码的可重用性、以及提高系统的可扩展性的有效手段。但是,如果继承树非常复杂、或者随便扩展本来不是专门为继承而设计的类,反而会削弱系统的可扩展性和可维护性。
1. 继承树的层次不可太多
继承树(不考虑最顶层的Object类)的层次应该尽量保持在两到三层。如果继承树的层次很多,会导致以下弊端:
(1)对象模型的结构太复杂,难以理解,增加了设计和开发的难度。在继承树最底层的子类会继承上层所有直接父类或间接父类的方法和属性,假如子类和父类之间还有频繁的方法覆盖和属性被屏蔽的现象,那么会增加运用多态机制的难度,难以预计在运行时方法和属性到底和哪个类绑定。
(2)影响系统的可扩展性。继承树的层次越多,在继承树上增加一个新的继承分支需要创建的类越多。
2. 继承树的上层为抽象层
当一个系统使用一棵继承树上的类时,应该尽可能地把引用变量声明为继承树的上层类型,这可以提高两个系统之间的松耦合。例如动物饲养员Feeder的feed()方法,它的参数为Animal和Food类型:
feed(Animal animal,Food food)
如果继承树上有接口类型,那么应该尽可能地把引用变量声明为继承树上层的接口类型。
位于继承树上层的类具有以下作用:
(1)定义了下层子类都拥有的相同属性和方法,并且尽可能地为多数方法提供默认的实现,从而提高程序代码的可重用性。
(2)代表系统的接口,描述系统所能提供的服务。
在设计继承树时,首先进行自下而上的抽象,即识别子类之间所拥有的共同属性和功能,然后抽象出共同的父类,位于继承树最上层的父类描述系统对外提供哪些服务。如果某种服务的实现方式适用于所有子类或者大多数子类,那么在父类中就实现这种服务。如果某种服务的实现方式取决于各个子类的特定属性和实现细节,那么在父类中无法实现这种服务,只能把代表这种服务的方法定义为抽象方法,并且把父类定义为抽象类。
例如热水器父类可分为电热水器和燃气热水器这两个子类,电热水器和燃气热水器采用不同的方式烧水,在热水器父类中无法提供烧水的具体实现,因此必须把热水器父类定义为抽象类:
public abstract class WaterHeating{
/** 烧水 */
public abstract void heating();
/** 调节水温 */
public abstract void adjust(int level);
}
由于继承树上层的父类描述系统对外提供的服务,但不一定实现这种服务,因此把继承树的上层称为抽象层。在进行对象模型设计时,应该充分地预计系统现在必须具备的功能,以及将来需要新增的功能,然后在抽象层中声明它们。抽象层应该比较稳定,这可以提高与其他系统的松耦合,以及系统本身的可维护性。
(3)继承关系最大的弱点:打破封装
继承关系最大的弱点就是打破了封装。每个类都应该封装它的属性以及实现细节,这样,当这个类的实现细节发生变化时,不会对其他依赖它的类造成影响。而在继承关系中,子类能够访问父类的属性和方法,也就是说,子类会访问父类的实现细节,子类与父类之间是紧密耦合关系,当父类的实现发生变化,子类的实现也不得不随之变化,这削弱了子类的独立性。
由于继承关系会打破封装,这增加了维护软件的工作量。尤其是在一个Java软件系统还使用了一个第三方提供的Java类库的场合。例如在基于Web的Java应用中,曾经流行使用Apache开源软件组织提供的Struts框架,这个框架的一个扩展点为Action类,在Struts的低版本中,Action类有以下两个方法perform()和saveErrors():
public ActionForward perform(
ActionMapping mapping,
ActionForm form,
ServletRequest request,
ServletResponse response)throws Exception
protected void saveErrors(HttpServletRequest request, ActionErrors errors)
在Java应用中,可以创建继承Action类的子类,例如LoginAction,然后在LoginAction类中覆盖Action类的perform()方法,在perform()方法中则会调用saveErrors()方法:
public class LoginAction extends Action{
public ActionForward perform(…){ //覆盖Action类的perform方法
ActionErrors errors=new ActionErrors();
……
saveErrors(request ,errors); //调用Action类的saveErrors()方法
}
}
而在Struts的升级版本中,Action类的perform()方法改名为execute()方法,并且saveErrors(HttpServletRequest request, ActionErrors errors)改为:
saveErrors(HttpServletRequest request, ActionMessages errors)
假如现有的Java应用希望改为使用Struts的升级版本,就必须对自定义的所有Action子类进行修改:
public class LoginAction extends Action{
public ActionForward execute(…){ //把perform()方法改为execute()方法
ActionMessages errors=new ActionMessages(); //把ActionErrors改为ActionMessages
……
saveErrors(request ,errors);
}
}
从以上例子可以看出,当由第三方提供的Struts框架的Action类做了修改,软件系统中所有Action的子类也要做相应地修改。
由于继承关系会打破封装,还会导致父类的实现细节被子类恶意篡改的危险。例如以下Account类的withdraw()方法和save()方法分别用于取款和存款:
public class Account{
protected double balance; //余额
protected boolean isEnough(double money){
return balance>=money;
}
public void withdraw(double money)throws Exception{ //取款
if(isEnough(money)) balance-= money;
else throw new Exception("余额不足!");
}
public void save(double money)throws Exception{ //存款
balance+=money;
}
}
它的子类SubAccount覆盖了Account类的isEnough()方法和save()方法的实现,使得该账户允许无限制的取款,并且按照实际存款数额的10倍来存款:
public class SubAccount extends Account{
protected boolean isEnough(double money){ //覆盖父类的isEnough()方法
return true;
}
public void save(double money)throws Exception{ //覆盖父类的save()方法
balance+=money*10;
}
}
以下程序定义了Account类型的引用变量account,实际引用SubAccount实例,根据Java虚拟机的动态绑定规则,account.save()和account.withdraw()方法会和SubAccount实例的相应方法绑定:
Account account=new SubAccount();
account.withdraw(2000); //调用SubAccount实例的withdraw()方法
account.save(100); //调用SubAccount实例的save()方法
4.精心设计专门用于被继承的类
由于继承关系会打破封装,因此随意继承对象模型中的任意一个类是不安全的做法。在建立对象模型时,应该先充分考虑软件系统中哪些地方需要扩展,为这些地方提供扩展点,也就是提供一些专门用于被继承的类。对这种专门用于被继承的类必须精心设计,以下给出一些建议:
(1)对这些类必须提供良好的文档说明,使得创建该类的子类的开发人员知道如何正确安全地扩展它。对于那些允许子类覆盖的方法,应该详细地描述该方法的自用性,以及子类覆盖此方法可能带来的影响。所谓方法的自用性,是指在这个类中,有其他的方法会调用这个方法。例如Account类的isEnough()方法,会被save()方法调用,因此子类覆盖isEnough()方法,还会影响到save()方法。
(2)尽可能地封装父类的实现细节,也就是把代表实现细节的属性和方法定义为private类型。如果某些实现细节必须被子类访问,可以在父类中把包含这种实现细节的方法定义为protected类型。当子类仅调用父类的protected类型的方法,而不覆盖它时,可把这种protected类型的方法看作是父类仅向子类但不对外部公开的接口。例如手机的存储容量,用户可以察看存储容量,但不能修改它,手机的子类可以察看存储容量,也可以修改它。因此在手机CellPhone父类中定义了如下存储容量storage属性以及相应的访问方法。
public class CellPhone{
private double storage;
public double getStorage(){return storage;} //对手机使用者以及手机子类公开
protected void setStorage(double storage){this.storage=storage;} //只对手机子类公开
…
}
(3)把不允许子类覆盖的方法定义为final类型。对于Account类,可以把它的isEnough()、withdraw()和save()方法都定义为final类型:
public class Account{
private double balance; //余额
protected final boolean isEnough(double money){
return balance>=money;
}
public final void withdraw(double money)throws Exception{ //取款
if(isEnough(money)) balance-= money;
else throw new Exception("余额不足!");
}
public final void save(double money)throws Exception{ //存款
balance+=money;
}
}
(4)父类的构造方法不允许调用可被子类覆盖的方法,因为如果这样做,可能会导致程序运行时出现未预料的错误。例如以下Base类的构造方法调用自身的method()方法:
public class Base{
public Base(){ method();}
public void method(){}
}
public class Sub extends Base{
private String str=null;
public Sub(){str="1234";}
public void method(){System.out.println(str.length());} //覆盖Base类的method()方法
public static void main(String args[]){
Sub sub=new Sub(); //抛出NullPointerException
sub.method();
}
}
运行Sub类的main()方法时,先构造Sub类的实例。由于在创建子类的实例时,Java虚拟机先调用父类的构造方法,因此Java虚拟机先执行Base类的构造方法Base(),在这个方法中调用method()方法,根据动态绑定规则,Java虚拟机调用Sub实例的method()方法,由于此时Sub实例的成员变量str为null,因此在执行str.length()方法时会抛出NullPointerException运行时异常。
(5)如果某些类不是专门为了继承而设计,那么随意继承它是不安全的。因此可以采取以下两种措施来禁止继承:
a.把类声明为final类型。
b.把这个类的所有构造方法声明为private类型,然后通过一些静态方法来负责构造自身的实例。
上文参考孙卫琴的经典Java书籍《Java面向对象编程》
标签:Java,继承,子类,利弊,money,父类,方法,public From: https://blog.51cto.com/sunweiqin/7873752