在软件开发中,不可避免的因为业务的变化,如增加新的功能,这时需要对代码进行修改。对于功能扩展,下面是一些不好的行为方式:

  • 直接修改原来代码
  • 拷贝一些代码在这基础上进行修改

上面这些方式,一来会破坏现有代码的结构,让代码比较混乱;二来有可能影响原来的功能,有可能会导致原来功能出现问题。

我们希望的理想方式是,不需要对原代码进行直接修改,而是通过扩展的方式(如实现新的类)来增加新的功能。这样的好处是代码结构比较清晰,而且不会影响原来的代码。

定义

这个正是面向对象编程中提倡的“开闭原则 (OCP, Open Closed Principle)”。开闭原则的核心理念是指:一个类或者模块对扩展是开放的,对修改是关闭的

下面我们通过一个实际的案例来了解。

案例

案例1

假设我们开发一个企业的人事系统,其中有个功能是计算员工的薪资,初始的设计如下,下面是示意代码:

1
2
3
public class EmployeeSalary {
	public int calulate(){... };
}

开始时,只考虑到企业的员工都是一种类型,只有一种统一的计算方式。但后面企业又增加了新的类型的员工,不同类型员工的计算薪资方式不同。

为了实现新增员工类型的薪资计算,代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class EmployeeSalary {
	private EmployeeType etype;
	public EmployeeSalary(EmployeeType etype){
		this.etype = etype;
	}
	public int calulate(){
		if( etype是某种员工)
			计算该类型员工薪资
		else if( etype是另一种员工)
				计算该类型员工薪资
	}
}

从实现功能的角度看,这么处理是没有问题的。但是这种设计违背了我们上面说的开闭原则,因为需要修改原来的类,而且可以看出,一旦增加的员工类型变多了,原来代码中需要增加更多的 else if 语句,代码会越来越乱。

我们可以采用一种新的设计,来遵循“开闭原则”,示意代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//下面是公共代码
interface Employee{
	public int calulateSalary();
}
class EmployeeSalary {
	private Employee employee;
	public EmployeeSalary(Employee employee){
		this.employee = employee;
	}
	public int calulate(){
		return employee.calulateSalary();
	}
}
//工人类型
class Worker implements Employee{
	@Override
	public int calulateSalary() {
		return 5000; //计算工人的薪资
	}
}
//管理者类型
class Manager implements Employee{
	@Override
	public int calulateSalary() {
		return 8000;//计算管理者的薪资
	}
}
//测试代码
public class TestSalary {
	public static void main(String[] args) {
		EmployeeSalary  employeeSalary=null; 
		employeeSalary= new EmployeeSalary(new Worker());
		int workerSalary = employeeSalary.calulate();
		employeeSalary = new EmployeeSalary(new Manager());
		int managerSalary = employeeSalary.calulate();
	}
}

可以看出,基于上面的设计,每增加一种新的员工类型,只需基于 Employee 接口实现一个新的类,而不需要对原来代码进行修改。这正体现了开闭原则的“对扩展是开放的,对修改是关闭”这个关键特性。

案例2

假设我们要针对某种汽车设计一个自动驾驶的功能,初始的设计如下,下面是示意代码:

1
2
3
4
public class AutoSystemCar {
	public void run() {... }
	public void stop() {... }
}

这个设计当前是没有问题的,我们只需实现该车自动驾驶的功能(如 run,stop)即可。但是,因为每种汽车的自动驾驶实现是不一样的,这时如果需要支持一种新的汽车,一种实现方式如下,下面是示意代码(伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AutoSystemCar {
	private Car car;
	public AutoSystemCar(Car car){}
	public void run(){	
		if(car is A车型)
			...
		else if(car is B车型)
			...
	}
	public void stop(){	
		if(car is A车型)
			...
		else if(car is B车型)
			...
	}
}

上面代码设计,从功能角度看也是没问题的,但是这种设计一样违背了我们上面说的开闭原则。

我们来遵循“开闭原则”重新设计,示意代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//下面是公共代码
interface Car{
	void run();
	void stop();
}
public class AutoSystemCar {
	private Car car;
	public AutoSystemCar(Car car){this.car=car;}
	public void run(){	
		car.run();
	}
	public void stop(){	
		car.stop();
	}
}
//下面是福特汽车的自动驾驶实现
class FordCar implements Car{
	@Override
	public void run() {}
	@Override
	public void stop() {}
}
//下面是红旗汽车的自动驾驶实现
class HqCar implements Car{
	@Override
	public void run() {}
	@Override
	public void stop() {}
}

可以看出,基于上面的设计,每增加一种新的汽车类型,只需基于Car接口实现一个新的类,而不需要对原来代码进行修改。

小结

前面我们通过例子来讲述了面向对象设计原则中“开闭原则”的含义,可以看出,符合开闭原则设计出来的类,可扩展性很好,可以在不改变已有代码的情况下,来实现新的功能。但大家可能会提到一个问题,如果事先无法知道业务的扩展性该怎么办?比如前面例子说的人事系统中的薪资计算,假设企业开始就真的只有一种员工类型,我们还需要先按照可扩展的方式来设计吗?而且即使想设计出可扩展的代码,在业务变化不清楚的情况下,也不一定能设计出合理的代码。软件设计中不是也提倡设计要恰到好处,不要镀金吗?

的确,在实际的项目开发中,是会面临这样的情况。很多时候,我们都是事后才知道业务变化的情况。因此,在真实的项目中,我们的设计原则是:开始先按照实际情况去做恰到好处的设计,不用过多考虑可扩展性,就如最初的代码。但后期如果需求有变化,这时就应该对原来的代码先进行重构,让原来代码先具备可扩展性,然后在这基础上添加新功能。就如上面两个例子展示的。当然,如果我们在开始设计时,根据经验能判断出未来很可能的业务扩展(或者已经知道要扩展,只是第一阶段暂不实现),这种情况我们一上来就应该考虑到这个变化,设计出可扩展的代码,以避免后期的修改。

【面向对象设计原则】之原则概述

【面向对象设计原则】之单一职责原则(SRP)

【面向对象设计原则】之开闭原则(OCP)

【面向对象设计原则】之里氏替换原则(LSP)

【面向对象设计原则】之接口隔离原则(ISP)

【面向对象设计原则】之依赖倒置原则(DIP)