序言

  面向对象编程(简称:OOP)要求我们按人的思考方式来解决程序设计中的各类关联问题。这种编程的好处使得对象之间具有某种独立性,否则可能会奇怪的出现在你家里长期住着一个邋遢的陌生人的情景,而真实的世界是不应该有这样的情况的。这种好的独立性,使得对象之间必须联系的时候才进行联系,这会使得编写的代码易于重用、维护及扩展。

  我们从简单工厂(Simple Factory)开始OOP的主题,为了形象的进行讲述,我们假想一家专车公司,称为世界专车公司(World Special Inc),我们想在这一行竞争中脱颖而出,成为世界第一(Number One),必须让我们的乘客客户获得更好,更快,更安全的服务。

按接口编程

  说明:程序的代码往往都是同一个团队写的,修改代码对你们来说非常方便,从而很容易忽视面向对象编程的方式。记住程序里面表示的外界对象,要按它们的真实情景来思考。比如代表司机的Driver类,就应该按Driver类是司机的方式来思考自己本身及与其他对象的联系。
  我们的专车公司当前有三类车:奔驰车(Bens)、宝马车(BWM, “别摸我”)、奥迪车(Audi),每辆车都会有一个车牌号(Plate No)。

Bens :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package object.world.special;

public class Bens {
private String plateNo ; // 车牌号码

public Bens(String plateNo){
this.plateNo = plateNo ;
}

public void openKey(){
System.out.println("车牌号码 " + plateNo + " 的Bens车的锁打开了");
}

public void start(){
System.out.println("车牌号码 " + plateNo + " 的Bens车启动了");
}

public void run(){
System.out.println("车牌号码 " + plateNo + " 的Bens车在路上");
}
}

BWM :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package object.world.special;

public class BWM {
private String plateNo ; // 车牌号码

public BWM(String plateNo){
this.plateNo = plateNo ;
}

public void openKey(){
System.out.println("车牌号码 " + plateNo + " 的BWM车的锁打开了");
}

public void start(){
System.out.println("车牌号码 " + plateNo + " 的BWM车启动了");
}

public void run(){
System.out.println("车牌号码 " + plateNo + " 的BWM车在路上");
}
}

Audi :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package object.world.special;

public class Audi {
private String plateNo ; // 车牌号码

public Audi(String plateNo){
this.plateNo = plateNo ;
}

public void openKey(){
System.out.println("车牌号码 " + plateNo + " 的Audi车的锁打开了");
}

public void start(){
System.out.println("车牌号码 " + plateNo + " 的Audi车启动了");
}

public void run(){
System.out.println("车牌号码 " + plateNo + " 的Audi车在路上");
}
}

Driver(世界专车公司的司机员工):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package object.world.special;

public class Driver {
private String staffNo ; // 员工编号
private Bens bens ;

public Driver(String staffNo , Bens bens){
this.staffNo = staffNo;
this.bens = bens ;
}

public void drive(){
System.out.println("员工编号为 "+ staffNo + " 司机在驾驶汽车");
bens.openKey();
bens.start();
bens.run();
}

public void change(Bens otherBens){
bens = otherBens;
}
}

乘客开始打专车服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package object.world.special;

public class ModelClient {

public static void main(String[] args) {
// 一位驾驶 Bens(奔驰)的驾驶员
Driver driver = new Driver("WS0001" ,new Bens("苏E10001"));
driver.drive();

// 给司机换辆 Bens(奔驰)
driver.change(new Bens("苏E10002"));
driver.drive();
}
}

让我们看看输出的结果是什么?

1
2
3
4
5
6
7
8
9
员工编号为 WS0001 司机在驾驶汽车
车牌号码 苏E10001 的Bens车的锁打开了
车牌号码 苏E10001 的Bens车启动了
车牌号码 苏E10001 的Bens车在路上
-------- 人为的分割线 --------
员工编号为 WS0001 司机在驾驶汽车
车牌号码 苏E10002 的Bens车的锁打开了
车牌号码 苏E10002 的Bens车启动了
车牌号码 苏E10002 的Bens车在路上

我们让司机换辆 BWM 上路 : driver.change(new BWM("苏E80001")); 结果发现代码根本编译不过,因为从Driver类里注意到这个司机有一个Bens属性,这表明这司机其实只会驾驶Bens车。这显然不符合实际情况,不是我们所想要的,专车公司要的是会驾驶各类小汽车的员工。

设计原则:针对接口编程,而不是针对实现编程

  我们修改代码,不应该把Driver驾驶的汽车类别固定死,而采用按接口编程。在Java语言里面,针对接口编程有两种方式:

1. 继承抽象超类(abstract super class),关键字 extends
2. 实现接口(interface),关键字 implements

  一般如果有很多基本的方法(method)子类都可以通用,往往继承抽象超类的方式,把那些基本的方法放入抽象超类。我们在这里采用实现接口的方式来处理,引入一个 Car 接口,接口里面的方法都是抽象的,并且是 public 的, 实现接口里面的类的方法则会具有多态(polymorphic)性:执行时会按照实际状态来执行真正的行为(Java程序其实指的是方法)。接口 Car 可以这样设计:

Car:

1
2
3
4
5
6
7
package object.world.special;

public interface Car {
void openKey();
void start();
void run();
}

我们发现接口里面的三个方法名与前面三类车的方法名是相同的,这是因为我们的类需要实现接口的缘故。修改后实现接口 Car 的 Bens 来看一下代码(BWM,Audi 一样处理)

Bens:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package object.world.special;

public class Bens implements Car{
private String plateNo ; // 车牌号码

public Bens(String plateNo){
this.plateNo = plateNo ;
}

public void openKey(){
System.out.println("车牌号码 " + plateNo + " 的Bens车的锁打开了");
}

public void start(){
System.out.println("车牌号码 " + plateNo + " 的Bens车启动了");
}

public void run(){
System.out.println("车牌号码 " + plateNo + " 的Bens车在路上");
}
}

  只在 class 这一行添加了 implements Car,意思是要实现 Car 里面的方法,这三个方法一开始就有了,如果 Car 添加新的方法,则 Bens 需要添加同样方法名的方法来实现。这样Bens(奔驰)、BWM(宝马)、Audi(奥迪)针对 Car 接口都有了自己不同的实现。
  接下来要修改Driver(司机)类,让司机会驾驶所有的小汽车(Car),把 Bens 替换为 Car 即可。
Driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package object.world.special;

public class Driver {
private String staffNo ; // 员工编号
private Car car ;

public Driver(String staffNo , Car car){
this.staffNo = staffNo;
this.car = car ;
}

public void drive(){
System.out.println("员工编号为 "+ staffNo + " 司机在驾驶汽车");
car.openKey();
car.start();
car.run();
}

public void change(Car otherCar){
car = otherCar;
}
}

乘客继承打专车服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package object.world.special;

public class ModelClient {

public static void main(String[] args) {
// 一位驾驶 Bens(奔驰)的驾驶员
Driver driver = new Driver("WS0001" ,new Bens("苏E10001"));
driver.drive();

// 给司机换辆 Bens(奔驰)
driver.change(new Bens("苏E10002"));
driver.drive();

// === 新添加的代码 ===
// 给司机换辆 BWM(别摸我)
driver.change(new BWM("苏E80001"));
driver.drive();

// 给司机换辆 Audi(奥迪)
driver.change(new Audi("苏E60001"));
driver.drive();
}
}

先让我们看看这次输出的结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
员工编号为 WS0001 司机在驾驶汽车
车牌号码 苏E10001 的Bens车的锁打开了
车牌号码 苏E10001 的Bens车启动了
车牌号码 苏E10001 的Bens车在路上
-------- 人为的分割线 --------
员工编号为 WS0001 司机在驾驶汽车
车牌号码 苏E10002 的Bens车的锁打开了
车牌号码 苏E10002 的Bens车启动了
车牌号码 苏E10002 的Bens车在路上
---- 人为的分割线 ,底下为新的代码 ----
员工编号为 WS0001 司机在驾驶汽车
车牌号码苏E80001的BWM车的锁打开了
车牌号码苏E80001的BWM车启动了
车牌号码苏E80001的BWM车在路上
-------- 人为的分割线 --------
员工编号为 WS0001 司机在驾驶汽车
车牌号码苏E60001的Audi车的锁打开了
车牌号码苏E60001的Audi车启动了
车牌号码苏E60001的Audi车在路上

  我们发现 ModelClient类的main方法 前面的代码并不需要做任何修改,另外这三类车,Driver 里面的 Car 属性都能够接受,同样是 driver.drive(); 的代码,然而可以根据具体是什么车来实现相应的行为,宝马车(BMW)展示的就是宝马车的行为,奥迪车(Audi)展示的就是奥迪车的行为,这就是接口编程的好处,使得司机轻松驾驶每一种类型的车。

参考:
Head First 设计模式(中文版)—— 中国电力出版社 ISBN:978-7-508-35393-7


下一讲:我们来讲述简单工厂(Simple Factory)的主题