Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5833026
  • 博文数量: 671
  • 博客积分: 10010
  • 博客等级: 上将
  • 技术积分: 7310
  • 用 户 组: 普通用户
  • 注册时间: 2006-07-14 09:56
文章分类

全部博文(671)

文章存档

2011年(1)

2010年(2)

2009年(24)

2008年(271)

2007年(319)

2006年(54)

我的朋友

分类: 项目管理

2007-07-16 11:16:01

 

类设计原则:

  1. SRP,单一职责原则,一个类应该有且只有一个改变的理由。
  2. OCP,开放封闭原则,你应该能够不用修改原有类就能扩展一个类的行为。
  3. LSP,Liskov替换原则,派生类要与其基类自相容。
  4. DIP,依赖倒置原则,依赖于抽象而不是实现。
  5. ISP,接口隔离原则,客户只要关注它们所需的接口。

包内聚性原则:

  1. REP,重用发布等价原则,重用的粒度就是发布的粒度。
  2. CCP,共同封闭原则,包中的所有类对于同一类性质的变化应该是共同封闭的。 
  3. CRP,共同重用原则,一个包中的所有类应该是共同重用的。

包耦合性原则:

  1. ADP,无环依赖原则,在包的依赖关系图中不允许存在环。
  2. SDP,稳定依赖原则,朝着稳定的方向进行依赖。
  3. SAP,稳定抽象原则,包的抽象程度应该和其稳定程度一致

_________________________________________________________________________


一.单一职责原则(The Sigle Responsibility Principle -----SRP)

一个类只能因为一个因素而改变,不然则导致易碎性,因为任何一个因素导致变化都会要修改这个类,尽管这些因素可能没有一点关系。

如果一个类承担的职责过多,就等于把这些职责耦合在一起。SPR中,我们把职责定义为变化的原因”(a reason for change)。如果你想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。例如考虑以下程序中Modem接口。看起来非常合理。该接口声明的4个函数确实是Modem所具有的功能。

interface Modem

{

  public void dial(string pno);

  public void hangup();

  public void send(char c);

  public void recv();

}

     然而,该接口中确有两个职责,第一个职责是连接管理;第二个职责是数据通信。dialhangup函数负责连接处理,而sendrecv函数进行通信。这两个职责是否应该分开依赖于程序变化的方式。如果应用程序的变化会影响连接函数的签名,那么这个设计就具有僵化的臭味,因为调用sendrecv的类必须要重新编译,部署的次数常常会超过我们希望的次数在这种情况下,这两个职责就应该分离。另一方面,如果应用程序的变化方式总是导致这两个职责同时变化,那么就不必分离它们。实际上,分离它们就会具有不必要的复杂性的臭味。

     下图展示了一种常见的违反SPR的情形。Employee类包含了业务规则和对于持久化的控制。这两个职责在大多数情况下不该混合在一起。业务规则往往会频繁的变化,而持久化的方式却不会如此频繁的变化,并且变化的原因也是完全不同的。

 

 

 

二.开放-封闭原则(The Open-Close Principle ---OPC)

遵循开放-封闭原则设计的模块有两个主要的特征:

1.       “对于扩展是开放的”(Open for extension)

这意味着模块的行为时可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。

2.       “对于更改是封闭的”(Closed fo modification)

对模块行为进行扩展时,不必改动模块的源代码,模块的二进制可执行版本,无论是可链接库,DLL或者JAVA.jar文件,都无需改动。

关键是抽象 模块可以操作一个抽象体。由于模块依赖一个固定的抽象体,所以它对于更改可以是关闭的。同时,通过从这个抽象体派生,也可以扩展此模块的行为。

 

如上图所示,如果Client需要一个不同的服务器类,那么只需要从ClientInterface接口派生一个新的类,无需对Client类做任何改动。

     如果我们预测到了一个变化就可以设计一个抽象来隔离它。这就导致了一个麻烦的结果,因为我们不可能预测到所有的变化,所以无论模块多么“封闭”,都会存在一些无法对之封闭的变化。没有对于所有的情况都贴切的模型。既然不可能完全封闭,那么就必须有策略地对待这个问题。也就是说,设计人员必须对他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生变化种类,然后构造抽象来隔离那些变化。

我们如何知道哪个变化有可能发生呢?我们进行适当的调查,提出正确的问题,并且使用我们的经验和一般常识。最终,我们会一直等到变化发生时才采取行动。

只受一次愚弄:最初编写代码时,假设变化不会发生。当变化发生时,我们就创建抽象来隔离以后发生同类变化。如果我们决定接受第一颗子弹,那么子弹来的越早对我们越有利。我们希望在开发工作展开不久就知道可能发生的变化。查明可能发生的变化所等待的时间越长,要创建正确的抽象就越困难。

因此我们需要刺激变化:

1.         我们首先编写测试。测试描述了系统的一种使用方法。通过首先编写测试,我们迫使系统成为可测试的。在一个具体有可测试性的系统中发生变化时,我们可以坦然对之。因为我们已经构建了使系统可测试的抽象。并且通常这些抽象中的许多都会隔离以后发生的其他种类的变化。

2.         使用很短的迭代周期进行开发。

3.         首先开发最重要的特性。

4.         尽早的发布软件。尽可能快地,尽可能频繁地把软件展示给客户和使用人员。

 

注意:

对于应用程序中的每个部分都肆意地进行抽象不是一个好主意。正确的做法是,开发人员应该仅仅对程序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。

 

 

三.里氏替换原则(The Liskov Substitution Principle --LSP)

        子类型必须能够替代它们的基类型。基类用户不必为了使用派生类而做任何特殊的事情。不必向下转型,实际上,它们根本不需要了解派生类,甚至不必知道是否存在派生类。基类的方法在子类中必须有具体意义,不能忽略。一个例子是由结算工资的方法CalcPay()可以看出来志愿者类VolunteeEmplayee不应该属于Employee的子类。

         想想违反该原则的后果,LSP的重要性就不言而喻了。假设有一个函数f,它的参数为指向某个基类B的指针。同样假设有B的某个派生类D,如果把D的对象作为B类型传递给f,会导致f出现错误行为。那么D就违反了LSP。显然,D对于f来说是脆弱的。

         一个反面的例子:正方形继承于矩形,我们首先注意到出问题的地方式,Square类并不同时需要成员变量itsHeightitsWidth。而且Square会继承SetWidthSetHeight函数。这两个函数对于Square是不合适的。不过我们可以暂时用下面方法避免。

      void Square::SetWidth(double w)

{

    Rectangle::SetWidth(w);

    Rectangle::SetHeight(w);

}

void Square::SetHeight(double h)

{

    Rectangle::SetWidth(h);

    Rectangle::SetHeight(h);

}

这样,当设置Square对象的宽时,它的长也会相应地改变。当设置长的时候,宽也会随之改变。

但考虑下面这个函数:

void g(Rectangle& r)

{

   r.SetWidth(5);

   r.SetHeight(4);

   assert(r.Area() == 20);

}

这个函数认为传递进来的一定时Rectangle,并调用了其成员函数SetWidthSetHeight。对于Rectangle来说,此函数运行正确,但如果传递进来的是Square对象就会发生断言错误。

函数g的表现说明有一些使用指向Rectangle对象的指针或者引用的函数,不能正确地操作Square对象。对于这些函数来说,Square不能替换Rectangle,因此SquareRectangle之间的关系是违反LSP的。

LSP让我们得出一个非常重要的结论:一个模型,如果孤立地看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。例如,如果孤立地看,RectangleSquare是自相容的且有效的。但对基类做出合理假设的程序员来看,这个继承模型就是有问题的。

        那么究竟是怎么回事呢?SquareRectangle这个显然合理的模型为什么会有问题?毕竟,Square应该是Rectangle。难道它们之间不存在IS-A关系?

        对于不是g函数的编写者来说,正方形可以是长方形,但从g的角度来看,Square对象绝对不是Rectangle对象。从行为方式的角度来看,Square不是Rentangle,对象的行为才是软件真正所关注的问题。LSP清楚地指出,OODIS-A关系就是行为方式而言的,行为方式是可以进行合理假设的,是客户程序所依赖的。

        有一些简单的启发规则可以提供一些有关违反LSP的提示。这些规则都和以某种方式从其基类中去除功能的派生类有关。完成的功能少于其基类的派生类通常是不能替换其基类的,因此就违反了LSP。在派生类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,还是值得注意下。另外一种LSP的违规是在派生类的方法中添加了其基类不会抛出的异常。如果基类的使用者不期望这些异常,那么把它们添加到派生类的方法中就会导致不可替换性。此时要遵循LSP,要么就必须改变设用者的期望,要么派生类九不应该抛出这些异常。

        结论:正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。术语“IS-A”的含义过于宽泛以至于不能作为子类型的定义。子类型的正确定义是“可替换性的”,这里的可替换性可以通过显示或者隐式的契约来定义。

 

 

.依赖倒置原则(The Dependency Inversion Priciple ---DIP)

先考虑一下当高层模块依赖于底层模块时意味着什么。高层模块包含了一个应用程序的重要的策略选择和业务模型。正是这些高层模块才使得其所在的应用程序区别于其他。然而,如果这些高层模块依赖于底层模块,那么对底层模块的改动就会直接影响到高层模块,从而迫使它们依次作出改动,即又影响依赖此高层模块的底层模块。

Booch曾经说过:"所有结构良好的面向对象构架都具有清晰的层次定义,每个层次通过一个定义良好的,受控的接口向外提供一组内聚的服务."对这个陈述可能会导致设计如下的结构:

 

这看起来似乎是正确的,然而它存在一个隐伏的错误特征,那就是:策略层对于其下一直到实现层的改动都是敏感的.这种依赖关系是传递的.策略层依赖于某些依赖于实现层的层次:因此策略层传递性的依赖于实现层。这是非常糟糕的。

下图展示了一个更为合适的模型。每个较高层次都为它所需要的服务声明一个抽象接口,较低的层次实现了这些接口,每个高层类都通过该抽象接口使用下一层,这样高层就不依赖于底层。

 

这里的倒置不仅仅是依赖关系的倒置,它也是接口所有权的倒置。我们通常会认为工具库应该有它们自己的接口。但是当应用了DIP时,我们发现往往是客户拥有抽象接口,而它们的服务者则从这些抽象接口派生。这就是著名的Hollywood原则:“Don’t call us,we’ll call you.”底层模块实现了高层模块中声明并被高层模块调用。

根据这个启发式规则,可知:

1.       任何变量都不应该持有一个指向具体类的指针或者引用。

2.       任何类都不应该从具体类派生。

3.       任何方法都不应该复写它的任何基类中已实现了的方法。

当然,每个程序中都会有违反该启发规则的情况。有时必须要创建具体类的实例,而创建这些实例的模块会依赖于它们。此外如果必须依赖具体类,最好依赖于不会改变的具体类,比如String

    什么是高层策略呢?它是应用背后的抽象,是那些不随具体细节的改变而改变的真理。它是系统内部的系统——它是隐喻。

 

 

.接口隔离原则(The Interface Segregation Principle  --ISP)

这个原则用来处理胖接口所具有的缺点,如果类的接口不是内聚的,就表示该类具有胖的接口。换句话说,类的胖接口可以分解成多组方法。每一组方法都服务于一组不同的客户程序。ISP承认存在有一些对象,它们确实不需要内聚的接口,即存在胖接口,但是ISP建议客户程序不应该看到它们作为单一的类存在。相反,客户程序看到的应该是多个具有内聚接口的抽象类。否则这个胖接口中一个并没被客户使用的方法如果改变,那么这个胖接口就要改变,从而所有客户也要改变。一个重构的方法是,让这个类派生于几个接口,而不同的用户只需引用他需要的接口。

重构中有一条准则就是“Introduce Parameter Object(引入参数对象)说的是,你常会看见特定的一组参数总是一起被传递给函数f,我们可以用一个对象包装所有这些数据,再以该对象取代它们。”其实不能简单的看见参数列就把它们规为一个对象,因为如果把它们聚合成一个对象o。那么函数就会依赖o中每一个接口。这样o中任何一个接口改变,那么函数f和函数所有客户程序都会受影响。所有要联系上下文再来看这样做到底是否值得。

常常可以根据客户所调用的服务方法来对客户进行分组。这种分组方法使得可以为每组而不是每个客户创建分离的接口。这极大地减少了服务需要实现的接口的数量,同时也避免让服务依赖于每个客户类型。有时不同客户组调用的方法会有重叠,如果重叠部分较少,那么组的接口应该分离。公用的方法应该在所有有重叠的接口中声明。服务者类会继承每一个接口,但它们中的公用方法只实现一次。

 

.迪米特法则(LoD:又称最少知识原则(LKP

如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用.如果其中一个类需要调用另一个类的方法的话,可以通过第三者转发这个调用.

缺点:

会在系统内造出大量的小方法,散落在系统的各个角落.这些方法仅仅是传递间接的调用,因此系统与系统中的商业逻辑无关.当设计师试图从一张类图看出总体的构架时,这些小方法会造成迷惑和困扰.

为了克服狭义迪米特法则的缺点,可以使用依赖倒转原则,引入一个抽象的类型引用"抽象陌生人"对象,使"某人"依赖于"抽象陌生人",换言之,就是将"抽象陌生人"变成朋友.


广义的迪米特法则:

一个模块设计得好坏的一个重要的标志就是该模块在多大的程度上将自己的内部数据与实现有关的细节隐藏起来.

信息的隐藏非常重要的原因在于,它可以使各个子系统之间脱耦,从而允许它们独立地被开发,优化,使用阅读以及修改.

迪米特法则的主要用意是控制信息的过载.在运用迪米特法则到系统的设计中时,要注意以下几点:

* 在类的划分上,应当创建有弱耦合的类.类之间的耦合越弱,就越有利于复用.

* 在类的结构设计上,每一个类都应当尽量降低成员的访问权限.

* 在类的设计上,只要可能,一个类应当设计成不变类.

* 在对其他类的引用上,一个对象对其他对象的引用应降到最低.

* 尽量限制局部变量的有效范围.

.注意:

运用这些原则的最好方法是有的放矢”,而不是主动出击,当你发现代码有问题时,才应该看这些原则是否可以解决问题.注意做大量单元测试.

阅读(2514) | 评论(0) | 转发(0) |
0

上一篇:COM线程模型

下一篇:VC快捷键

给主人留下些什么吧!~~