如果说泛型消灭行为相同、类型不同的分支语句,多态则消灭(父)类型相同、行为不同的分支语句。
我在1999年曾经写国庆阅兵的直升飞机编队指挥系统,其地面的核心是一个地图显示系统。地图的显示代码大致如下:
public class PlaneMap { Listitems = 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 { Listitems = 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()来调用。
人,兔子,小汽车。显然可以让人和兔子继承“动物”,而小汽车继承“交通工具”。
然而问题来了:“跑”,也就是run()方法如何实现?
人和兔子好办,让基类“动物”实现run(),然后人和兔子来@Override即可;小汽车嘛,则可以让“交通工具”来帮忙,也没问题。
但如果是这样,要想让他们被扔到同一个List中集中处理,就比较难了,因为他们不像之前的各种MapItem有共同的基类。一个技术上可行的方法是让动物和交通工具强行从一个类派生,比如“可以跑的东西RunableItem”,但很可能动物早就继承了更深的基类“生物”,而交通工具也是早已继承了“机械”,这可怎么办?
曾经有一段时间盛行对“多继承”的讨论,然而现在主流语言都没有采用这种设计,而是引入了接口。
大致实现如下:
容器类(相当于之前的飞机地图PlaneMap):
public class LetsRun { private ListrunableList = new ArrayList (); 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结尾。
还有一种通俗的表达:想用多继承的时候,就是应该用接口的时候。
后面这个是自己总结的,如果早一些看到,就不会在前三年都没用接口了。
下一讲,神秘的工厂模式将登场,来建立复杂逻辑分支的解耦。