设计一个模块时,应当使该模块在不被修改的前提下被扩展,即可在不必修改源代码的情况下改变该模块的行为。
陈述:
软件实体(类、模块、函数等)应该是可以扩展的,同时还可以是不必修改的,更确切的说,函数实体应该:
(1)对扩展是开放的
当应用的需求变化时,我们可以对模块进行扩展,使其具有满足改变的新的行为。即:我们可以改变模块的功能
(2)对更改是封闭的
对模块进行扩展时,不必改动模块已有的源代码或二进制代码。
分析:
- 世界是变化的(而且变化很快),软件是对现实的抽象。---->软件必须能够扩展。
- 如果任何修改都需要改变已经存在的代码,那么可能导致牵一发动全身现象,进而导致雪崩效应,使软件质量显著下降。
实现OCP的关键是抽象:
例1:既不开放也不封闭的Client:
问题:
client和server都是具体类,接口与实现没有实现分离。如果我们想要让client调用一个新的server类,那么我们不得不修改client的源代码。从而带来编译、链接、部署等一系列的问题。
class client{
server& s;
public:
client(server& SER):s(SER) {}
void useServer(){
s.ServerFunc();
}
};
class server{
int serverData;
public:
void ServerFunc();
};
修改后的设计:
- 设计中ClientInterfece类是一个拥有抽象成员函数的抽象类。Client类使用一个抽象类,然而Client的对象却是用Server类的派生类的对象。
- 如果希望Client对象使用一个不同的服务器类,那么只需从ClientInterfece类派生一个新的类,无需对Client类做任何改动。
class client{
ClientInterface& ci;
public:
client(ClientInterface &
CI):ci(CI){}
void useServer(){
ci.ServerFunc();
}
};
class ClientInterface{
virtual void ServerFunc()=0;
};
class server:public ClientInterface{
int serverData;
public:
void ServerFunc();
};
问题:
为什么上述的ClientInterface这个类要取这么个名字,而不叫AbastractServer?
其实这里面蕴含了一个思想:
——client类中更多的描述了高层的策略,而Server类中是对这些策略的一种具体实现。
- 而接口是策略的一个组成部分,它与client端的关系更加密切。
- ClientInterface中定义了client期望Server做什么,而server具体类是对client这种要求的一种具体实现。
- OCP原则要求我们清晰地区分策略和策略的具体实现形式。允许扩展具体的实现形式(开放),同时将这种扩展与策略隔离开来,使其对上层的策略透明(封闭)。
例2:
//---------shape.h-----------------
emum ShapeType{circle,square};
struct Shape{
ShapeType itsType;
};
//---------circle.h-----------------
struct Circle{
ShapeType itsType;
double itsRadius;
CPoint itscenter;
};
//---------square.h-----------------
struct Square{
ShapeType itsType;
double itsSide;
CPoint itsTopLeft;
};
//---------drawAllShapes.cpp----------
typedef struct Shape * ShapePointer;
void DrawAllShapes(ShapePointer list[], int n){
int i;
for(i=0;i<n;i++){
struct Shape* s=list[i];
switch (s->itsType){
case square:
s->Square();
break;
case circle:
s->DrawCircle();
break;
}
}}
例2的问题:
这个程序不符合OCP,如果需要处理的几何图形中再加入“三角形”将引发大量的修改。
- 僵化的
- 脆弱的
- 牢固的
例2 修改后的设计:
class Shape{
public:
virtual void Draw() const=0;
};
class Square:public Shape{
public:
virtual void Draw() const;
};
class Circle:public Shape{
public:
virtual void Draw() const;
};
void DrawAllShapes(Vector<Shape*>&
list){
vector<Shape*>::iterator i;
for(i=list.begin();i!=list.end();i++)
(*i)->Draw();
}
完全封闭了吗?
- 上述代码并不完全封闭——“如果我们希望正方形在所有圆之前绘制”会怎么样?——对绘图的顺序无法实现封闭
- 更糟糕的是,刚才的设计反而成为了实现“正方形在所有圆之前绘制”功能的障碍。
小结:
- 一般而言,无论模块多么“封闭”,都会存在一些无法对之封闭的变化
- 我们怎么办?
----这需要对领域的了解,丰富的经验和常识。
--------错误的判断反而不美,因为OCP需要额外的开销(增加复杂度)
----敏捷的思想—— 我们预测他们,但是直到我们发现他们才行动
OCP----封装思想的体现
对可变性的封装原则:
- 找到一个系统的可变因素,将之封装起来。
- 考虑你的设计中什么会发生变化-------对应思路:什么会导致设计改变
具体的:
- 一种可变性不应该散落在代码的很多角落里,而应被封装在一个对象里。(继承可看作封装变化的方法。)
- 一种可变性不应与另一种可变性混在一起。(继承层次不应太多。)
相应设计模式:
- Strategy
- Simple Factory
- Factory Method
- Abstract Factory
- Builder
- Bridge
- Façade
- Mediator
参考资源:
《设计模式:可复用面向对象软件的基础》,ERICH GAMMA RICHARD HELM RALPH JOHNSON JOHN VLISSIDES著作,李英军 马晓星 蔡敏 刘建中译,机械工业出版社,2005.6
《敏捷软件开发:原则、模式与实践》,Robert C. Martin著,邓辉译,清华大学出版社,2003.9
《设计模式解析》,Alan Shalloway等著(徐言声译),人民邮电出版社,2006.10