资源预览内容
第1页 / 共14页
第2页 / 共14页
第3页 / 共14页
第4页 / 共14页
第5页 / 共14页
第6页 / 共14页
第7页 / 共14页
第8页 / 共14页
第9页 / 共14页
第10页 / 共14页
亲,该文档总共14页,到这儿已超出免费预览范围,如果喜欢就下载吧!
资源描述
面向对像设计原则一. 类设计五原则1. 单一职责原则(Single Responsibility Principle SRP)SRP中,把职责定义为变化的原因。如果你能想到多个动机去改变一个类,那么这个类就具有多于一个的职责。这里说的变化的原因,只有实际发生时才有意义。可能预测到会有多个原因引起这个类的变化,但这仅仅是预测,并没有真的发生,这个类仍可以认为是具有单一职责,不需要分离职责。SRP就是告诉我们,一个类,只有一个引起它变化的原因,只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。 在面向对像开发过程中,遵守 SRP的好处是显而易见的。它可以让我们的代码更简单,更安全,更易于扩展。许多设计模式的运用可以帮我们遵守 SRP。例如:proxy, strategy, chain of responsibility 等。以proxy为例:有一个 DBManager,在进行数据库操作时,有形如这样的接口, DoAction()。做了如下实现:DoAction ()If( CheckPermit(m_id) )DoAction();先进行一个 ID验证,然后如果可以做操作则 DoAction。在实现数据库操作的过程中,又考虑权限验证的问题,这显然是逻辑交错。会出现什么问题?如果现在需要对不同类型的用户进行不同类型的权限验证,比如外网用户用 QQ号和密码验证,内网用户查询不需要验证,修改用员工号和动态密钥验证。这样的扩展对于上述代码来说实现的代价就是破坏原有代码结构,同时新功能使逻辑进一步交错,更加混乱。如何解决?采用 proxy模式解决此问题。实现 CheckPermitProxy,并由它负责权限验证,并持有 DBManager指针.然后以如下代码进行操作:CheckPermitProxy(DBOPManger().DoAction();这样,如果我们数据库对于内网用户,是一套验证机制,对于外网用户是另一套验证机制,则只需要实现两个不同的 proxy, CheckPermitProxyInner, CheckPermitProxyOuter,使用者的代码也十分简单:CheckPermitProxyInner(DBOPManger().DoAction();CheckPermitProxyOuter(DBOPManger().DoAction();这样的设计,另一个好处就是代码字面上完全自解释。一目了然。2. 开闭原则(the Open Closed Principle OCP)软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,引入新功能。开闭原则中“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即不应该修改原有的代码。遵守开闭原则,我们应该尽量使用面向对像技术,例如继承,多态,接口等。当然,开闭原则同样适用于非面向对象的项目。有一段关于画几何图形的代码:struct SquareShapeType itsType;double itsSide;Point itsTopLeft;/ These functions are implemented elsewhere/void DrawSquare(struct Square*)void DrawCircle(struct Circle*);typedef struct Shape *ShapePointer;void DrawAllShapes(ShapePointer list, int n)int i;for (i=0; iitsType)case square:DrawSquare(struct Square*)s);break;case circle:DrawCircle(struct Circle*)s);break;当我们要在DrawAllShapes里增加三角形时,就得改变该函数的代码。不符合OCP。当然,此类情况只需要简单运用多态,就可以使DrawAllShapes函数的代码稳定。只有真正稳定下来的代码才可以保证其安全性。class Shapepublic:virtual void Draw() const = 0;class Square : public Shapepublic:virtual void Draw() const;class Circle : public Shapepublic:virtual void Draw() const;void DrawAllShapes(Set& list)for (Iteratori(list); i; i+)(*i)-Draw();OCP在软件开发过程中直接影响产品质量。假设公司 A开发了应用库 windows.a,这个应用库随着版本的更新,功能越来越多。现有公司 B,用了这个库的 1.0版本,之后公司A开发了 1.1版本,增加了一些功能。如果公司 A没有遵守开闭原则,新加的功能不是扩展而是通过改变原来的代码实现的,比如改变了某个运用的调用过程,这会导致使用者一旦更新了新版本的应用库则也需要更改相应的调用代码或是原有可用功能出现了问题。这影响了 A公司产品的用户体验。显然不是公司 A想要看见的。开闭原则在软件开发中另一个重要的价值就是使代码稳定,BUG 收敛。任何新功能的开发,不应该影响到现有的功能。新功能只能是扩展现有的代码实现,而不能更改现有的代码。新功能的开发,带来新的 BUG,大多是在常理之中。但新功能的开发,导致原有功能的 BUG则是一个项目无法承受的。开发新功能的代价,决定了一个软件项目可以走多远。因此,几乎所有的设计模式都有这方面的考虑。例如:decorator, adapter, builder, strategy, state, bridge等。都体现了以稳定的代码应对无穷变化的思想。下面,以decorator为例说明:为何要出现装饰者这样的设计模式呢?为何不直接使用继承实现?在什么场合下装饰者比直接继承更合适?例如,咖啡屋报价软件。基于面向对像设计,可以有一个公共的接口 IDrink,之后继承至这个接口,会有 Milk,Coffee,Wine 等。可以分别实现 GetPrice()。但随着业务的扩展,比如咖啡里加糖,冰镇等,需要有不同的报价。这时 GetPrice()的实现则慢慢复杂化。出现了许多 if else(switch case)分支。每当业务扩展,或发生改变,相应的GetPrice()就在不停变化。显然不符合开闭原则。于是我们想到继续继承 Coffee,比如SugarCoffee,IceCoffee 等。这样确实符合了开闭原则,新功能不会影响到现有代码了。但是如果还有加糖冰镇呢?组合爆炸了吧!于是出现了装饰者模式,其设计类图如上图。最核心的部分就是一个装饰者可以用于装饰另一个装饰者,应对无穷组合,即充分利用了现有代码,又不需要改变他们。这样我们只需要从 IDrink派生出 IDrinkDecorator,并继承此接口,实现不同的装饰,比如 SugarDecorator, IceDecorator 等。如果要计算加糖冰镇咖啡的价格,只须要形如这样的代码:IceDecorator(SugarDecorator(Coffee().GetPrice()。总之,遵守开闭原则,在软件开发过程中十分重要。它直接关系到一个软件产品,是否处于 BUG收敛,功能完善的过程进而影响产品的质量和生命期。3. 替换原则 (the Liskov Substitution Principle LSP)子类应当可以替换父类并出现在父类能够出现的任何地方。这个原则是 Liskov于1987年提出的设计原则。它同样可以从 Bertrand Meyer 的 DBC (Design by Contract) 的概念推出。同样,几乎所有的设计模式都遵守了这条原则。Strategy, proxy,command 等。还是绘画几何图形的例子:void DrawShape(const Shape& s)if (typeid(s) = typeid(Square)DrawSquare(static_cast(s);else if (typeid(s) = typeid(Circle)DrawCircle(static_cast(s);利用 LSP和多态技术就可以避免这些类型转换,并使函数代码稳定。还有一些情况下,对 LSP的遵守就不向上面例子那样简单。有这样的一个设计,Square is a Rectangle。因为方形要求长宽相等,于是重载Rectangle的 SetWidth和 SetHeight函数,在设置长或宽之后,同时改变宽或长的值,保证方形的几何性质。看起来似乎一切都很好。但是:void g(Rectangle& r)r.SetWidth(5);r.SetHeight(4);assert(r.GetWidth() * r.GetHeight() = 20);用户在使用基类指针时,他们只知道基类接口的 precondition和 postcondition,因此,子类对这些接口都实现都需要和基类所预期的结果一致。Rectangle的 setwidth,的 postcondition是 width = w ,height = ori.height。这显然和 square的 width=w height = width不同。因此方形不能直接继承于矩行。虽然事实上 Square is a Rectangle。4. 依赖原则 (the Dependency Inversion Principle DIP)怎样的设计是一个不好的设计?显然,不满足需求的设计一定是不好的,但满足需求的设计就一定好吗?当一个程序在满足所有现有需求时,只要出现一个或以上下述情况,同样可以认为是一个不好的设计。a.增加新需求会有巨大代价,难以扩展。b.增加新需求,很容易带来原有功能的破坏。c.不能提取成独立模块以复用。一个好的设计可以让程序员生活得轻松一些!看如下 Copy类:Copy依赖于 Read Keyboard和 Write Printer,后两者有很强复用性,因为它们职责单一,且没有依赖。但 Copy几乎没有被复用的可能。假如我们现在想要将 Keyborad的内容复制到显示器上,Copy 则完全无法使用。因为它依赖于 Writeprinter,且只能在Printer上输出。如何解决?这样的设计,让 Copy包含 Reader和 Writer两个接口,而 Keyborad Reader和Printer Writer继承至他们。则可以让 copy可以简单的被复用。因为 Copy现在只依赖于Reader和 Writer,而不是一个具体的 Keyboard Reader或是 Printer Writer。更高的抽象度意味着更好的复用性和代码稳定性。在结构化设计中,我们可以看到底层的模块是对高层抽象模块的实现(高层抽象模块通过调用底层模块),比如一开始那个无法复用的 Copy。这说明,抽象的模块要依赖具体实现相关的模块,底层模块的具体实现发生变动时将会严重影响高层抽象的模块,显然这是结构化方法的一个硬伤。面向对象方法的依赖关系刚好相反,具体实现类依赖于抽象类和接口。根据 DIP,在进行业务设计时,与特定业务有关的依赖关系应该尽量依赖接口和抽象类,而不是依赖于具体类。具体类只负责相关业务的实现,修改具体类不影响与特定业务有关的依赖关系。假设,有个台灯类 Lamp ,和走道的
网站客服QQ:2055934822
金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号