外国网站建站,长春建站公司网站,原生多重筛选插件wordpress,网站的建设可以起到什么作用是什么意思目录
1#xff1a;PDF上传链接
9.1 开放-封闭原则#xff08;OCP#xff09;
9.2 描述
9.3 关键是抽象
9.3.1 shape应用程序
9.3.2 违反OCP
糟糕的设计
9.3.3 遵循OCP
9.3.4 是的#xff0c;我说谎了
9.3.5 预测变化和“贴切的”结构
9.3.6 放置吊钩
1.只受一次…目录
1PDF上传链接
9.1 开放-封闭原则OCP
9.2 描述
9.3 关键是抽象
9.3.1 shape应用程序
9.3.2 违反OCP
糟糕的设计
9.3.3 遵循OCP
9.3.4 是的我说谎了
9.3.5 预测变化和“贴切的”结构
9.3.6 放置吊钩
1.只受一次愚弄
2.刺激变化
9.3.7 使用抽象获得显示封闭
9.3.8 使用“数据驱动”的方法获取封闭性
9.4 结论 1PDF上传链接
【免费】敏捷软件开发(原则模式与实践)资源-CSDN文库
Ivar Jacobson曾说过“任何系统在其生命周期中都会发生变化。如果我们期望开发出的系统不会再第一版后就被抛弃你就必须牢牢记住一点”。Bertrand Meyer在1988年提出著名的开发-封闭原则The Open - closed Principle简称OCP为我们提供了指引。
9.1 开放-封闭原则OCP 软件实体类、模块、函数等等应该是可以扩展的但是不可修改的。 如果程序中的一处改动就会产生连锁反应导致一系列相关模块的改动那么设计就具有僵化性的臭味。OCP建议我们应该对系统进行重构这样以后对系统在进行那样的改动时就不会导致更多的修改。如果正确地应用OCP那么以后再进行同样的改动就只需要添加新的代码而不必改动已经正常运行的代码。 也许这看起来像是重所周知的可望而不可及的美好理想-----然后事实上却有一些相对简单并且有效的策略可以帮助接近这个理想。
9.2 描述 遵循开放-封闭原则设计出的模块具有两个主要的特征。它们是 1对于扩展是开放的Open for extension 这意味着模块的行为是可以扩展的当应用的需求改变时我们可以对模块进行扩展使其具有满足那些改变的新行为换句话说我们可以改变模块的功能。 2对更改是封闭的closed for modification 对模块行为进行扩展时不必改动模块的源代码或者二进制代码。模块的二进制可执行版本无论是可链接的库、dll或者Java的jar文件都无需改动。 这个两个特征好像是相互矛盾的扩展模块行为的通常方式就是修改该模块的源代码。不允许修改的模块常常都被认为具有固定的行为。 怎么可能在不改动模块源代码的情况下去更改它的行为呢怎么才能在无需对模块进行改动情况下就改变它的功能呢
9.3 关键是抽象 在C、Java或者其他任何的OOPL面向对象编程语言中可以创建出固定去能够描述一组任意个可能行为的抽象体。这个抽象体就是抽象类。而这一组任意个可能得行为则表现为可能得派生类。 模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体所以他对于更改可以是关闭的同时通过从这个抽象体派生也可以扩展此模块的行为。 9.3.1 shape应用程序 下面的例子在许多讲述OOD面向对象的设计的书中都提过。他就是声名狼藉的“shape”样列。它常常被用来展示多态的工作原理。不过这次我们将使用它来阐明OCP。 我们有一个需要再标准的GUI上面绘制圆和正方形的应用程序。圆和正方形必须要按照特定的顺序绘制。我们将创建一个列表列表由按照适当的顺序排列的圆和正方形组成程序遍历该列表依次绘制出每个圆和正方形。
9.3.2 违反OCP 如果使用C语言并采用不遵循OCP的过程化方法我们也许会得到程序9.1中所示的解决方法。其中我们看到了一组的数据结构它们的第一个成员都相同但是其余的成员都不同。每个结构中的第一个成员都是一个用来标识该结构是代表圆或者正方形的类型码。DrawAllShapes函数遍历一个数组该数组的元素是指向这些数据结构的指针DrawAllShapes函数先检查类型码然后根据类型码调用对应的函数(DrawCircle或者DrawSquare)。
程序9.1 Square/Circle问题的过程化解决方案
--shape.h --
enum ShapeType {circle,square
};
struct Shape {ShapeType itsType;
}
-circle.h ---
struct Circle {ShapeType itsType;double itsRadius;Point itsCenter;
}
-square.h ----
struct Square {ShapeType itsType;double itsside;Point itsTopLeft;
}:
--drawA11 Shapes.cc----------------
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:DrawSquare((struct Square*)s);break;case circle:Drawcircle((struct circle*)s);Break;default:break;}}
} DrawAllShapes函数不符合OCP,因为它对于新的形状类型的添加不是封闭的。如果希望这个函数能够绘制包含有三角形的列表就必须得更改这个函数。事实上每增加一种新的形状类型都必须要更改这个函数。 当然这只是一个简单的例子。在实际程序中类似DrawAllShapes函数中的switch语句会在应用程序的各个函数中重复不断地出现每个函数中switch语句负责完成的工作差别甚微。这些函数中可能有负责拖曳形状对象的有负责拉伸形状对象的有负责移动形状对象的有负责删除形状对象的等等。在这样的应用程序中增加一种新的形状类型就意味着要找出所有包含上述switch语句或者链式if/else语句的函数并在每一处都添加对新增的形状类型的判断。 更糟的是并不是所有的switch语句和if/else链都像DrawAllShapes中的那样有比较好的结构。更有可能的情形是if语句中的判断条件由逻辑操作符组合而成或者是处理方式相同的case语句被成组处理。在一些极端错误的实现中会有一些函数对于Square的处理竞然和对于circle的处理一样。在这样的函数中甚至根本就没有switch/case语句或者if/else链。这样要发现和理解所有的需要增加对新的形状类型进行判断的地方恐怕就非常的困难了。 同样在进行上述改动时我们必须要在ShapeType enum中添加·个新的成员。由于所有不同种类的形状都依赖于这个eum的声明所以我们必须要重新编译所有的形状模块。并且也必须要重新编译所有依赖于Shape类的模块。
糟糕的设计 再来回顾一下。程序9.1中的解决方法是僵化的这是因为增加Triangle会导致Shape、Square、Circle以及DrawAllShapes的重新编译和重新部署。该方法是脆弱的因为有许多其他的即难以查找又难以理解的switch/case或者lse语句。该方法是牢固的因为想在另一个程序中复用DrawAllShapes时都必须要附带上Square和Circle,即使那个新程序不需要它们。因此在程序9.1中展示了许多糟糕设计的臭味。
9.3.3 遵循OCP 程序9.2中展示了一个square/circle问题的符合OCP的解决方案。在这个方案中我们编写了一个名为Shape的抽象类。这个抽象类仅有一个名为Draw的抽象方法。Circle和Square都从Shape类派生。
程序9.2问题的OoD解决方案class Shape {public:virtual void Draw () const 0;
};class Square: public Shape {public:virtual void Draw() const 0;
};class circle: public Shape{public:virtual void Draw () const 0;
};void DrawAllShapes(vectorShape* list)
{vectorShape*::iterator I;for (i list.begin(); i ! list.end(); i) {(*i)-Draw ();}
} 可以看到如果我们想要扩展程序9.2中DrawAllShapes函数的行为使之能够绘制一种新的形状我们只需要增加一个新的Shape类的派生类.DrawAllShapes函数并不需要改变这样DrawAllShapes就符合了OCP。无需改动自身代码就可以扩展它的行为。实际上增加一个Triangle类对于这里展示的任何模块完全没有影响。很明显为了能够处理Triangle类必须要改动系统中的某些部分但是这里展示的所有代码都无需改动。 在实际的应用程序中Sape类可能会有更多的方法。但是在应用程序中增加一种新的形状类型依然非常简单因为所需要做的工作只是创建Sape类的新的派生类并实现它的所有函数。再也不需要为了找出需要更改的地方而在应用程序的所有地方进行搜寻。这个解决方案不再是脆弱的。 同时这个方案也不再是僵化的。在增加一个新的形状类型时现有的所有模块的源码都无需改动并且现有的所有二进制模块都无需进行重新构建rebuild)。只有一个例外那就是实际创建Shape类新的派生类实例的模块必须被改动。通常情况下创建Shape类新的派生类实例的工作要么是在main中或者被main调用的一些函数中完成要么是在被main创建的一些对象的方法中完成。 最后这个方案也不再是牢固的。现在在任何应用程序中重用DrawAllShapes时都无需再附带上Square和Circle。因而这个解决方案就不再具有前面提及的任何糟糕设计的特征。 这个程序是符合OCP的。对它的改动是通过增加新代码进行的而不是更改现有的代码。因此它就不会引起像不遵循OCP的程序那样的连锁改动。所需要的改动仅仅是增加新的模块以及为了能够实例化新类型的对象而进行的围绕main的改动。
9.3.4 是的我说谎了 上面的例子其实并非是100%封闭的如果我们要求所有的圆必须在正方形之前绘制那么程序9.2中的DrawAllShapes函数会怎样呢DrawAllShapes函数无法对这种变化做到封闭。要实现这个需求我们必须要修改DrawAllShapes的实现使它首先扫描列表中所有的圆然后再扫描所有的正方形。
9.3.5 预测变化和“贴切的”结构 如果我们预测到了这种变化那么就可以设计一个抽象来隔离它。我们在程序92中所选定的抽象对于这种变化来说反倒成为一种障碍。可能你会觉得奇怪还有什么比定义一个Shape类并从它派生出Square类和Cice类更贴切的结构呢为何这个贴切的模型不是最优的呢很明显这个模型对于一个形状的顺序比形状类型具有更重要意义的系统来说就不再是贴切的了。 这就导致了一个麻烦的结果一般而言无论模块是多么的“封闭”都会存在一些无法对之封闭的变化。没有对于所有的情况都贴切的模型。 既然不可能完全封闭那么就必须有策略地对待这个问题。也就是说设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类然后构造抽象来隔离那些变化。 这需要设计人员具备一些从经验中获得的预测能力。有经验的设计人员希望自己对用户和应用领域很了解能够以此来判断各种变化的可能性。然后他可以让设计对于最有可能发生的变化遵循OCP原则。 这一点不容易做到。因为它意味着要根据经验猜测那些应用程序在生长历程中有可能遭受的变化。如果开发人员猜测正确他们就获得成功。如果他们猜测错误他们会遭受失败。并且在大多数情况下他们都会猜测错误。 同时遵循OCP的代价也是昂贵的。创建正确的抽象是要花费开发时间和精力的。同时那些抽象也增加了软件设计的复杂性。开发人员有能力处理的抽象的数量也是有限的。显然我们希望把OCP的应用限定在可能会发生的变化上。 我们如何知道哪个变化有可能发生呢我们进行适当的调查提出正确的问题并且使用我们的经验和一般常识。最终我们会一直等到变化发生时才采取行动。
9.3.6 放置吊钩 我们怎样去隔离变化呢在上个世纪我们常常说的一句话是我们会在我们认为可能发生变化的地方放置吊钩(hook)。我们觉得这样做会使软件灵活一些。 然而我们放置的吊钩常常是错误的。更糟的是即使不使用这些吊钩也必须要去支持和维护它们从而就具有了不必要的复杂性的臭味。这不是一件好事。我们不希望设计背着许多不必要的抽象。通常我们更愿意一直等到确实需要那些抽象时再把它放置进去。
1.只受一次愚弄 有句古老的谚语说“愚弄我一次应感羞愧的是你。再次愚弄我应感羞愧的是我。”这也是一种有效的对待软件设计的态度。为了防止软件背着不必要的复杂性我们会允许自己被愚弄一次。这意味着在我们最初编写代码时假设变化不会发生。当变化发生时我们就创建抽象来隔离以后发生的同类变化。简而言之我们愿意被第一颗子弹击中然后我们会确保自己不再被同一只枪发射的其他任何子弹击中。
2.刺激变化 如果我们决定接受第一颗子弹那么子弹到来的越早、越快就对我们越有利。我们希望在开发工作展开不久就知道可能发生的变化。查明可能发生的变化所等待的时间越长要创建正确的抽象就越困难。 因此我们需要去刺激变化。我们已在第2章中讲述的一些方法来完成这项工作。 1我们首先编写测试。测试描绘了系统的一种使用方法。通过首先编写测试我们迫使系统成为可测试的。在一个具有可测试性的系统中发生变化时我们可以坦然对之。因为我们已经构建了使系统可测试的抽象。并且通常这些抽象中的许多都会隔离以后发生的其他种类的变化。 2我们使用很短的迭代周期进行开发个周期为几天而不是几周。 3我们在加入基础结构前就开发特性并且经常性地把那些特性展示给涉众。 4我们首先开发最重要的特性。 5尽早地、经常性地发布软件。尽可能快地、尽可能频繁地把软件展示给客户和使用人员。
9.3.7 使用抽象获得显示封闭 第一颗子弹已经击中我们用户要求我们在绘制正方形之前先绘制所有的圆。现在我们希望可以隔离以后所有的同类变化。 怎样才能使得DrawAllShapes函数对于绘制顺序的变化是封闭的呢请记住封闭是建立在抽象的基础之上的。因此为了让DrawAllShapes对于绘制顺序的变化是封闭的我们需要一种“顺序抽象体”。这个抽象体定义了一个抽象接口通过这个抽象接口可以表示任何可能的排序策略。 一个排序策略意味着给定两个对象可以推导出应该先绘制哪一个。我们可以定义一个Shpe类的抽象方法叫作Precedes.。这个方法以另外一个Shape作为参数并返回-一个bool型结果。如果接收消息的Shape对象应该先于作为参数传入的Shape对象绘制那么函数返回true。 在C中这个函数可以通过重载operator来表示。程序9.3中展示了添加了排序方法后的Shape类。 既然我们已经有了决定两个Shape对象的绘制顺序的方法我们就可以对列表中的shape对象进行排序后依序绘制。程序9.4展示了C的实现代码。 图9.3.7.1 这给我们提供了一种对Shape对象排序的方法也使得可以按照一定的顺序来绘制它们。但是我们仍然没有一个好的用来排序的抽象体。按照目前的设计Shape对象应该覆写Precedes方法来指定顺序。这究竟是如何工作的呢我们应该在Circle:Precedes成员函数中编写一些什么代码来保证圆一定会被先于正方形绘制呢请看程序9.5。 图9.3.7.2 显然这个函数以及所有Shape类的派生类中的Precedes函数都不符合OCP。没有办法使得这些函数对于Shape类的新派生类做到封闭。每次创建一个新的Shape类的派生类时所有的Precedest)函数都需要改动。 当然如果从来不需创建新的Shape类的派生类就没有关系了。另一方面如果需要频繁的创建新的Sape类的派生类这个设计就会遭到沉重的打击。我们再次被第一颗子弹击中。
9.3.8 使用“数据驱动”的方法获取封闭性 如果我们要使Shape类的各个派生类间互不知晓可以使用表格驱动的方法。程序9.6展示了一·种可能的实现。 通过这种方法我们成功地做到了一般情况下DrawAllShapes函数对于顺序问题的封闭也使得每个Shape派生类对于新的Shape派生类的创建或者基于类型的Shape对象排序规则的改变是封闭的。比如改变顺序为正方形必须最先绘制。 对于不同的Shapes的绘制顺序的变化不封闭的惟-一部分就是表本身。可以把表放置在一个单独的模块中和所有其他模块隔离因此对于表的改动不会影响到其他任何模块。事实上在C中我们可以在链接时选择要使用的表。
9.4 结论 在许多方面OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处也就是灵活性、可重用性以及可维护性。然而并不是说只要使用一种面向对象语言就是遵循了这个原则。对于应用程序中的每个部分都肆意地进行抽象同样不是一个好主意。正确的做法是开发人员应该仅仅对程序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。