19.1 面向对象的编程和继承
面向对象的编程是过去30-40年里软件开发中最重要的新思想之一。它引入了诸如类、继承、私有方法和实例变量等概念。如果谨慎地使用,这些机制可以帮助产生更好的软件设计。例如,私有方法和变量可以用来确保信息隐藏:类之外的任何代码都不能调用私有方法或访问私有变量,因此它们不可能被任何外部的东西依赖。
面向对象编程的关键因素之一是继承。继承有两种形式,它们对软件的复杂性有不同的影响。继承的第一种形式是接口继承,其中父类定义了一个或多个方法的签名,但不实现这些方法。每个子类必须实现这些签名,但不同的子类可以用不同的方式实现相同的方法。例如,接口可以定义执行I/O的方法;一个子类可以实现磁盘文件的I/O操作,另一个子类可以实现网络套接字的相同操作。
接口继承通过将同一个接口用于多种用途,提供了对抗复杂性的手段。它允许在解决一个问题(如如何使用I/O接口读写磁盘文件)时获得的知识被用来解决其他问题(如通过网络套接字进行通信)。另一种思考方式是深度:一个接口的不同实现越多,接口就越深。为了让一个接口有很多实现,它必须抓住所有底层实现的基本特征,同时避开不同实现之间的细节;这个概念是抽象的核心所在。
继承的第二种形式是实现继承。在这种形式中,父类不仅定义了一个或多个方法的签名,而且还定义了默认的实现。子类可以选择继承父类对某个方法的实现,或者通过定义一个具有相同签名的新方法来覆盖它。如果没有实现继承,同一个方法的实现可能需要在几个子类中重复,这将在这些子类之间产生依赖性(修改需要在该方法的所有副本中重复)。因此,实现继承减少了系统发展过程中需要修改的代码量;换句话说,它减少了第二章中描述的变更放大问题。
然而,实现继承在父类和其每个子类之间产生了依赖性。父类中的类实例变量经常被父类和子类访问;这导致了继承层次中的类之间的信息泄露,并且很难在不看其他类的情况下修改继承层次中的一个类。例如,一个开发者在对父类进行修改时,可能需要检查所有的子类,以确保这些修改不会破坏任何东西。同样地,如果一个子类重写了父类中的一个方法,子类的开发者可能需要检查父类中的实现。在最坏的情况下,程序员需要完全了解父类下面的整个类层次结构,以便对任何一个类进行修改。广泛使用实现继承的类层次结构往往具有很高的复杂性。
因此,应该谨慎地使用实现继承。在使用实现继承之前,要考虑基于组合的方法是否能提供同样的好处。例如,可以使用小型辅助类来实现共享功能。与其从父类那里继承功能,原始类可以各自建立在辅助类的功能上。
如果在实现继承之外没有可行的替代方案,那么尽量将父类管理的状态与子类管理的状态分开。一种方法是让某些实例变量完全由父类的方法来管理,而子类只能以只读的方式或通过父类的其他方法来使用它们。这就是在类的层次结构中应用信息隐藏的概念来减少依赖性。
尽管面向对象编程所提供的机制可以帮助实现干净的设计,但它们本身并不能保证良好的设计。例如,如果类是浅的,或者有复杂的接口,或者允许外部访问其内部状态,那么它们仍然会导致高复杂性。
Last updated