面向对象编程原则.ppt_第1页
面向对象编程原则.ppt_第2页
面向对象编程原则.ppt_第3页
面向对象编程原则.ppt_第4页
面向对象编程原则.ppt_第5页
已阅读5页,还剩39页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

设计模式 1 导言 面向对象设计原则 目录 1面向对象的设计原则2设计模式概论3单件4观察者 面向对象的设计原则 1单一职责SRP2 OCP开闭原则3 里氏代换LSP4 依赖倒转DIP5 接口隔离ISP6 迪米特法则LOD7合成聚合复用原则 CARP Booch和Rumbaugh的新的 统一 标识符 单一职责SRP 一个优良的系统设计 强调模块间保持低耦合 高内聚的关系 在面向对象设计中这条规则同样适用 所以面向对象的第一个设计原则就是 单一职责原则 SRP SingleResponsibilityPrinciple 单一职责 强调的是职责的分离 在某种程度上对职责的理解 构成了不同类之间耦合关系的设计关键 因此单一职责原则或多或少成为设计过程中一个必须考虑的基础性原则 1 单一职责原则 SRP 一个类 最好只做一件事 只有一个引起它变化的原因 例如 在一个Game类中 可能会具有两个不同的职责 一个职责是维护创建当前轮的比赛 另一个职责是计算总比赛得分 根据srp原则 着两个职责应该分离到两个类中 Game类保持维护创建当前轮的比赛 Scorer类负责计算比赛的得分 如何要把这两个职责分离到单独的类中呢 如果一个类承担的职责过多 等于把这些职责耦合在了一起 一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力 这种耦合会导致脆弱的设计 当变化发生时 设计会遭受到意想不到的破坏 例如 考虑下图的设计 Retangle类具有两方法 如图 一个方法把矩形绘制在屏幕上 另一个方法计算矩形的面积 有两个不同的Application使用Rectangle类 如上图 一个是计算几何面积的 Rectangle类会在几何形状计算方面给予它帮助 另一个Application实质上是绘制一个在舞台上显示的矩形 Rectangle类具有了两个职责 第一个职责是提供一个矩形形状几何数据模型 第二个职责是把矩形显示在屏幕上 对于SRP的违反导致了一些严重的问题 首先 我们必须在计算几何应用程序中包含核心显示对象的模块 其次 如果绘制矩形Application发生改变 也可能导致计算矩形面积Application发生改变 导致不必要的重新编译 和不可预测的失败 一个较好的设计是把这两个职责分离到下图所示的两个完全不同的类中 这个设计把Rectangle类中进行计算的部分一道GeometryRectangle类中 现在矩形绘制方式的改变不会对计算矩形面积的应用产生影响了 1 1什么是职责 在SRP中 我们把职责定义为 变化的原因 areasonforchange 如果你能够想到多于一个的动机去改变类 那么这个类就具有多于一个的职责 有时 我们很难注意到这一点 我们习惯于以组的形式去考虑职责 classModem public voiddial pno String voidhangup voidsend c Char voidrecv 上述Modem接口 大多数人会认为这个接口看起来非常合理 该接口声明了4个函数确实是Modem所具有的功能 然而 该接口却显示出了两个职责 一个是连接管理 dial hangup 第二个是数据通信 send recv 这两个职责应该被分离开么 这依赖于应用程序的变化 是按照实际情况决定的 如果应用程序的变化会影响连接管理 那么设计就具有僵化的臭味 因为 调用send和recv的类必须要重新编辑 在这种情况下 这两个职责应该被分离 这样做会避免这两个职责耦合在一起 另一方面 如果应用程序的变化总是导致这两方面职责同时变化 那么就不必分离他们 实际上 分离他们就会具有不必要的复杂性臭味 1 2持久化 上图展示了一种常见的违反SRP的情况 Employee类包含了业务逻辑和对于持久层的控制 这两个职责在大多数情况下决不应该混合在一起 业务规则往往会频繁的变化 而持久化的方式却不会如此频繁的变化 并且变化的原因也是完全不同的 把业务规则和持久模块绑定在一起的做法是不妥的 当僵化性和脆弱性的臭味变得强烈 那么就应该使用FACADE和PROXY模式对设计进行重构 分离这两个职责 小结 SRP是所有原则中最简单的之一 也是最难正确应用的 我们会自然的把职责结合在一起 软件设计要做的许多内容 就是发现职责并把那些职责相互分离 分离的原则也不是教条性的 需要应实际需求而定 2 OCP开闭原则 ClosedforModification OpenforExtension 是所有面向对象原则的核心 软件设计本身所追求的目标就是封装变化 降低耦合 而开放封闭原则正是对这一目标的最直接体现 其他的设计原则 很多时候是为实现这一目标服务的 例如以Liskov替换原则实现最佳的 正确的继承层次 就能保证不会违反开放封闭原则 OCP的动机很简单 软件是变化的 不论是优质的设计还是低劣的设计都无法回避这一问题 OCP说明了软件设计应该尽可能地使架构稳定而又容易满足不同的需求 为什么要OCP 通常 对于开发完的代码都需要多种测试才能够投入使用 这包括 1设计人员进行初期的架构设计2要经过开发人员的单元测试 集成测试 3然后再到测试人员的白盒测试 黑盒测试 4最后还要由用户进行一定的测试 经过漫长的测试 代码才能够投入使用 但是软件产品的维护和升级又是一个永恒的话题 在维护的过程中 你可能要不断地增加一些小功能 在升级的过程中 你要增加一些较大的功能 这种功能的扩展 就要求我们改变原有的代码 但是 对原代码的修改就会深刻地影响到原来的功能的方方面面 1可能对旧代码引入了新的错误 使你不得不对旧代码进行大规模的修改 2可能引起你不得不重新构造系统的架构 3即使新增的代码对旧代码没有影响 你也不得不对原来的系统做一个全面的测试 4经过一段时间 也许你认为以前代码更好 更符合用户需求所有上述列出来的问题 都是对系统功能进行扩展所不能承受的代价 换句话说 我们设计出来的系统 一定要是扩展性良好的系统 如何才能够设计出扩展性良好的系统呢 这就需要在软件系统设计时遵守开闭原则 玉帝的智慧 玉帝招安美猴王的例子不劳师动众 不破坏天规便是 闭 收仙有道便是 开 招安之法便是玉帝天庭的 开一闭 原则 通过给美猴王封一个 弼马温 的官职 便可使现有系统满足变化了的需求 而不必更改天庭的既有秩序 如何在OO中引入OCP原则 把对实体的依赖改为对抽象的依赖就行了 下面的例子说明了这个过程 05赛季的时候 一辆F1赛车有一台V10引擎 但是到了06赛季 国际汽联修改了规则 一辆F1赛车只能安装一台V8引擎 车队很快投入了新赛车的研发 不幸的是 从工程师那里得到消息 旧车身的设计不能够装进新研发的引擎 我们不得不为新的引擎重新打造车身 于是一辆新的赛车诞生了 但是 麻烦的事接踵而来 国际汽联频频修改规则 搞得设计师在 赛车 上改了又改 最终变得不成样子 只能把它废弃 为了能够重用这辆昂贵的赛车 工程师们提出了解决方案 首先 在车身的设计上预留出安装引擎的位置和管线 然后 根据这些设计好的规范设计引擎 或是引擎的适配器 于是 新的赛车设计方案就这样诞生了 做到开闭原则 就注意以下两点 1 多使用抽象类在设计类时 对于拥有共同功能的相似类进行抽象化处理 将公用的功能部分放到抽象类中 所有的操作都调用子类 这样 在需要对系统进行功能扩展时 只需要依据抽象类实现新的子类即可 如图10 1所示 在扩展子类时 不仅可以拥有抽象类的共有属性和共有函数 还可以拥有自定义的属性和函数 2 多使用接口 与抽象类不同 接口只定义子类应该实现的接口函数 而不实现公有的功能 在现在大多数的软件开发中 都会为实现类定义接口 这样在扩展子类时实现该接口 如果要改换原有的实现 只需要改换一个实现类即可 如图各子类由接口类定义了接口函数 只需要在不同的子类中编写不同的实现即可 当然也可以实现自有的函数 Liskov 女程序员 替换原则 在一个软件系统中 子类应该可以替换任何基类能够出现的地方 并且经过替换以后 代码还能正常工作 第一个例子 正方形不是长方形 正方形不是长方形 是一个理解里氏代换原则的最经典的例子 在数学领域里 正方形毫无疑问是长方形 它是一个长宽相等的长方形 所以 我们开发的一个与几何图形相关的软件系统中 让正方形继承自长方形是顺利成章的事情 现在 我们截取该系统的一个代码片段进行分析 正方形不是长方形 长方形类Rectangle classRectangle doublelength doublewidth public doublegetLength returnlength voidsetLength doubleheight this length length doublegetWidth returnwidth voidsetWidth doublewidth this width width 正方形类Square classSquare publicRectangle public voidsetWidth doublewidth Rectangle setLength width Rectangle setWidth width voidsetLength doublelength Rectangle setLength length Rectangle setWidth length 正方形不是长方形 由于正方形的度和宽度必须相等 所以在方法setLength和setWidth中 对长度和宽度赋值相同 类TestRectangle是我们的软件系统中的一个组件 它有一个resize方法要用到基类Rectangle resize方法的功能是模拟长方形宽度逐步增长的效果 测试类TestRectangle classTestRectangle public voidresize Rectangle 正方形不是长方形 我们运行一下这段代码就会发现 假如我们把一个普通长方形作为参数传入resize方法 就会看到长方形宽度逐渐增长的效果 当宽度大于长度 代码就会停止 这种行为的结果符合我们的预期 假如我们再把一个正方形作为参数传入resize方法后 就会看到正方形的宽度和长度都在不断增长 代码会一直运行下去 直至系统产生溢出错误 所以 普通的长方形是适合这段代码的 正方形不适合 我们得出结论 在resize方法中 Rectangle类型的参数是不能被Square类型的参数所代替 如果进行了替换就得不到预期结果 因此 Square类和Rectangle类之间的继承关系违反了里氏代换原则 它们之间的继承关系不成立 正方形不是长方形 鸵鸟不是鸟 鸵鸟非鸟 也是一个理解里氏代换原则的经典的例子 鸵鸟非鸟 的另一个版本是 企鹅非鸟 这两种说法本质上没有区别 前提条件都是这种鸟不会飞 生物学中对于鸟类的定义 恒温动物 卵生 全身披有羽毛 身体呈流线形 有角质的喙 眼在头的两侧 前肢退化成翼 后肢有鳞状外皮 有四趾 所以 从生物学角度来看 鸵鸟肯定是一种鸟 我们设计一个与鸟有关的系统 鸵鸟类顺理成章地由鸟类派生 鸟类所有的特性和行为都被鸵鸟类继承 大多数的鸟类在人们的印象中都是会飞的 所以 我们给鸟类设计了一个名字为fly的方法 还给出了与飞行相关的一些属性 比如飞行速度 velocity 鸟类Bird classBird doublevelocity public voidfly Iamflying voidsetVelocity doublevelocity this velocity velocity doublegetVelocity returnthis velocity 鸵鸟不会飞怎么办 我们就让它扇扇翅膀表示一下吧 在fly方法里什么都不做 至于它的飞行速度 不会飞就只能设定为0了 于是我们就有了鸵鸟类的设计 鸵鸟类Ostrich classOstrich publicBird publicfly Idonothing publicsetVelocity doublevelocity this velocity 0 publicgetVelocity return0 鸵鸟不是鸟 好了 所有的类都设计完成 我们把类Bird提供给了其它的代码 消费者 使用 现在 消费者使用Bird类完成这样一个需求 计算鸟飞越黄河所需的时间 对于Bird类的消费者而言 它只看到了Bird类中有fly和getVelocity两个方法 至于里面的实现细节 它不关心 而且也无需关心 于是给出了实现代码 测试类TestBird classTestBird public voidcalcFlyTime Birdbird try doubleriverWidth 3000 cout riverWidth bird getVelocity endl catch cout Anerroroccured endl 鸵鸟不是鸟 如果我们拿一种飞鸟来测试这段代码 没有问题 结果正确 符合我们的预期 系统输出了飞鸟飞越黄河的所需要的时间 如果我们再拿鸵鸟来测试这段代码 结果代码发生了系统除零的异常 明显不符合我们的预期 对于TestBird类而言 它只是Bird类的一个消费者 它在使用Bird类的时候 只需要根据Bird类提供的方法进行相应的使用 根本不会关心鸵鸟会不会飞这样的问题 而且也无须知道 它就是要按照 所需时间 黄河的宽度 鸟的飞行速度 的规则来计算鸟飞越黄河所需要的时间 我们得出结论 在calcFlyTime方法中 Bird类型的参数是不能被Ostrich类型的参数所代替 如果进行了替换就得不到预期结果 因此 Ostrich类和Bird类之间的继承关系违反了里氏代换原则 它们之间的继承关系不成立 鸵鸟不是鸟 4 4鸵鸟到底是不是鸟 鸵鸟到底是不是鸟 鸵鸟是鸟也不是鸟 这个结论似乎就是个悖论 产生这种混乱有两方面的原因 原因一 对类的继承关系的定义没有搞清楚 面向对象的设计关注的是对象的行为 它是使用 行为 来对对象进行分类的 只有行为一致的对象才能抽象出一个类来 类的继承关系就是一种 Is A 关系 实际上指的是行为上的 Is A 关系 可以把它描述为 Act As 我们再来看 正方形不是长方形 这个例子 正方形在设置长度和宽度这两个行为上 与长方形显然是不同的 长方形的行为 设置长方形的长度的时候 它的宽度保持不变 设置宽度的时候 长度保持不变 正方形的行为 设置正方形的长度的时候 宽度随之改变 设置宽度的时候 长度随之改变 所以 如果我们把这种行为加到基类长方形的时候 就导致了正方形无法继承这种行为 我们 强行 把正方形从长方形继承过来 就造成无法达到预期的结果 鸵鸟非鸟 基本上也是同样的道理 我们一讲到鸟 就认为它能飞 有的鸟确实能飞 但不是所有的鸟都能飞 问题就是出在这里 如果以 飞 的行为作为衡量 鸟 的标准的话 鸵鸟显然不是鸟 如果按照生物学的划分标准 有翅膀 有羽毛等特性作为衡量 鸟 的标准的话 鸵鸟理所当然就是鸟了 鸵鸟没有 飞 的行为 我们强行给它加上了这个行为 所以在面对 飞越黄河 的需求时 代码就会出现运行期故障 鸵鸟到底是不是鸟 原因二 设计要依赖于用户要求和具体环境 继承关系要求子类要具有基类全部的行为 这里的行为是指落在需求范围内的行为 图中鸟类具有4个对外的行为 其中2个行为分别落在A和B系统需求中 系统需求和对象关系示意图 A需求期望鸟类提供与飞翔有关的行为 即使鸵鸟跟普通的鸟在外观上就是100 的相像 但在A需求范围内 鸵鸟在飞翔这一点上跟其它普通的鸟是不一致的 它没有这个能力 所以 鸵鸟类无法从鸟类派生 鸵鸟不是鸟 B需求期望鸟类提供与羽毛有关的行为 那么鸵鸟在这一点上跟其它普通的鸟一致的 虽然它不会飞 但是这一点不在B需求范围内 所以 它具备了鸟类全部的行为特征 鸵鸟类就能够从鸟类派生 鸵鸟就是鸟 所有派生类的行为功能必须和使用者对其基类的期望保持一致 如果派生类达不到这一点 那么必然违反里氏替换原则 在实际的开发过程中 不正确的派生关系是非常有害的 伴随着软件开发规模的扩大 参与的开发人员也越来越多 每个人都在使用别人提供的组件 也会为别人提供组件 最终 所有人的开发的组件经过层层包装和不断组合 被集成为一个完整的系统 每个开发人员在使用别人的组件时 只需知道组件的对外裸露的接口 那就是它全部行为的集合 至于内部到底是怎么实现的 无法知道 也无须知道 所以 对于使用者而言 它只能通过接口实现自己的预期 如果组件接口提供的行为与使用者的预期不符 错误便产生了 里氏代换原则就是在设计时避免出现派生类与基类不一致的行为 如何正确地运用里氏代换原则 里氏代换原则目的就是要保证继承关系的正确性 我们在实际的项目中 是不是对于每一个继承关系都得费这么大劲去斟酌 不需要 大多数情况下按照 Is A 去设计继承关系是没有问题的 只有极少的情况下 需要你仔细处理一下 这类情况对于有点开发经验的人 一般都会觉察到 是有规律可循的 最典型的就是使用者的代码中必须包含依据子类类型执行相应的动作的代码 动物类Animal classAnimal stringname public voidAnimal Stringname this name name voidprintName try cout Iama name endl catch cout Anerroroccured endl 猫类Cat classCat publicAnimal public Cat Stringname Animal name voidMew try cout Mew endl catch cout Anerroroccured endl 狗类Dog publicclassDog publicAnimal Dog Stringname Animal name voidBark try cout Bark endl catch cout Anerroroccured endl 测试类 TestAnimalclassTestAnimal public voidTestLSP Animal 象这种代码是明显不符合里氏代换原则的 它给使用者使用造成很大的麻烦 甚至无法使用 对于以后的维护和扩展带来巨大的隐患 实现开闭原则的关键步骤是抽象化 基类与子类之间的继承关系就是一种抽象化的体现 因此 里氏代换原则是实现抽象化的一种规范 违反里氏代换原则意味着违反了开闭原则 反之未必 里氏代换原则是使代码符合开闭原则的一个重要保证 依赖倒置DIP 1 高层模块不应该依赖于低层模块 二者都应该依赖于抽象 2 抽象不应该依赖于细节 细节应该依赖于抽象 要针对接口编程 不要针对实现编程 依赖 在程序设计中 如果一个模块a使用 调用了另一个模块b 我们称模块a依赖模块b 高层模块与低层模块 往往在一个应用程序中 我们有一些低层次的类 这些类实现了一些基本的或初级的操作 我们称之为低层模块 另外有一些高层次的类 这些类封装了某些复杂的逻辑 并且依赖于低层次的类 这些类我们称之为高层模块 高层模块包含了一个应该程序中的重要的策略选择和业务模型 正是这些高层模块才使得其所有的应用程序区别于其他 如果高层依赖于低层 那么对低层模块的改动就会直接影响到高层模块 从而迫使它们依次做出改动 具体原则是 1 任何变量都不能拥有一个具体类的指针或者引用 2 任何类都不应该从具体类派生 3 任何方法都不应该覆写基类中已经实现的方法 也就是说应当使用接口和抽象类进行变量类型声明 参数类型声明 方法返还类型说明 以及数据类型的转换等 而不要用具体类进行变量的类型声明 参数类型声明 方法返还类型说明 以及数据类型的转换等 要保证做到这一点 一个具体类应当只实现接口和抽象类中声明过的方法 而不要给出多余的方法 依赖倒置DIP 基于这个原则 设计类结构的方式应该是从上层模块到底层模块遵循这样的结构 上层类 抽象层 底层类 HighLevelClasses 高层模块 AbstractionLayer 抽象接口层 LowLevelClasses 低层模块 缺点 耦合太紧密 Light发生变化将影响ToggleSwitch 解决办法一 将Light作成Abstract 然后具体类继承自Light 优点 ToggleSwitch依赖于抽象类Light 具有更高的稳定性 而BulbLight与TubeLight继承自Light 可以根据 开放 封闭 原则进行扩展 只要Light不发生变化 BulbLight与TubeLight的变化就不会波及ToggleSwitch 缺点 如果用ToggleSwitch控制一台电视就很困难了 总不能让TV继承自Light吧 解决办法一 接口隔离ISP 一 ISP简介 ISP InterfaceSegregationPrinciple 第一 客户端不应该依赖他不需要的接口也就是对接口的细化纯洁 第二 类直接的依赖应该建立在最小的接口上面 第三 建立单一的接口几个模块就要有及格接口而不是一个庞大的臃肿的接口 其他 接口是对外的承诺 承诺的越少 月利于开发 但是开发的过程中也要注意一个度的概念 否则接口太多也不利于维护 在我们进行设计的时候 一个重要的工作就是恰当地划分角色和角色对应的接口 因此 这里的接口往往有两种不同的含义 二 举例说明 1 接口对应的角色指一个类型所具有的方法特征的集合 仅仅是一种逻辑上的抽象 接口的划分就直接带来类型的划分 这里 我们可以把接口理解成角色 一个接口只是代表一个角色 每个角色都有它特定的一个接口 这里的这个原则可以叫做角色隔离原则 例如 我们将电脑的所有功能角色集合为一起 构建了一个接口 如图10 3所示 此时 我的电脑和你的电脑要实现该接口 就必须实现所有的接口函数 显然接口混乱 并不能够满足实际的需求 我的电脑可能是用来工作和学习的 你的电脑可能是用来看电影 上网和打游戏等娱乐活动的 那我们就可以将电脑的角色划分为两类 如图10 4所示 2 角色对应的接口指某种语言具体的接口定义 有严格的定义和结构 比如Java语言里面的Interface结构 对不同的客户端 同一个角色提供宽窄不同的接口 也就是定制服务 仅仅提供客户端需要的行为 客户端不需要的行为则隐藏起来 对于图10 4中的接口定义 如果我的电脑除了工作和学习之外 还想上网 那就没办法了 必须实现娱乐电脑的接口 这样就必须实现它的所有接口函数了 此时我们需要将对应角色中的接口再进行划分 如图10 5所示 这样 经过以上的划分 如果我的电脑想增加某一项功能 只需要继承不同的接口类即可 由此可见 对接口角色的划分 是从大的类上进行划分的 对角色的接口进行的划分 是对类的接口函数的划分 它们两者由粗到细 实现了接口的完全分离 迪米特法则 LawofDemeterLoD 又叫做最少知识原则 LeastKnowledgePrinciple LKP 就是说 一个对象应当对其他对象有尽可能少的了了解 迪米特法则最初是用来作为面向对象的系统设计风格的一种法则 与1987年秋天由IanHolland在美国东北大学为一个叫做迪米特 Demeter 的项目设计提出的 因此叫做迪米特法则 LIEB89 LIEB86 这条法则实际上是很多著名系统 比如火星登陆软件系统 木星的欧罗巴卫星轨道飞船的软件系统的指导设计原则 没有任何一个其他的OO设计原则象迪米特法则这样有如此之多的表述方式 如下几种 1 只与你直接的朋友们通信 Onlytalktoyourimmediatefriends 2 不要跟 陌生人 说话 Don ttalktostrangers 3 每一个软件单位对其他的单位都只有最少的知识 而且局限于那些本单位密切相关的软件单位 就是说 如果两个类不必彼此直接通信 那么这两个类就不应当发生直接的相互作用 如果其中的一个类需要调用另一个类的某一个方法的话 可以通过第三者转发这个调用 合成 聚合复用原则 Composite AggregateReusePrinciple或CARP 定义 在一个新的对象里面使用一些已有的对象 使之成为新对象的一部分 新的对象通过向这些对象的委派达到复用这些对象的目的 应首先使用合成 聚合 合成 聚合则使系统灵活 其次才考虑继承 达到复用的目的 而使用继承时 要严格遵循里氏代换原则 有效地使用继承会有助于对问题的理解 降低复杂度 而滥用继承会增加系统构建 维护时的难度及系统的复杂度 如果两个类是 Has a 关系应使用合成 聚合 如果是 Is a 关系可使用继承 Is A 是严格的分类学意义上定义 意思是一个类是另一个类的 一种 而 Has A 则不同 它表示某一个角

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论