怎样的升级才能面对需求的改变却可以保持相对稳定,从而使得系统可以在第一个版本以后不断推出新的版本呢?
开放-封闭原则(The Open-Closed Principle, OCP)为我们提供了指引。
软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。
如果程序中一处改动就会产生连锁反应,导致一系列相关模块的改动,那么设计就具有僵化性的臭味。
OCP建议我们应该对系统进行重构,这样以后对系统再就行那样的改动时,就不会导致更多的改动。
如果正确地应用OCP,那么以后再进行同样的改动时,就只需要添加新的代码,而不必改动已经正常运行的代码。

1. 描述

遵循开发-封闭原则设计出的模块具有两个主要的特征:
(1)对于扩展是开放的Open for extension
这意味着模块的行为是可以扩展的。当应用的需求改变时,可以对模块进行扩展,使其具有满足那些改变的新行为。换句话说,可以改变模块的功能。
(2)对于更改是封闭的Closed for modification
对模块行为进行扩展时,不必改动模块的源代码或二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或Java的jar文件,都无需改动。

怎样可能在不改动模块源代码的情况下去更改它的行为呢?
怎样才能在无需对模块进行改动的情况下就改变它的功能呢?

2. 关键是抽象

在C++、Java或其他任何的OOPL中,可以创建出固定却能够描述一组任意个可能行为的抽象体。这个抽象体就是抽象基类。而这一组任意个可能的行为则表现为可能的派生类。
模块可以操作一个抽象体,由于模块依赖于一个固定的抽象体,所以它对于更改可以是关闭的。同时,通过从这个抽象体派生,也可以扩展此模块的行为。

不遵循OCP的设计。Client类和Server类都是具体类。Client类使用Server类。如果希望Client对象使用另外一个不同的服务器对象,那么就必须要把Client类中使用Server类的地方更改为新的服务器类。

上图展示了一个针对上述问题的遵循OCP的设计。在这个设计中,ClientInterface类是一个拥有抽象成员函数的抽象类。Client类使用这个抽象类;

然而Client类的对象却使用Server类的派生类的对象。如果希望Client对象使用一个不同的服务器类,只需要从ClientInterface类派生一个新的类,无需对Client类做任何修改。

Client需要实现一些功能,它可以使用ClientInterface抽象接口去描绘那些功能。ClientInterface的子类型可以以任何它们所选择的方式去实现这个接口。这样,就可以通过创建ClientInterface的新的子类型的方式去扩展、更改Client中指定的行为。

为何把抽象接口命名为ClientInterface。为何不把它命名为AbstractServer?
因为抽象类和它们的客户的关系要比和实现它们的类的关系更密切一些。

Policy类具有一组是实现了某种策略的公有函数。与Client类的函数类似,这些策略函数使用一些抽象接口描绘了一些要完成的功能。
不同的是,在这个结构中,这些抽象接口是Policy类本身的一部分。这些函数在Policy的子类型中实现。这样,可以通过从Policy类派生出新类的方式,对Policy中指定的行为进行扩展或更改。
Template Method模式:既开放由封闭的典型。

3. Shape应用程序

在一个标准的GUI上绘制圆和正方形的应用程序。圆和正方形要按照特定的顺序绘制。
创建一个列表,列表由按照适当的顺序排列的圆和正方形组成,程序遍历该列表,依次绘制出每个圆和正方形。

class Point {
double x;
double y;
} class Shape {
ShapeType itsType;
} class Circle extends Shape {
double itsRadius;
Point itsCenter;
} class Square extends Shape {
double itsSide;
Point itsTopLeft;
} enum ShapeType {
circle,
square
} public class Ocp {
void drawAllShapes(Shape[] list, int n) {
for (int i = 0; i < n; i++) {
Shape sh = list[i];
switch (sh.itsType) {
case square:
drawSquare((Square)sh);
break;
case circle:
drawCircle((Circle)sh);
break;
default:
break;
}
}
} void drawSquare(Square square) {
System.out.println(square);
} void drawCircle(Circle circle) {
System.out.println(circle);
}
}

drawAllShapes函数不符合OCP,因为它对于新的形状类型的添加不是封闭的。如果希望这个函数能够绘制包含有三角形的列表,就必须得修改这个函数。事实上,没增加一种新的形状类型,都必须要更改这个函数。
同时,在进行上述改动时,必须要在ShapeType里面增加一个新的成员,由于所有不同种类的形状都依赖于这个enum的声明,所以我们必须要重新编译所有的形状模块。并且也必须要重新编译所有依赖于Shape类的模块。

下面代码展示了符合OCP的解决方案。

class Point {
double x;
double y;
} abstract class Shape {
ShapeType itsType;
public abstract void draw();
} class Circle extends Shape {
double itsRadius;
Point itsCenter;
@Override
public void draw() {
System.out.println(this);
}
} class Square extends Shape {
double itsSide;
Point itsTopLeft;
@Override
public void draw() {
System.out.println(this);
}
} enum ShapeType {
circle,
square
} public class Ocp {
void drawAllShapes(Shape[] list, int n) {
for (int i = 0; i < n; i++) {
list[i].draw();
}
}
}

可以看出,如果想要扩展程序中drawAllShapes函数的行为(对扩展开放),使之能够绘制一种新的形状,只需要增加一个新的Shape的派生类。drawAllShapes函数并不需要改变(对修改封闭)。
这样drawAllShapes就符合OCP,无需改动自身代码,就可以扩展它的行为。
假如增加Triangle类对于这里展示的任何模块完全没有影响。为了能够处理Triangle类,需要要改动系统中的某些部分,但是这里展示的所有代码都无需改动。

上面的例子其实并非是100%封闭的,如果要求所有的圆必须在正方形之前绘制,那么程序中的drawAllShapes函数会怎样?
drawAllShapes函数无法对这种变化做到封闭。

3.1 预测变化和“贴切的”结构

如果预测到了这种变化,那么就可以设计一个抽象来隔离它。
这就导致了一个麻烦的结果,一般而言,无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化,没有对于所有的情况都贴切的模型。
既然不可能完全封闭,那么就必须由策略地对待这个问题。也就是说,设计人员必须对于他设计的模型应该对哪种变化封闭做出选择。
他必须先猜测出最可能发生的变化种类,然后构造抽象来隔离这些变化。

同时,遵循OCP的代价也是昂贵的。创建正确的抽象是要花费开发时间和精力的。同时,那些抽象也增加了软件设计的复杂性。开发人员有能力处理的抽象的数量也是有限的。
显然,希望把OCP的应用限定在可能会发生的变化上。

3.2 使用抽象获得显式封闭

用户要求在绘制正方形之前先绘制所有的圆,我们希望可以隔离以后所有的同类变化。
怎样才能使得drawAllShapes函数对于绘制顺序的变化时封闭的呢?请记住封闭是建立在抽象的基础之上的。因此,为了让drawAllShapes对于绘制顺序的变化四封闭的,需要一种“顺序抽象体”。
这个抽象体定义了一个抽象接口,通过这个抽象接口可以表示任何可能的排序策略。

class Point {
double x;
double y;
} abstract class Shape implements Comparable<Shape>{
ShapeType itsType;
public abstract void draw();
public int precedes(Shape sh) {
if (sh.itsType == ShapeType.square) {
return 1;
} else {
return -1;
}
} @Override
public int compareTo(Shape sh) {
return precedes(sh);
}
} class Circle extends Shape {
double itsRadius;
Point itsCenter;
public Circle(ShapeType shapeType) {
itsType = shapeType;
}
@Override
public void draw() {
System.out.println(this);
}
} class Square extends Shape {
double itsSide;
Point itsTopLeft; public Square(ShapeType shapeType) {
itsType = shapeType;
} @Override
public void draw() {
System.out.println(this);
}
} enum ShapeType {
circle,
square
} public class Ocp {
static void drawAllShapes(Shape[] list, int n) {
Arrays.sort(list);
for (int i = 0; i < n; i++) {
list[i].draw();
}
} public static void main(String[] args) {
Shape[] list = new Shape[5];
list[0] = new Circle(ShapeType.circle);
list[1] = new Square(ShapeType.square);
list[2] = new Circle(ShapeType.circle);
list[3] = new Square(ShapeType.square);
list[4] = new Circle(ShapeType.circle);
drawAllShapes(list, 5);
}
}

显然precedes函数以及所有Shape类的派生类中的precedes函数都不符合OCP。没有办法使得这些函数对于Shape类的新派生类做到封闭。每次创建一个新的Shape类的派生类时,所有的precedes函数都需要改动。

class Point {
double x;
double y;
} abstract class Shape implements Comparable<Shape> {
ShapeType itsType;
public abstract void draw();
public int precedes(Shape sh) {
int thisIdx = -1;
int argIdx = -1;
for (int i = 0; i < ShapeType.SORT_SHAPE_TYPE.length; i++) {
ShapeType shapeType = ShapeType.SORT_SHAPE_TYPE[i];
if (shapeType == this.itsType) {
thisIdx = i;
} if (shapeType == sh.itsType) {
argIdx = i;
}
} return thisIdx - argIdx;
} @Override
public int compareTo(Shape sh) {
return precedes(sh);
}
} class Circle extends Shape {
double itsRadius;
Point itsCenter;
public Circle(ShapeType shapeType) {
itsType = shapeType;
}
@Override
public void draw() {
System.out.println(this);
}
} class Square extends Shape {
double itsSide;
Point itsTopLeft; public Square(ShapeType shapeType) {
itsType = shapeType;
} @Override
public void draw() {
System.out.println(this);
}
} enum ShapeType {
circle,
square; public static final ShapeType[] SORT_SHAPE_TYPE = {square, circle};
} public class Ocp {
static void drawAllShapes(Shape[] list, int n) {
Arrays.sort(list);
for (int i = 0; i < n; i++) {
list[i].draw();
}
} public static void main(String[] args) {
Shape[] list = new Shape[5];
list[0] = new Circle(ShapeType.circle);
list[1] = new Square(ShapeType.square);
list[2] = new Circle(ShapeType.circle);
list[3] = new Square(ShapeType.square);
list[4] = new Circle(ShapeType.circle);
drawAllShapes(list, 5);
} }

通过这种方法,成功做到了一般情况下drawAllShapes函数对于顺序问题的封闭,也使得每个Shape派生类对于新的Shape派生类的创建或基于类型的Shape对象排序规则的改变是封闭的。
对于不同的Shape的绘制顺序的变化不封闭的唯一部分就是ShapeType对象。

4. 结论

OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处(灵活性、可用性以及可维护性)。
然而,并不是说只要使用一种面向对象语言就是遵循了这个原则。正确的做法是,开发人员应该仅仅对程序中呈现出频繁变化的那些部分作出抽象。

开放-封闭原则(OCP)的更多相关文章

  1. 开放-封闭原则(OCP)开-闭原则 和 依赖倒转原则,单一职责原则

    单一职责原则 1.单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因 2.如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会消弱或抑制这个类完成其他职责的能力. ...

  2. 开放封闭原则(OCP)

    开放封闭原则 转:http://baike.baidu.com/view/2493421.htm转:http://dev.csdn.net/article/38/38826.shtm 开放封闭原则(O ...

  3. 1开放封闭原则OCP

    一.什么是开放封闭原则 开放封闭原则(Open-Closed Principle):一个软件实体 应当对扩展开放,则修改关闭. 在设计一个模块时,应当使得这个模块可以在不被修 改的前提下被扩展.也就是 ...

  4. 设计模式学习--面向对象的5条设计原则之开放封闭原则--OCP

    一.OCP简介(OCP--Open-Closed Principle):Software entities(classes,modules,functions,etc.) should be open ...

  5. 开放-封闭原则(OCP)

    对于僵化性的臭味,应用OCP原则之后,再进行同样的改动时,只需添加新代码,而不必改动已正常运行的代码. 扩展模块行为的方式通常是修改模块的Code,不允许修改的模块常常被认为是具有固定的行为. Ope ...

  6. C++ 设计模式 开放封闭原则 简单示例

    C++ 设计模式 开放封闭原则 简单示例 开放封闭原则(Open Closed Principle)描述 符合开放封闭原则的模块都有两个主要特性: 1. 它们 "面向扩展开放(Open Fo ...

  7. 设计模式之开放-封闭原则(引申出Objective-C中继承、Category、Protocol三者的区别,这点面试常问)

    开放封闭原则(OCP原则The Open-Closed Principle)是面向对象的核心设计所在.它是说,软件开发实体(类.模块.函数等)应该可以扩展,但是不能修改. 这个原则有两个特征,一个是说 ...

  8. Observer观察者模式与OCP开放-封闭原则

    目录 场景引入 在联网坦克项目中使用观察者模式 总结 在学习Observer观察者模式时发现它符合敏捷开发中的OCP开放-封闭原则, 本文通过一个场景从差的设计开始, 逐步向Observer模式迈进, ...

  9. (转) 面向对象设计原则(二):开放-封闭原则(OCP)

    原文:https://blog.csdn.net/tjiyu/article/details/57079927 面向对象设计原则(二):开放-封闭原则(OCP) 开放-封闭原则(Open-closed ...

随机推荐

  1. tp框架防止表单重复提交

    转载 框架官方 http://www.thinkphp.cn/topic/9090.html 第三方 https://my.oschina.net/caomenglong/blog/728908

  2. MySQL JOIN 连接时,条件为以逗号分隔的字段与 ID 相匹配

    一.背景 有一张相片表,一张相片所属标签表,一张相片可以归属于多个标签,表结构如下: 现在需要通过一次查询,得到每一张照片对应的标签名称,标签名称之间以固定的分隔符连接,结果如下图: 二.查询语句 原 ...

  3. UI5-技术篇-Hybrid App-3-jsbin百度地图

    今天研究了下如何在SAPUI5中加载百度地图,现将相关过程进行备注. 1.实现思路 1.1了解百度地图相关应用过程 A.百度地图申请应用AK:http://lbsyun.baidu.com/apico ...

  4. SpringMVC、SpringFox和Swagger整合项目实例

    目标 在做项目的时候,有时候需要提供其它平台(如业务平台)相关的HTTP接口,业务平台则通过开放的HTTP接口获取相关的内容,并完成自身业务~ 提供对外开放HTTP API接口,比较常用的是采用Spr ...

  5. Android笔记(三十六) AsyncTask是如何执行的?

    在上一个例子中,我们是在LoadImage的onPostExecute中修改的UI,不是说只允许在主线程中修改UI吗?我们看一下源代码是如何操作的. MainActicity.java package ...

  6. 2013.4.29 - KDD第十一天

    今天上午在图书馆写FIrst集,真心没写出来,算法是昨天找好的,不过实现的话还是需要很大的代码量,然后就打算用郑茂或者韩冰的代码了. 晚上图书馆快关门的时候开始思考KDD的问题, 我一开始打算给中秋发 ...

  7. java AST JCTree简要分析

    JCTree简要分析 [toc] JCAnnotatedType 被注解的泛型:(注解的Target为ElementType.TYPE_USE时可注解泛型) public static class A ...

  8. ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制

    ClassLoader类加载器 Java 中的类加载器大致可以分成两类: 一类是系统提供的: 引导类加载器(Bootstrap classloader):它用来加载 Java 的核心库(如rt.jar ...

  9. 《CoderXiaoban》第八次团队作业:Alpha冲刺5

    项目 内容 这个作业属于哪个课程 任课教师博客主页链接 这个作业的要求在哪里 实验十二 团队作业8:软件测试与ALPHA冲刺 团队名称 Coderxiaoban团队 作业学习目标 (1)掌握软件测试基 ...

  10. python_并发编程——守护进程

    1.守护进程 守护进程会随着主进程的代码执行结束而结束. 语法:进程对象.daemon = True时,表示将进程设置为守护进程,一定在start之前设置. import time from mult ...