首先,我们看下开放-封闭原则(Open-Closed Principle,简称OCP)的概念:
-
- 是指软件实体(类、模块、函数等)应该可以扩展,但是不可修改。
- 任何新功能(functionality)应该通过添加新class、属性或方法来实现,而不是通过改变现有的代码。
实现准则:
-
- 在继承子类中实现新的功能
- 允许客户端(clients)访问带有抽象接口的原始基类
为什么要遵循开放-封闭原则?
-
- 如果不遵循OCP原则,一个类或函数总是允许添加新的逻辑,那么我们不能不重新测试新的逻辑依赖的整个类
- 如果不遵循OCP原则很可能破坏单一职责原则,因为随着后期功能的添加,一个类或函数可能完成多项任务,从这个角度来讲,单一职责原则和开放-封闭原则是高度相互依赖的
- 如果不遵循OCP原则,在现有类上添加功能,后期该类的体积将越来越大,导致维护该类的困难增加
遵循开放-封闭原则使得设计在面对需求的改变时可以保持相对稳定,因为我们并没有改动原有的代码,而原有代码已被使用和测试过,是稳定的。我们仅仅是在重用原始代码的基础上通过类基础的方式添加了新的代码。
开放-封闭原则要求在设计之初,尽量让原始基类足够好,写好之后就不要去修改了,如果有新需求,增加一些类就行了,原始基类代码能不动就不动。事实上对原始基类的绝对修改关闭是不可能的,既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪些变化封闭做出选择。他必须先猜测出最有可能发生变化的地方,然后通过构造抽象来隔离那些变化。
事实上,事先猜测可能的变化也是有困难的,但我们可以在发生小变化时及时想办法应对发生更大变化的可能,也即等到变化发生就考虑创建抽象来隔离以后发生的同类变化。当然,并不是什么时候应对变化都是容易的。我们希望在开发工作展开不久就知道可能发生的变化。查明可能发生变化所等待的时间越长,要创建正确的抽象就越困难,也就是说当变化部分的代码已经在多个地方用到了,再考虑抽象和分离,代价就变大了。
下面我们通过一个实例来说明开放-封闭原则。
public class Employee { public int ID { get; set; } public string Name { get; set; } public Employee() { } public Employee(int id, string name) { this.ID = id; this.Name = name; } public double CalculateBonus(double salary) { return salary * 0.1; } }
以上是一个普通的Employee类,里面封装了ID和Name字段和一个计算奖金的方法,目前运行良好。当我们接到一个新需求,需要区分正式工和合同工并对正式工的奖金计算改用薪水乘以0.2 时,我们应该通过何种方式实现呢?。我们先来看看不用开放-封闭原则如何实现。
public class Employee { public int ID { get; set; } public string Name { get; set; } public string EmployeeType { get; set; } public Employee() { } public Employee(int id, string name,string employeeType) { this.ID = id; this.Name = name; this.EmployeeType = employeeType; } public double CalculateBonus(double salary) { if (this.EmployeeType == "Permanent") return salary * 0.2; else return salary * 0.1; } }
从上面代码可以看出,不使用开放-封闭原则时,我们直接在原始Employee类中添加了一个EmployeeType字段,修改了Employee构造器,并在CalculateBonus方法中新增了判断员工类型的逻辑。这种实现方式的缺点见本篇开头部分所述。
接下来看如何使用开放-封闭原则实现。
public abstract class Employee { public int ID { get; set; } public string Name { get; set; } public Employee() { } public Employee(int id, string name) { this.ID = id; this.Name = name; } public abstract double CalculateBonus(double salary); } public class PermanentEmployee : Employee { public PermanentEmployee() { } public PermanentEmployee(int id, string name) : base(id, name) { } public override double CalculateBonus(double salary) { return salary * 0.2; } } public class TemporaryEmployee : Employee { public TemporaryEmployee() { } public TemporaryEmployee(int id, string name) : base(id, name) { } public override double CalculateBonus(double salary) { return salary * 0.1; } }
从以上代码可以看到,遵循开放-封闭原则的实现通过后期创建新class的方式添加新功能,并未在原始Employee基类的实现上做改动,仅仅是将可能变化的CalculateBonus方法通过添加abatract关键字来将具体的实现转移到后期添加的继承子类中,这样就将变化隔离出来了。
开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开放人员应该仅对程序中呈现出频繁变化的部分构造抽象,然而,对于应用程序中的每个部分都刻意进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。