面向抽象编程和面向接口编程
原创
以下内容来自《Java 2实用教程》,主编:耿祥义、张跃平
鉴于面向抽象编程和面向接口编程思维培养的重要性,写此博客巩固。
面向抽象编程:
在设计程序时,经常会使用到abstract类,其原因是,abstract类只关心操作,而不关心这些操作具体的实现细节,
可以使程序的设计者把主要精力放在程序的设计上,而不必拘泥于细节的实现(将这些细节留给子类的设计者),即避免
设计者把大量的时间和精力花费在具体的算法上。例如,在设计地图时,首先考虑地图最重要的轮廓,不必去考虑诸如城
市中的街道牌号等细节,细节应当由抽象类的非抽象子类去实现,这些子类可以给出具体的实例,来完成程序功能的具体
实现。在设计一个程序时,可以通过在abstract类中声明若干个abstract方法,表明这些方法在整个系统设计中的重要性,
方法体的内容细节由它的非abstract子类去完成。
使用多态进行程序设计的核心技术之一是使用上转型对象,即将abstract类声明的对象作为其子类对象的上转型对象,
那么这个上转型对象就可以调用子类重写的方法。
所谓面向抽象编程,是指当设计某种重要的类时,不让该类面向具体的类,而是面向抽象类,即所设计类中的重要数据
是抽象类声明的对象,而不是具体类的声明的对象。
以下通过一个简单的问题来说明面向抽象编程的思想。
例如,我们已经有了一个Circle类(圆类),该类创建的对象Circle调用getArea()方法可以计算圆的面积。Circle类
的代码如下:
Circle.java
1 public class Circle { 2 double r; 3 Circle(double r){ 4 this.r=r; 5 } 6 public double getArea() { 7 return(3.14*r*r); 8 } 9 }
现在要设计一个Pillar类(柱类),该类的对象调用getVolume()方法可以计算柱体的体积。Pillar类的代码如下:
Pillar.java
1 public class Pillar { 2 Circle bottom; //bottom是用具体类Circle声明的对象 3 double height; 4 Pillar (Circle bottom,double height){ 5 this.bottom=bottom; 6 this.height=height; 7 } 8 public double getVolume() { 9 return bottom.getArea()*height; 10 } 11 }
上述Pillar类中,bottom是用具体类Circle声明的对象,如果不涉及用户需求的变化,上面Pillar类的设计没有什么不妥,
但是在某个时候,用户希望Pillar类能创建出底是三角形的柱体。显然上述Pillar类无法创建出这样的柱体,即上述设计的Pillar
类不能应对用户的这种需求(软件设计面临的最大问题是用户需求的变化)。我们发现,用户需求的柱体的底无论是何种图形,但
有一点是相同的,即要求该图形必须有计算面积的行为,因此可以用一个抽象类封装这个行为标准:在抽象类里定义一个抽象方法
abstract double getArea(),即用抽象类封装许多子类都必有的行为。
现在我们来重新设计Pillar类。首先,我们注意到柱体计算体积的关键在计算出底面积,一个柱体在计算底面积时不应该关心
它的底是什么形状的具体图形,只应该关心这种图形是否具有计算面积的方法。因此,在设计Pillar类时不应该让它的底是某个具体
类声明的对象,一旦这样做,Pillar类就依赖该具体类,缺乏弹性,难以应对需求的变化。
下面我们将面对抽象重新设计Pillar类。首先编写一个抽象类Geometry,该抽象类中定义了一个抽象的getArea()方法。
Geometry类如下:
Geometry.java
1 public abstract class Geometry { 2 public abstract double getArea(); 3 }
上述抽象类将所有计算面积的算法抽象为一个标识:getArea(),即抽象方法,不用考虑算法的细节。
现在Pillar类的设计者可以面向Geometry类编写代码,即Pillar类应该把Geometry对象作为自己的成员,该成员可以调用Geometry
的子类重写的getArea()方法。这样一来,Pillar类就可以将计算底面积的任务指派给Geometry类的子类的实例(用户的各种需求将由
不同的子类去负责)。
以下Pillar类的设计不再依赖具体类,而是面向Geometry类,即Pillar类中的bottom是用抽象类Geometry声明的对象,而不是具体类
声明的对象。重新设计的Pillar类的代码如下:
Pillar.java
1 public class Pillar { 2 Geometry bottom; //bottom是抽象类Geometry声明的变量 3 double height; 4 Pillar (Geometry bottom,double height){ 5 this.bottom=bottom; 6 this.height=height; 7 } 8 public double getVolume() { 9 if(bottom==null) { 10 System.out.println("没有底,无法计算体积"); 11 return -1; 12 } 13 return bottom.getArea()*height; //bottom可以调用子类重写的getArea方法 14 } 15 }
下列Circle和Rectangle类都是Geometry的子类,二者都必须重写Geometry类和getArea()方法来计算各自的面积。
Circle.java
1 public class Circle extends Geometry{ 2 double r; 3 Circle(double r){ 4 this.r=r; 5 } 6 public double getArea() { 7 return(3.14*r*r); 8 } 9 }
Rectangle.java
1 public class Rectangle { 2 double a,b; 3 Rectangle(double a,double b){ 4 this.a=a; 5 this.b=b; 6 } 7 public double getArea() { 8 return a*b; 9 } 10 }
注意到,当增加了Circle和Recangle类后,我们不必修改Pillar类的代码。现在,我们就可以用Pillar类创建
出具有矩形底或圆形底的柱体了,如下列Application.java所示,程序运行效果如图5.13所示。
Application.java
1 public class Application { 2 public static void main(String args[]) { 3 Pillar pillar; 4 Geometry bottom=null; 5 pillar = new Pillar(bottom,100); //null底的柱体 6 System.out.println("体积"+pillar.getVolume()); 7 bottom=new Rectangle(12,22); 8 pillar=new Pillar(bottom,58); //pillar是具有矩形底的柱体 9 System.out.println("体积"+pillar.getVolume()); 10 bottom=new Circle(10); 11 pillar =new Pillar (bottom,58); //pillar是具有圆形底的柱体 12 System.out.println("体积"+pillar.getVolume()); 13 } 14 }
通过面向抽象类设计Pillar类,使得该Pillar类不再依赖具体类,因此每当系统增加新的Geometry的子类时,
例如增加一个Triangele子类,那么我们不需要修改Pillar类的任何代码,就可以使用Pillar创建出具有三角形底
的柱体。
通过前面的讨论我们可以做出如下总结:
面向抽象编程的目的是为了应对用户需求的变化,将某个类中经常因需求变化而需要改变的代码从该类中分离
出去。面向抽象编程的核心是让类中每种可能的变化对应地交给抽象类的一个子类去负责,从而让该类的设计者不
去关心具体实现,避免所设计的类依赖于具体的实现。面向抽象编程使设计的类容易应对用户需求的变化。
面向接口编程:
抽象类最本质的特性是可以包含抽象方法,这一点和接口类似,只不过接口中只有抽象方法而已。抽象类将其抽象方法
的实现交给其子类,而接口将其抽象方法的实现交给实现该接口的类。在设计程序时,学习怎样面向接口去设计程序。接口
只关心操作,但不关心这些操作的具体实现细节,可以使我们把主要精力放在程序的设计上,而不必拘泥于细节的实现。也
就是说,可以通过在接口中声明若干个abstract方法,表明这些方法的重要性,方法体的内容细节由实现接口的类去完成。
使用接口进行程序设计的核心思想是使用接口回调,即接口变量存放实现该接口的类的对象的引用,从而接口变量就可以回
调类实现的接口方法。利用接口也可以体现程序设计的“开-闭原则”,即对扩展开放,对修改关闭。例如,程序的主要设计
者可以设计出如下图所示的一种结构关系。
从下图可以看出,当程序再增加实现接口的类(由其他设计者去实现),接口变量variable所在的类不需要做任何修改,
就可以回调类重写的接口方法。
当然,在程序设计好后,首先应当对接口的修改“关闭”,否则,一旦修改接口,例如,为它增加一个abstract方法,
那么实现该接口的类都需要作出修改。但是,程序设计好后,应当对增加实现接口的类“开放”,即在程序中再增加实现
接口的类时,不需要修改其他重要的类。
个人的一点小薄见:
面向抽象编程和面向接口编程的思路都是一样的,面向抽象编程依靠上转型对象来实现;面向接口编程依靠接口回调
来实现;这种思想对于软件设计十分重要。Java中的一大法宝是多态,多态分两种,其中一种就是通过继承来实现的,子
类通过定义自己的行为来“展示自己”,每个子类都有不同的行为,所以展现出多态性。而我们可以建立一个类,这个类
可以帮助“每一个子类”来展现他们自己,而不用他们自己亲自动手,这会大大缩减程序的代码长度,假如有100个子类,
如果要求每一个子类都亲自展现自己,必须定义100段不同的代码;而通过一个类轮流为它们服务,这会显得更方便,这
杨的一个帮助子类展现自己的类被称为面向抽象的类(接口也一样),面向抽象的类为子类实例的上转型对象,通过调用
子类重写的方法来实现多态。
19:17:05
2018-07-07