【面向对象设计原则】之开闭原则(OCP)
在软件开发中,不可避免的因为业务的变化,如增加新的功能,这时需要对代码进行修改。对于功能扩展,下面是一些不好的行为方式:
- 直接修改原来代码
- 拷贝一些代码在这基础上进行修改
上面这些方式,一来会破坏现有代码的结构,让代码比较混乱;二来有可能影响原来的功能,有可能会导致原来功能出现问题。
我们希望的理想方式是,不需要对原代码进行直接修改,而是通过扩展的方式(如实现新的类)来增加新的功能。这样的好处是代码结构比较清晰,而且不会影响原来的代码。
定义
这个正是面向对象编程中提倡的“开闭原则 (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接口实现一个新的类,而不需要对原来代码进行修改。
小结
前面我们通过例子来讲述了面向对象设计原则中“开闭原则”的含义,可以看出,符合开闭原则设计出来的类,可扩展性很好,可以在不改变已有代码的情况下,来实现新的功能。但大家可能会提到一个问题,如果事先无法知道业务的扩展性该怎么办?比如前面例子说的人事系统中的薪资计算,假设企业开始就真的只有一种员工类型,我们还需要先按照可扩展的方式来设计吗?而且即使想设计出可扩展的代码,在业务变化不清楚的情况下,也不一定能设计出合理的代码。软件设计中不是也提倡设计要恰到好处,不要镀金吗?
的确,在实际的项目开发中,是会面临这样的情况。很多时候,我们都是事后才知道业务变化的情况。因此,在真实的项目中,我们的设计原则是:开始先按照实际情况去做恰到好处的设计,不用过多考虑可扩展性,就如最初的代码。但后期如果需求有变化,这时就应该对原来的代码先进行重构,让原来代码先具备可扩展性,然后在这基础上添加新功能。就如上面两个例子展示的。当然,如果我们在开始设计时,根据经验能判断出未来很可能的业务扩展(或者已经知道要扩展,只是第一阶段暂不实现),这种情况我们一上来就应该考虑到这个变化,设计出可扩展的代码,以避免后期的修改。