文章目錄
- 什麼是繼承
- 什麼是組合
- 繼承群組合有什麼差別和聯系
- 為什麼不推薦使用繼承
- 組合的好處
- 必須使用繼承
- 必須使用組合
- 參考資料
什麼是繼承
繼承就是子類繼承父類的特征和行為,使得子類對象(執行個體)具有父類的執行個體域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。
/**
* 動物
*/
public class Animal {
public void breathing() {
System.out.println("呼氣...吸氣...");
}
}
/**
* 飛行動物
* 繼承,可以獲得父類屬性和方法
*/
public class FlyingAnimals extends Animal{
public void filying() {
System.out.println("飛行...");
}
public static void main(String[] args) {
FlyingAnimals flyingAnimals = new FlyingAnimals();
flyingAnimals.breathing();
flyingAnimals.filying();
}
}
什麼是組合
組合是通過對現有對象進行拼裝即組合産生新的具有更複雜的功能。
/**
* 動物
*/
public class Animal {
public void breathing() {
System.out.println("呼氣...吸氣...");
}
}
/**
* 爬行動物
* 組合,可以擷取組合對象的屬性和方法
*/
public class Reptilia {
private Animal animal;
public Reptilia(Animal animal) {
this.animal = animal;
}
public void crawling() {
System.out.println("爬行...");
}
public void breathing() {
animal.breathing();
}
public static void main(String[] args) {
Animal animal = new Animal();
Reptilia reptilia = new Reptilia(animal);
reptilia.breathing();;
reptilia.crawling();
}
}
繼承群組合有什麼差別和聯系
繼承最大的一個好處就是代碼複用。假如兩個類有一些相同的屬性和方法,我們就可以将這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就可以重用父類中的代碼,避免代碼重複寫多遍。不過,這一點也并不是繼承所獨有的,我們也可以通過其他方式來解決這個代碼複用的問題,比如利用組合關系而不是繼承關系。
繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關系,可以解決代碼複用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。在這種情況下,我們應該盡量少用,甚至不用繼承。
繼承主要有三個作用:表示 is-a 關系,支援多态特性,代碼複用。而這三個作用都可以通過組合、接口、委托三個技術手段來達成。除此之外,利用組合還能解決層次過深、過複雜的繼承關系影響代碼可維護性的問題。
盡管我們鼓勵多用組合少用繼承,但組合也并不是完美的,繼承也并非一無是處。在實際的項目開發中,我們還是要根據具體的情況,來選擇該用繼承還是組合。如果類之間的繼承結構穩定,層次比較淺,關系不複雜,我們就可以大膽地使用繼承。反之,我們就盡量使用組合來替代繼承。除此之外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。
為什麼不推薦使用繼承
我們定義一個抽象的“鳥”的類,所有鳥都繼承這個類。
AbstractBird 中定義的fly方法并不完全都适用于所有的鳥,比如鴕鳥不會飛,繼承父類之後隻能重寫fly方法,抛出一個異常表示不會飛。
但是這顯然違背了裡氏替換原則,也違背了我們之後要講的最小知識原則(Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的機率,也徒增了代碼量。
public class AbstractBird {
//...省略其他屬性和方法...
public void fly() { //... }
}
public class Ostrich extends AbstractBird { //鴕鳥
//...省略其他屬性和方法...
public void fly() {
throw new UnSupportedMethodException("I can't fly.'");
}
}
或許我們可以再更細分一下,分别定義會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,繼承關系就變成了:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICciV2dsQXYtJ3bm9CX0gTMx81dsQWZ4lmZf1GLlpXazVmcvwVZnFWbp1zczV2YvJHctM3cv1Ces0zaHRGcWdUYuVzVa9GczoVdG1mWfVGc5RHLwIzX39GZhh2csATMflHLwEzX4xSZz91ZsAzMfRHLGZkRGZkRfJ3bs92YskmNhVTYykVNQJVMRhXVEF1X0hXZ0xCNx8VZ6l2cssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL0kzNzcDO2MDZlZjYjRWNzYzXwETN1gDMwMzLcBTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
但是,鳥會不會飛隻是其中一個行為,會不會叫、會不會下蛋……如果每一種行為都定義一個抽象類,那繼承關系會越來越複雜,而且都是強耦合性。
是以,繼承最大的問題就在于:繼承層次過深、繼承關系過于複雜會影響到代碼的可讀性和可維護性。這也是為什麼我們不推薦使用繼承。
組合的好處
上面關于繼承的問題,針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 接口,隻讓會飛的鳥去實作這個接口。對于會叫、會下蛋這些行為特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口。我們将這個設計思路翻譯成 Java 代碼的話,就是下面這個樣子:
public interface Flyable {
void fly();
}
public interface Tweetable {
void tweet();
}
public interface EggLayable {
void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
//... 省略其他屬性和方法...
@Override
public void tweet() { //... }
@Override
public void layEgg() { //... }
}
public class Sparrow impelents Flyable, Tweetable, EggLayable {//麻雀
//... 省略其他屬性和方法...
@Override
public void fly() { //... }
@Override
public void tweet() { //... }
@Override
public void layEgg() { //... }
}
不過,我們知道,接口隻聲明方法,不定義實作。也就是說,每個會下蛋的鳥都要實作一遍 layEgg() 方法,并且實作邏輯是一樣的,這就會導緻代碼重複的問題。那這個問題又該如何解決呢?
我們可以針對三個接口再定義三個實作類,它們分别是:實作了 fly() 方法的 FlyAbility 類、實作了 tweet() 方法的 TweetAbility 類、實作了 layEgg() 方法的 EggLayAbility 類。然後,通過組合和委托技術來消除代碼重複。具體的代碼實作如下所示:
public interface Flyable {
void fly();
}
public class FlyAbility implements Flyable {
@Override
public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
private TweetAbility tweetAbility = new TweetAbility(); //組合
private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
//... 省略其他屬性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}
我們知道繼承主要有三個作用:表示 is-a 關系,支援多态特性,代碼複用。而這三個作用都可以通過其他技術手段來達成。比如 is-a 關系,我們可以通過組合和接口的 has-a 關系來替代;多态特性我們可以利用接口來實作;代碼複用我們可以通過組合和委托來實作。是以,從理論上講,通過組合、接口、委托三個技術手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關系,特别是一些複雜的繼承關系。
不過,之是以“多用組合少用繼承”這個口号喊得這麼響,隻是因為,長期以來,我們過度使用繼承。其實,組合并不完美,繼承也不是一無是處。隻要我們控制好它們的副作用、發揮它們各自的優勢,在不同的場合下,恰當地選擇使用繼承還是組合,這才是我們所追求的境界。
必須使用繼承
一些特殊的場景要求我們必須使用繼承。如果你不能改變一個函數的入參類型,而入參又非接口,為了支援多态,隻能采用繼承來實作。比如下面這樣一段代碼,其中 FeignClient 是一個外部類,我們沒有權限去修改這部分代碼,但是我們希望能重寫這個類在運作時執行的 encode() 函數。這個時候,我們隻能采用繼承來實作了。
public class FeignClient { // Feign Client架構代碼
//...省略其他代碼...
public void encode(String url) { //... }
}
public void demofunction(FeignClient feignClient) {
//...
feignClient.encode(url);
//...
}
public class CustomizedFeignClient extends FeignClient {
@Override
public void encode(String url) { //...重寫encode的實作...}
}
// 調用
FeignClient client = new CustomizedFeignClient();
demofunction(client);
必須使用組合
public class Url {
//...省略屬性和方法
}
public class Crawler {
private Url url; // 組合
public Crawler() {
this.url = new Url();
}
//...
}
public class PageAnalyzer {
private Url url; // 組合
public PageAnalyzer() {
this.url = new Url();
}
//..
}