开放—封闭原则

来源:互联网 发布:sql 嵌套select语句 编辑:程序博客网 时间:2024/04/30 13:10
正如Ivar所说,“所有的系统在它的生命周期内都会改变,开发系统时期待它比第一个版本能够持续更长的时间往往另人头疼。”怎么设计才能使其面对改变比较稳定并且比第一个版本持续更长时间?Bertrand Meyer在1988年就给出了指导方针即后来创造的著名的开放-封闭原则。“软件实体(类、模块、函数等)应该对于扩展开放,但对于修改封闭。

        当对程序进行一个单一个改动会导致它所依赖的模型的一系列的改变,这种程序就是我们不想要的“坏”设计。程序变得脆弱、死板、不可预知和不能重用。开放-封闭原则通过最直接的方法改变这种状况。它这样描述:你应该设计永远不再必修的模块,当需求改变时,你通过添加新的代码扩展这些模块的行为,而不是改变已经在使用的旧代码。

   描述:

   遵守开放-封闭原则的模块有两个主要的属性。

  1. 它们对扩展开放。这意味着模块的行为可以扩展。当需求改变时我们可以通过多种方法创建模块的行为。
  2. 它们对修改封闭。模块的源代码是不可以访问的。不允许任何人去修改代码。

这两个属性看起来并不一致。通常情况下扩展一个模块的行为需要修改这个模块。一个模块通常有固定的行为通常情况下不能修改。怎么解决这两个属性不一致的问题呢?

  抽象是关键

在C++中,利用面向对象的设计,它可以设计一个固定的抽象代表一组未确定的可能的操作。这里的抽象是抽象的基类,未确定的操作组由子类来实现。模块可以操作一个抽象。这样的模块可以对修改封闭因为它依赖于一个固定的抽象,通过创建新的子类可以扩展它的行为。

如图1所示为一个简单的没有遵守开放-封闭原则的设计。Client 和 Server类都是实体类。Client类使用Server类。如果我们希望一个Client对象使用不同的服务器对象,这个Client类需要修改去创建新的服务器类。

图2显示了对应的遵守开放-封闭原则的设计。此时AbstractServer类是个拥有纯虚函数的虚类。Client类使用这个虚类。然后Client对象将使用Server类的子类。如果我们想要Client对象使用一个不同的server类,可以新建一个AbstractServer类的子类,Client类可以不用修改。

形状抽象

  考虑下面的例子,我们有一个应用需要在标准GUI上画圆形和方形。这个圆形和方形在特定的指令下被画出。通过合适的指令需要一组圆和方形,程序遍历执行指令画出每个圆形和方形。用面向过程的技术没有遵守开放-封闭原则,我们将解决这个问题如下面的程序所示,我们看到一组拥有相同的第一个元素的数据结构,但其它元素不同。第一个元素是个类型,用来判断它是圆形还是方形。函数DrawAllShapes遍历指向这些数据结构的指针的数组,检查它的类型并调用相应的函数。

Enum ShapeType {circle, square};

Struct shape

{

       ShapType itsType;

};

Struct Circle

{

       ShapeType itsType;

       Double itsRadius;

       Point itsCenter;

};

Struct Square

{

       ShapeType itsType;

       Double itsSide;

       Point itsTopLeft;

};

Void DrawSquare(struct Square*);

Void DrawCircle(struct Circle*);

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:

           DrawSqure((struct Square*)s);

       Break;

       Case circle:

              DrawCircle((struct Circle*)s);

Break;

}

}

}

       函数DrawAllShapes没有遵守开放-封闭原则,因为它没有对添加新的形状封闭。如果我想扩展函数使它画出一组形状,其中包括三角形,那么我需要修改函数。实际上每次新增加一个形状都需要修改这个函数。

       上面的程序中是一个简单的例子。在实际应用中DrawAllShapes中的switch分支会被各种各样的函数重复使用。每一个所做的事仅有一点不相同。如果添加一个新的形状意味着查找每一个switch 语句并且添加新的形状。而且其它的switch语句分支可能并不会像在DrawAllShapes里一样比较清楚的排列。于是当添加一个一个形状时查找和理解每一个switch会带来无穷的繁琐。

       下面的代码是遵守开放-封闭原则的一个方案。这种方法有个抽象的Shape类被创建,这个抽象类只有一个纯虚函数Draw。圆形和方形两个类都继承于Shape类。

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(Set<shape*>& list)

{

       for(Iterator<Shape*>i(list); i; i++)

       (*i)->Draw();

}

在这段程序中如果我们想扩展DrawAllShapes函数来画一种新的图形,这个函数不需要修改,因此它遵守了开放-封闭原则。它的行为可以被扩展而不需要对它进行修改。在现实世界中Shape类可能有多个方法,然而向应用中添加一个新的形状仍然是件很简单的事。因为需要做的就是创建一个新的继承类并且实现所有的函数,不再需要在应用中到处寻找看哪些地方需要修改。

       选择性封闭

一般来说无论程序多么优秀也难做到100%的封闭。例如我们的程序二中的DrawAllShapes函数,如果要求所有的圆形应该在方形之前被画出。此时这个函数对这类改变不再封闭。通常情况下,无论一个模块多么封闭,总是无法对一些改变封闭。既然无法做到完全的封闭,可以进行选择性封闭。即设计者需要选择程序可以对哪些需求改变封闭。这需要通过经验得来的预见性。有经验的设计者对用户和行业足够了解,能够判断每种需求改变的可能性。所以他可以确保程序对大多数需求改变封闭。

通过抽象获得明确的封闭。

我们怎么可以使DrawAllShapes函数对要求改变画图的顺序封闭?记住封闭是基于抽象的。因此为了让它对顺序封闭,我们需要“顺序抽象”。这个特定的顺序与在其它形状前画特定类型的形状有关。顺序规则意味着,给定任何两个对象,它能够发现先画哪个。因此我们可以给Shape定义一个方法Precedes,它的参数为另外一种形状并且返回一个bool类型。在C++中这个函数可以通过重载操作符“<”来表示。下面的程序显示了添加了顺序方法的Shape类。

class Shape

{

       public:

         virtual void Draw() const = 0;

         virtual bool Precedes(const Shape&) const = 0;

         bool operator<(const shape& s){return Precedes(s);}

};

现在我们有了方法来检查两个对象的顺序,我们可以对它们排序然后再画它们。下面的程序是C++的实现。

void DrawAllShapes(Set<Shape*>& list)

{

       OrderedSet<Shape*> orderdList = list;

       orderedList.Sort();

       for(Iterator<Shape*> i(orderedList); i; i++)

       (*i)->Draw();

}

它给了我们一种给对象排序的方法,并且以合适的顺序画出它们。但我们仍然没有一个像样的排序抽象。一个图形通过重载Precedes方法实现对特定的图形排序,它是怎么实现的呢?下面的代码实现了Circle::Precedes保证圆形在方形之前画。

bool Circle::Precedes(const Shape& s) const

{

    if (dynamic_cast<square*>(s))

       return true;

      else

              return false;

}

很明显这个函数并不遵守开放-封闭原则。它没办法对新添加一种图形封闭。每次添加一个新的图形,这个函数就需要修改。

通过“数据驱动”的方法实现封闭

Shape的继承类可以通过一个表驱动的方法使不用修改每一个继承类来实现封闭性。下面的代码显示了一种可能的实现方法。通过这种方法我们可以成功地实现DrawAllShapes函数对顺序问题和每个Shape的继承类对新添加一个图形的封闭性。

class Shape

{

       public:

              virtual void Draw() const = 0;

              virtual bool Precedes(const Shape&) const;

              bool operator<(const Shape& s) const{return Precedes(s);}

       private:

              static char* typeOrderTable[];

}

static char* typeOrderTable[]=

{

           “Circle”,

              “Square”,

0

};

bool Shape::Precedes(const Shape& s) const

{

     // cal the result by look up the table

}

上面的实现唯一对顺序没有实现封闭的是表本身。然而这个表可以放在它自己的模块内,与其它模块相分离,这样它的修改就不会影响到其它的模块了。

探索与约定

正如前面所说,开放-封闭原则是面向对象思想的一些探索和约定的最初的动机,下面是一些比较重要的。

使所有成员变量为私有

这是面向对象编程最基本的一条。成员变量应该只能被成员方法所见,它不应被其它类所见,即使继承类。因此它们应该被声明为private,而不是public或protected。

根据开放-封闭原则,这条约定的原因很清楚。当类的成员变量改变了,每一个依赖于这些成员的函数都需要修改。在面向对象设计中,我们认为成员方法对成员变量的改变不应该封闭。但是其它类包括子类都应该它具有封闭性。这种要求我们叫做“封装”。

现在如果我们有一个成员变量,我们永远不会去改变它,那么它还需要声明为private吗?是不是可以把它声明为private,以便让客户程序更容易访问?如果这个变量确实不会被改变,而且客户程序也遵守约定不会去改变它,那么把它声明为public完全没有问题,但是我们无法保证不会有客户程序不小心去改变它,如果这样的话其它所有依赖它的客户程序就会出错。

不要使用全局变量

它与不使用public成员变量类似。任何模块使用了全局变量都无法对其它可以对全局模块进行写操作的模块封闭。任何一个模块没有按其它模块期待的方式使用全局变量,都会影响到其它模块。很多模块都会受到某个突发奇想的操作的影响是比较危险的。

另一方面,如果一个全局变量没有太多的依赖,而且不会用不一致的方式使用,它不会有什么危害。设计者在使用全局变量前要评估它对封闭性带来的影响和它对程序带来的方便性。选择性的使用全局变量通常很廉价。

运行时变量是危险的

通常情况下认为使用dynamic_cast或任何依赖于运行时的行为都是比较危险的。下面两段程序第一段违反了开放-封闭原则,而第二段使用了dynamic_cast,但并没有违反开放-封闭原则。做为通常原则,如果依赖运行时没有违反开放-封闭原则,它是安全的。

class Shape{};

class Square:public Shape

{

     private:

            friend DrawSquare(Square*);

};

class Circle:public Shape

{

     private:

     friend DrawCircle(Circle*);

};

void DrawAllShapes(Set<Shape*>& ss)

{

     for(Iterator<shape*>i(ss); i; i++)

     {

            Circle* c = dynamic_cast<Circle*>(*i);

            Square* s = dynamic_cast<Square*>(*i);

            if(c)

                   DrawCircle(c);

            else if (s)

                   DrawSquare(s);

}

}

class Shape

{

     public:

            virtual void Draw() const = 0;

};

class Square : public Shape

{

}

void DrawSquaresOnly(Set<Shape*>& ss)

{

     for(Iterator<shape*>i(ss); i; i++)

{

            Square* s = dynamic_cast<Square*>(*i);

            if(s)

                   s->Draw();

}

}

   结论

    关于开放-封闭原则有很多可以讲的。很多情况下这条原则是面向对象的核心。遵守这条原则可以最大程序获得面向对象技术带来的好处。遵守这条原则并不仅仅表示使用面向对象的语言,而是它要求设计师认为容易被改变的部分使用抽象技术。

原创粉丝点击