面向抽象編程和面向接口編程
原創
以下內容來自《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
面向抽象編程和面向接口編程