面向对象设计原则之开闭原则
两截门--一个被水平分割为两部分的门,这样每一部分都可以独立保持开放或封闭
开放-封闭原则(The Open-Closed Principle)
软件实体(类、模块、函数)应该是可以扩展的,但是不可以修改的。
如果程序中的一处改动就会产生连锁反应,导致一系列的相关模块的改动,那么设计就具有僵化的臭味。如果正确的应用OCP,那么以后再进行同样的改动时,就只需要添加新的代码,而不必改动已经正常运行的代码。
描述
主要两个特征:
- “对于扩展是开放的”
模块的行为是可以扩展的,当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。简言之,我们可以改变模块的功能。 - “对于更改是封闭的”
对模块进行扩展时,不必改动模块的源代码。
关键是抽象
下图展示了一个简单的不遵循OCP的设计。Client类和Server类都是具体类,Client类使用Server类。如果我们希望Client对象使用另外一个不同的服务器对象,那么就必须要把Client类中使用Server类的地方更改为新的服务器类
下图是根据OCP设计重构的设计
ClientInterface类是一个拥有抽象成员函数的抽象类,Client类使用这个抽象类。如果我们希望Client对象使用一个不同的服务器类,那么只需要冲ClientInterface类派生一个新的类,无需对Client类做任何改动。
Shape应用程序
应用程序中有Circle、Square列表,需求是:绘制他们
从OCP原则考虑,设计方案如下,
class Shape{
draw();
}
class Circle extends Shape{
draw(){
}
}
class Square extends Shape{
draw(){
}
}
Class DrawAllShape{
void drawAllShapes(List<Shape> lists){
for(Shape shape :lists){
shape.draw();
}
}
}
根据此设计,如果新增一个Triangle类,只需新增代码即可,无需改动上述代码,达到了开闭原则。真的吗???
并非100%封闭
如果需求要求所有圆必须在正方形之前绘制,那么DrawAllShape无法对这种变化做到封闭。要实现这个需求,我们需要修改DrawAllShape,使它首先扫描列表中的圆,然后在扫描所有的正方形。
预测变化和贴切的结构
一般来说,无论模块是多么封闭,都会存在一些无法对之封闭的变化,没有对于所有的情况都贴切的模型。
既然不能完全封闭,就要有策略的对待这个问题,设计人员必须对于他设计的模块应该对哪种变化封闭作出选择,他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。这需要设计人员具备一些从经验中获得的预测能力。
遵循OCP的代价是昂贵的,创建正确的抽象是要花费开发时间和精力的,同时,那些抽象也增加了软件设计的复杂性,开发人员有能力处理的抽象的数量也是有限的,显然,我们希望把OCP的应用限定在可能会发生的变化上。
- 只受一次愚弄
最初编写代码的时候,假设变化不会发生,当变化发生了,我们就创建抽象来隔离以后发生的同类变化。 - 刺激变化
尽早查明可能发生的变化,尽早接受变化带来的改变。
Shape改造
class Shape{
draw();
//precedes();
precedes(OrderRule order){
}
}
Class DrawAllShape{
sort(list);
void drawAllShapes(List<Shape> lists){
for(Shape shape :lists){
shape.draw();
}
}
}
precedes()考虑到枚举排序的话违背了OCP,可以将规则定义起来,改造成precedes(OrderRule order),排序规则从OrderRule中读取,此时对与Shape绘制顺序的变化不封闭的唯一部分就是OrderRule。
结论
在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以的带来面向对象技术所声称的巨大好处(灵活性,可重用性,可维护性)。对于应用程序中的每个部分都肆意的进行抽象同样不是一个好主意。正确的做法是,开发人员应该仅仅对程序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。