软件设计模式详解:OCP原则

软件设计模式详解:OCP原则

看到两篇关于OCP的文章, 纳之.

原文:   http://www.cnblogs.com/muzongyan/archive/2010/08/05/1793454.html

    http://blog.csdn.net/beyondhaven/article/details/6821091

定义:

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则的含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。

软件实体包括以下几个部分:

  • 项目或软件产品中按照一定的逻辑规则划分的模块
  • 抽象和类
  • 方法

开闭原则是为软件实体的未来事物而制定的对现行开发设计进行约束的一个原则。

注意:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段了。

变化的类型:

  • 逻辑变化
  • 子模块变化
  • 可见试图变化

一个项目的基本路径应该是这样的:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码修改,保持历史代码的纯洁性,提高系统的稳定性。

开闭原则的重要性:

世界是变化的(而且变化很快),软件是对现实的抽象。---->软件必须能够扩展。

如果任何修改都需要改变已经存在的代码,那么可能导致牵一发动全身现象,进而导致雪崩效应,使软件质量显著下降。

软件实体(类、模块、函数等)应该是可以扩展的,同时还可以是不必修改的,更确切的说,函数实体应该:

(1)对扩展是开放的

当应用的需求变化时,我们可以对模块进行扩展,使其具有满足改变的新的行为。即:我们可以改变模块的功能

(2)对更改是封闭的

对模块进行扩展时,不必改动模块已有的源代码或二进制代码。

  • 开闭原则对测试的影响

开闭原则可是保持原有的测试代码仍然能够正常运行,我们只需要对扩展的代码进行测试就可以了。

  • 开闭原则可以提高复用性

在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。

  • 开闭原则可以提高可维护性
  • 面向对象开发的要求

如何使用开闭原则:

  • 抽象约束

第一,通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;

第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;

第三,抽象层尽量保持稳定,一旦确定即不允许修改。

  • 元数据(metadata)控制模块行为

元数据就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

Spring容器就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control)

  • 制定项目章程

在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。

  • 封装变化

对变化的封装包含两层含义:

第一,将相同的变化封装到一个接口或者抽象类中;

第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

实现OCP的关键是抽象:

例1:既不开放也不封闭的Client:

软件设计模式详解:OCP原则

问题:
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();
};

修改后的设计:

软件设计模式详解:OCP原则

  • 设计中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,如果需要处理的几何图形中再加入“三角形”将引发大量的修改。

  •  僵化的

增加Triangle会导致Shape、Square、Circle以及DrawAllShapes的重新编译和部署

  •  脆弱的

因为存在大量的既难以查找又难以理解的Switch和If语句,修改稍有不慎,程序就会莫明其妙的出错

  •  牢固的

想在一个程序中复用DrawAllShapes,都必须带上Circle、Square,即使那个程序不需要他们

例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----封装思想的体现

对可变性的封装原则:

  •  找到一个系统的可变因素,将之封装起来。
  •  考虑你的设计中什么会发生变化-------对应思路:什么会导致设计改变

具体的:

  •  一种可变性不应该散落在代码的很多角落里,而应被封装在一个对象里。(继承可看作封装变化的方法。)
  •  一种可变性不应与另一种可变性混在一起。(继承层次不应太多。)