开闭原则是什么
一个软件实体应该对扩展开放,对修改关闭。
这个原则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。换言之,应当可以在不必修改源代码的情况下改变这个模块的行为。
在软件系统面临新的需求时,系统的设计必须是稳定的。满足“开-闭”原则的设计,可以给一个软件系统两个无可比拟的优越性:
- 通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性;
- 已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。
如何做到开闭原则
抽象化是关键
解决问题的关键在于抽象化。在像 Java 语言这样的面对对象的编程语言里面,可以给系统定义出一个一劳永逸、不再更改的抽象设计,此设计允许有无穷无尽的行为在实现层被实现。在 Java 语言里,可以给出一个或多个抽象 Java 类或 Java 接口,规定出所有的具体类必须提供的方法的特征(Signature)作为系统设计的抽象层。这个抽象层预见了所有的可能扩展,因此,在任何扩展情况下都不会改变。这就使得系统的抽象层不需修改,从而满足了“开-闭”原则的第二条:对修改关闭。
同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了“开-闭”原则的第一条。
对可变性的封装原则
“开-闭”原则如果从另一个角度讲述,就是所谓的“对可变性的封装原则”,即找到一个系统的可变因素,将之封装起来。
考虑你的设计中什么可能会发生变化。与通常将焦点放到什么会导致设计改变的思考方式正好相反,这一思路考虑的不是什么会导致设计改变,而是考虑你允许什么发生变化而不让这一变化导致重新设计。
“对可变性的封装原则”意味着两点:
(1)一种可变性不应当散落在代码的很多角落里,而应当被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承等级结构中的具体子类。
继承应当被看做是封装变化的方法,而不应当被认为是从一般的对象生成特殊的对象的方法。
(2)一种可变性不应当与另一种可变性混合在一起。继承结构一般不应超过两层,不然就意味着将两种不同的可变性混合在一起了。
尽管在很多情况下,无法百分之百地做到“开-闭”原则,但是如果往这个方向上的努力能够得到部分的成功,也可以显著地改善一个系统的结构。
UML类图

示例代码
package principle;
/**
* 开闭原则
*
* @author asyyr
*/
public class Ocp {
public static void main(String[] args) {
new GraphicEditor().draw(new Rectangle());
}
}
// 作为使用者,新增Shape的类型,完全无需改动
class GraphicEditor {
public void draw(Shape shape) {
shape.draw();
}
}
abstract class Shape {
int m_type;
abstract void draw();
}
class Rectangle extends Shape {
private int m_type = 1;
@Override
void draw() {
System.out.println("draw " + this.m_type + " shape");
}
}
class Circle extends Shape {
private int m_type = 2;
@Override
void draw() {
System.out.println("draw " + this.m_type + " shape");
}
}
与其它设计原则的关系
做到“开-闭”原则不是一个简单的工作,但是也有规律可循,这些规律同样以设计原则的身份出现,但是它们都是“开-闭”原则的手段和工具,是附属于“开-闭”原则的。
里氏代换原则
里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。
里氏代换原则是对“开-闭”原则的补充。正如前面所谈到的,实现“开-闭”原则的关键步骤是抽象化。而基类与子类的继承关系就是抽象化的具体体现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
一般而言,违反里氏代换原则的,也违背“开-闭”原则,反过来不一定成立。
依赖倒转原则
依赖倒转原则讲的是,要依赖于抽象,不要依赖于实现。
看上去依赖倒转原则与“开-闭”原则有很大的相似之处,实际上,它们之间的关系是目标与手段之间的关系。“开-闭”原则是目标,而达到这一目标的手段是依赖倒转原则。
换言之,要想实现“开-闭”原则,就应当坚持依赖倒转原则。违反依赖倒转原则,就不可能达到“开-闭”原则的要求。
合成/聚合复用原则
合成/聚合复用原则讲的是,要尽量使用合成/聚合,而不是继承关系达到复用的目的。
显然,合成/聚合复用原则是与里氏代换原则相辅相成的,两者又都是对实现“开-闭”原则的具体步骤的规范。前者要求设计师首先考虑合成/聚合关系,后者要求在使用继承关系时,必须确定这个关系是符合一定条件的。
遵守合成/聚合复用原则是实现“开-闭”原则的必要条件;违反这一原则就无法使系统实现“开-闭”原则这一目标。
迪米特法则
迪米特法则讲的是,一个软件实体应当与尽可能少的其它实体发生相互作用。
当一个系统绵连功能扩展的时候,其中会有一些模块,它们需要修改的压力比其它一些模块要大。最后的结果可能是这些模块需要修改或者不需要修改。但是不论是哪一种情况,如果这些模块是相对孤立的,那么它们就不会将修改的压力传递给其它的模块。
这就是说,一个遵守迪米特原则设计出来的系统在功能需要扩展时,会相对更容易地做到对修改的关闭。也就是说,迪米特法则是一条通向“开-闭”原则的道路。
接口隔离原则
接口隔离原则讲的是,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。
显然,接口隔离原则与广义的迪米特法则都是对一个软件实体与其它的软件实体的通信的限制。广义的迪米特法则要求尽可能限制通信的宽度和深度。接口隔离原则所限制的是通信的宽度,也就是说,通信应当尽可能地窄。
显然,遵循接口隔离原则和迪米特法则,会使一个软件系统在功能扩展的过程中,不会将修改的压力传递到其它的对象。
在设计模式的体现
策略模式
策略模式讲的是,如果有一组算法,那么就将每一个算法封装起来,使得它们可以互换。
显然,策略模式就是从对可变性的封装原则出发,达到“开-闭”原则的一个范例,如下图所示:
在采用策略模式之前,架构师必须从“开-闭”原则出发,考虑系统是否有可能在将来引入新的折扣算法。如果确有可能,那么就应当将所有的折扣算法封装起来,因为它们是可变化的因素。系统必须能够在新的算法出现时,很方便地将新的算法插入到已有的系统中,而不必修改已有的系统。
为了使这些算法成为“即插即用(Pluggable)”的对象,必须使这些算法能够相互替换。而做到这一点的关键,就是给这些对象定义出相同的接口,也就需要有一个抽象策略角色作为这些对象所组成的等级结构的超类型。
简单工厂模式
“开-闭”原则要求系统允许新的产品加入系统中,而无需对现有代码进行修改。在简单工厂模式中,这对于产品的消费角色是成立的,而对于工厂角色是不成立的。如下图所示。每次增加一个新的产品,都需要修改工厂角色。但是产品的消费者则可以避免进行修改。
工厂方法模式
在工厂方法模式中,具体工厂类都有共同的接口,它们“生产”出很多的处于一个等级结构中的产品对象。使用这个设计的系统可以允许向系统加入新的产品类型,而不必修改已有的代码,只需要再加入一个相应的新的具体工厂类就可以了。对于增加新的产品类而言,这个系统完全支持“开-闭”原则。工厂方法模式的简略类图如下图所示:
抽象工厂模式
抽象工厂模式封装了产品对象家族的可变性,从而一方面可以使系统动态地决定将哪一个产品组的产品实例化,另一方面可以在新的产品对象引进到已有的系统中时不必修改已有的系统。换言之,在产品对象家族发生变化时,这一设计可以维持系统的“开-闭”性。抽象工厂模式的简略类图如下图所示:
建造模式
建造模式封装了建造一个有内部结构的产品对象的过程,因此,这样的系统是向产品内部表象的改变开放的。建造模式的简略类图如下图所示:
桥梁模式
桥梁模式是“对可变性的封装原则”的极好例子。在桥梁模式中,具体实现类代表不同的实现逻辑,但是所有的具体实现类又有共同的接口。新的实现逻辑可以通过创建新的具体实现类加入到系统里面。桥梁模式的简略类图如下图所示:
门面模式
假设一个系统开始的时候与某一个子系统耦合在一起,后来又不得不换成另外一个子系统,那么门面模式便可以发挥门面模式和适配器模式两种作用,将新的子系统仍然与本系统耦合在一起。这样一来,使用门面模式便可以改变子系统内部功能而不会影响到客户端。门面模式的简略类图如下图所示:
调停者模式
调停者模式使用一个调停者对象协调各个同事对象的相互作用,这些同事对象不再发生直接的相互作用。一旦有新的同事对象添加到系统中来的时候,这些已有的同事对象都不会受到任何影响,但是调停者对象本身却需要修改。换言之,调整者模式以一种不完美的方式支持“开-闭”原则。调停者模式的简略类图如下所示:
访问者模式
访问者模式使得在节点加入新的方法变得很容易,仅仅需要在一个新的访问者类中加入此方法就可以,但是访问者模式不能很好地处理增加新的节点的情况。换言之,访问者模式提供了倾斜的可扩展性设计:方法集合的可扩展性和类集合的不可扩展性。也就是说,访问者模式的使用可以使一个节点系统对方法集合的扩展开放。访问者模式的简略类图如下图所示:
迭代子模式
迭代子模式将访问聚集元素的逻辑封装起来,并且使它独立于聚集对象的封装。这就提供了聚集存储逻辑与迭代逻辑独立演变的空间,使系统可以在无需修改消费迭代子的客户端的情况下,对聚集对象的内部结构进行功能扩展。迭代子模式的简略类图如下图所示:
当学习设计模式时,要学会问一个问题:这个设计模式可以对什么样的变换开放,以及它做到这一点所付出的代价是什么。