1. 程式人生 > >複用-組合、繼承、代理區別和用法

複用-組合、繼承、代理區別和用法

繼承主要有兩個作用:
1.為匯出類提供方法,這個特性在程式碼複用有比較多的應用
2.表現匯出類與基類之間的關係。這個特性在多型裡非常有用,後面會講到。
我們先來講為匯出類提供方法這個特性——繼承是程式碼複用中重要的一種方式,很難把它與複用割裂開看,而且放在一起比較似乎更容易理解繼承在為匯出類提供方法這方面的用途以及與其他兩種方式的區別。那麼這一篇我們講組合&繼承&代理——程式碼複用的思想實現。

複用程式碼主要有組合、繼承、代理幾種方式。關於這幾種方式的使用,簡單來說就是:
1.組合:在新類中嵌入某個物件,通過引用呼叫它的方法獲得其功能,但是新類的使用者看到的只是新類所定義的介面;
2.繼承

:從基類繼承獲得基類的全部非private屬性和方法,即擁有與基類相同的特性。
3.代理:組合和繼承的“中間”方法,嵌入其基類,然後在匯出類的方法中通過基類的引用呼叫其方法獲得功能,對外提供的還是匯出類的方法
下面結合例子具體來講講組合、繼承、和代理。

(一、組合
在編寫程式碼過程中,將物件引用置於新類中的做法就是組合。新類只是需要它的功能,但是兩者沒有什麼關聯——組合體現的是一種整體與部分,一種擁有的關係。類似於現代化組裝,每個物件都是一個零件,擁有各自的功能,組合起來的新類才是我們需要的產品,但是我們不需要去了解每一個零件。就比如造一臺手機,用到CPU、GPU、ROM、等等。
如:

public class mobilePhone{
    private MobilePhone mobilePhone{
          CPU cpu = new CPU();
          GPU gpu = new GPU();
          ROM rom = new ROM();
    }
}

組合的優點:  
1.當前物件只能通過所包含的那個物件去呼叫其方法,所以所包含的物件的內部細節對當前物件是不可見的。
2.當前物件與包含的物件是一個低耦合關係,如果修改包含物件的類中程式碼不需要修改當前物件類的程式碼。
3. 當前物件可以在執行時動態的繫結所包含的物件。可以通過set方法給所包含物件賦值。
缺點:
1.容易產生過多的物件。

(二、繼承
以現有的類作為基類建立新的類,預設擁有父類全部的非private的屬性和方法,不能選擇性的繼承,同時新類可以增加新的屬性和方法或者覆蓋基類的方法。它描述的是一種is-a描述的關係,是類與類,介面與介面之間最常見的關係。例如學生是個人,那麼學生就可以繼承人這個類。
這麼說似乎同樣有些抽象,那麼下面我們來看例子:

public class BoyFriend{
    private String name ;
    private String sex ;
    private int age ;
    private GirlFriend girlfriend;
}
public class GirlFriend{
    private String name ;
    private String sex ;
    private int age ;
    private BoyFriend boyFriend;
}

我們定義了兩個類,boyfriend和girlFriend,看兩者的屬性,我們不難發現,除了各自的boyFriend和girlFriend不同外,其他欄位完全相同,如果這個時候我們來個其他身份,我們是不是又要重寫一遍name,age,sex等重複的屬性值呢,所以這裡就可以引入繼承特性實現複用。
在使用繼承特性之前,我們需要想清楚一點,他們繼承什麼,也就是他們的共性是什麼,比如這裡的男女朋友,大人、小孩,都是人,都有各自的名字、年齡、性別等等,並且都有相同的行為吃飯、走路、說話等等,所以我們認為他們都擁有人的屬性和行為,同時也應該繼承這些屬性和行為。
通過extends關鍵字表達類的繼承關係

public class Human{
    /*
     * 對屬性的封裝
     * 姓名、性別、年齡都是這個人的私有屬性
     */
    private String name ;
    private String sex ;
    private int age ;
    /*
     * setter()、getter()是該物件對外開放的介面,此處省略setter、getter方法
     */
}

public class BoyFriend extends Human{
	private GirlFriend girlFriend;
}

public class GirlFriend extends Human{
	private BoyFriend boyFriend;
}

使用繼承後,程式碼量減少的同時,我們還能夠非常清晰的看到他們的關係。
繼承所描述的是“is-a”的關係,如果有兩個物件A和B,可以描述為“A是B”,則可以表示A繼承B,其中B被A稱為父類或者超類,A是B的子類或者派生類。
優點:
使子類可以重寫父類的方法來方便地實現對父類的擴充套件。
缺點:
1.因為子類繼承了父類全部非private屬性和方法—父類的內部細節對子類是可見的。
2.子類從父類繼承的方法在編譯時就確定下來了——繼承後,在呼叫匯出類構造器的時候,會插入對基類的構造器的呼叫,所以無法在執行期間改變從父類繼承的方法的行為。
3.如果對父類的方法做了修改的話(比如增加了一個引數),則子類的方法必須做出相應的修改。所以說子類與父類是一種高耦合,違背了面向物件思想。

關於呼叫子類構造器插入父類構造器,我們來看下面這段程式碼:

Class Human{
 Human(){
        System.out.println("Person Constrctor-----Human" );
    }
}

public class BoyFriend extends Human{![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20181113125049952.png)
    private GirlFriend girlFriend ;
    BoyFriend (){
        System.out.println("BoyFriend Constructor...");
    }
    
   public static void main(String[] args) {
        BoyFriend boyFriend = new BoyFriend ();
    }
}

Output:
先輸出父類,後輸出子類

上面這個例子我們可以看到,構建過程是從父類開始向子類一級一級的完成構建。在我們並沒有顯式的引用父類的構造器時,編譯器仍會預設給子類呼叫父類的構造器。如果我們顯式的定義了構造器,編譯器就不會自動新增構造器了
注意:所有的構造器預設為static
但是呼叫父類的無參構造器是有前提的:父類有無參構造器。如果父類沒有預設無參構造器,我們就必須顯示的使用super()來呼叫父類構造器,否則編譯器會報錯

所以《Thinking in Java》中關於是否使用繼承有這麼一句話:
問一問自己是否需要從子類向父類進行向上轉型,如果必須向上轉型,則繼承是必要的,但是如果不需要,則應當考慮自己是否需要繼承。(現在可能有點看不懂,看到多型一篇就明白了)

(三、代理
代理是組合與繼承結合的中間方法,在新類中new一個存在的父類物件(組合的做法),此時向新類暴露了物件的全部介面(繼承的結果)。但是有所不同的是,此時代理可以選擇性的開放擁有的介面。借用thinking in Java上面的例子:

final class PlaneControl {//final表示不可繼承
    protected void speed() {}
    protected void fire() {}
    protected void left() {}
    protected void right() {}
}

public class DelegationTest{    
    private PlaneControl planeControl;    //private外部不可訪問
    /*
     * 飛行員許可權代理類,普通飛行員不可以開火
     */
    PlaneDelegation(){
        planeControl=new PlaneControl();
    }
    public void speed(){
        planeControl.speed();
    }
    public void left(){
        planeControl.left();
    }
    public void right(){
        planeControl.right();
    }
}

可以看到基類的介面通過組合的方式全部獲得了,然後對外提供了想提供的部分,外界無法獲得全部的基類方法(這裡是簡化版的靜態代理方法,具體靜態代理與動態代理的對比後面會詳細聊聊)。

所以總得來說:
組合:如果只是需要使用父類的功能,可以選用組合。
繼承:如果想要向上轉型為父類,使用多型,就使用繼承。
代理:如果想使用父類方法,但是又不想暴露全部介面,使用代理。