天天看點

陳舊文法密度之八——使用多态消滅if-else if-else

用多态消滅if – else if - else

如果說泛型消滅行為相同、類型不同的分支語句,多态則消滅(父)類型相同、行為不同的分支語句。

我在1999年曾經寫國慶閱兵的直升飛機編隊指揮系統,其地面的核心是一個地圖顯示系統。地圖的顯示代碼大緻如下:

public class PlaneMap {
    List<MapItem> items = new ArrayList<>();

    public void show() {
        for (MapItem item : items) {
            if (item.type == MapItem.Types.City) {
                MapItemCity theCity = (MapItemCity) item;
                //.... Draw the city icon.
            } else if (item.type == MapItem.Types.Road) {
                MapItemRoad theRoad = (MapItemRoad) item;
                //... Draw the dual polylines of the road.
            } else if (item.type == MapItem.Types.River) {
                MapItemRiver theRiver = (MapItemRiver) item;
                //... Draw the dual polylines of the river and fill them.
            }
        }
    }
}      

各種元素的繪制方法五花八門,比如City城市需要顯示一個圖示Icon(不同級别的城市圖示不同);道路需要顯示兩條(某些小路其實是一條)邊緣;河流則要顯示河岸并填充(但複雜的河流應該顯示某些江心洲之類的額外河岸)……這個Show方法很快就變得非常巨大。

得虧當年隻是做飛機的導航,因而隻要一些空中導航用的核心元素即可。如果顯示類似高德地圖之類的汽車用地圖,這個方法可以包含整個系統的一半代碼都不誇張。

類似這時候就需要使用虛函數來解決這個問題了。

Java代碼大緻如下:

在基類中聲明一個實際上沒有用的show方法:

public class MapItem {
//    public Types type;
//    public enum Types {City, Road, River}

    public void show() {
    }
}      

注意這個時候,type也不需要了,因為子類類型将代替這個變量進行分支判斷工作。

在每個子類中挨個實作show方法,代碼就是之前分支語句中的代碼:

public class MapItemCity extends MapItem {
    @Override
    public void show() {
        //.... Draw the city icon.
    }
}      

剩下的代碼則變得極其簡單:

public class PlaneMap {
    List<MapItem> items = new ArrayList<>();

    public void add(MapItem item) {
        items.add(item);
    }

    public void show() {
        for (MapItem item : items) {
            item.show();
        }
    }      

隻要各種類型的Item(隻要繼承了MapItem基類即可,比如MapItemCity, MapItemRoad, MapItemRiver)用add()方法加入地圖,然後隻有兩行代碼的show()方法就會順序調用所有Item的show()方法。

由于基類的空的show()方法被子類中的@Override方法重寫了,是以實際調用的實際上是子類方法。如果有些重複代碼希望在所有子類中調用,則可以寫在基類的show()方法中,然後在子類的show中利用super.show()來調用。

(虛)基類 vs. 接口

假設有下面這樣三個類,該如何設計繼承關系呢?

人,兔子,小汽車。顯然可以讓人和兔子繼承“動物”,而小汽車繼承“交通工具”。

然而問題來了:“跑”,也就是run()方法如何實作?

人和兔子好辦,讓基類“動物”實作run(),然後人和兔子來@Override即可;小汽車嘛,則可以讓“交通工具”來幫忙,也沒問題。

但如果是這樣,要想讓他們被扔到同一個List中集中處理,就比較難了,因為他們不像之前的各種MapItem有共同的基類。一個技術上可行的方法是讓動物和交通工具強行從一個類派生,比如“可以跑的東西RunableItem”,但很可能動物早就繼承了更深的基類“生物”,而交通工具也是早已繼承了“機械”,這可怎麼辦?

曾經有一段時間盛行對“多繼承”的讨論,然而現在主流語言都沒有采用這種設計,而是引入了接口。

大緻實作如下:

容器類(相當于之前的飛機地圖PlaneMap):

public class LetsRun {
    private List<iRunable> runableList = new ArrayList<iRunable>();

    public void letsRun() {
        for ( iRunable runable : runableList) {
            runable.run();
        }
    }
}      

接口(相當于之前的MapItem基類):

public interface iRunable {
    void run();
}      

其中一個類兔子(相當于之前的城市City):

public class Rabbit implements iRunable {
    @Override
    public void run() {
        //As a rabbit, run with 4 legs.
    }
}      

總結一下:

派生關系(基類-派生類)代表天然的唯一從屬關系;接口實作關系(接口-實作類)代表一種能力(可能有很多能力),是以接口常常以able結尾。