怎样的升级才能面对需求的改变却可以保持相对稳定,从而使得系统可以在第一个版本以后不断推出新的版本呢?
开放-封闭原则(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. Attribute与Property关系

    总的来说,其实是HTML Attribute 与 DOM property之间的关系. 1 什么是Property? JS DOM Object对象有property.一个property可能是不同数 ...

  2. input里面的提示文字修改(placeholder里的文字修改,el-input也适用)

    input::-webkit-input-placeholder { /* WebKit browsers */ color: red; } input:-moz-placeholder { /* M ...

  3. 解决点击空<a>标签返回页面顶部的问题

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. Node学习之(第三章:仿Apache显示目录列表的功能)

    前言 今天咱们用Node.js中的核心模块以及上节学习的模板引擎art-template来实现服务器软件Apache的大体功能.用过Apache的朋友都知道,我们只需把本地文件放置在Apache的ww ...

  5. sql server 处理特殊字符问题

    对于中文版的SQL SERVER,默认安装后使用的默认排序规则为Chinese_PRC_CI_AS,在此排序规则下,使用varchar类型来可以“正常存取”存放中文字符以及一些东南亚国家的字符, 同时 ...

  6. CentOS7- ABRT has detected 1 problem(s). For more info run: abrt-cli list --since 1548988705

    CentOS7重启后,xshell连接,后出现ABRT has detected 1 problem(s). For more info run: abrt-cli list --since 1548 ...

  7. 编译制作Linux 3.18内核rpm包(升级centos6.x虚拟机内核)

    介绍 openstack平台需要使用各种Linux发行版模板镜像,其制作方法主要有两种,要么是基于各大Linux发行版ISO光盘手动制作,要么是使用官方提供的模板镜像再做修改 之前制作的opensta ...

  8. windows系统开机执行文件

    以下都是在windows系统下执行的 开机自启程序 新建xxx.bat的文件 编辑自己的bat文件,将所要加载的项目引导进来 注意: 如果有相对路径的话,就需要先加载项目,在用python 执行脚本程 ...

  9. puppeteer UI自动化测试demo(一)

    一.简介 这个不大常见,比较常见的是selenium和weddriver: 所以就增加一个说明,来自官网的. Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTool ...

  10. node+express 搭建本地服务

    首先,得有node环境,其次建个项目 目录例如  酱紫! 再次 写server.js,当然你可以换个名字a.js .b.js.why.js随你喜欢 var express = require('exp ...