1.36k likes | 1.77k Views
第 9 章 面向对象设计. 面向对象的设计原则 系统设计 对象设计 设计模式 RUP 的设计活动 RUP 的实现活动. 9.1 面向对象的设计概念及原则. 1 、有关概念 面向对象设计将面向对象分析创建的分析模型变换为设计模型,它将作为软件构造的蓝图。但由于面向对象分析与设计活动是一个迭代与演化的过程,概念与表示方法的一致性使得分析与设计阶段平滑过渡。
E N D
第9章 面向对象设计 • 面向对象的设计原则 • 系统设计 • 对象设计 • 设计模式 • RUP的设计活动 • RUP的实现活动
9.1 面向对象的设计概念及原则 1、有关概念 面向对象设计将面向对象分析创建的分析模型变换为设计模型,它将作为软件构造的蓝图。但由于面向对象分析与设计活动是一个迭代与演化的过程,概念与表示方法的一致性使得分析与设计阶段平滑过渡。 传统的设计方法将问题域分解成一系列任务来完成,这些任务形成过程式软件的基本结构。面向对象方法把问题域作为一系列相互作用的对象,在此基础上构造出基于对象的软件系统结构。 面向对象设计包括系统设计和对象设计。系统设计包括如何把整个系统分解为子系统、子系统的软硬件布局等策略性决策;对象设计是根据具体的实现策略,对分析模型进行扩充。通过系统设计和对象设计产生设计模型,是进一步完成系统实现的基础。下表列出了分析模型与设计模型的区别:
分析模型 设计模型 概念模型,回避了实现问题; 物理模型,是实现蓝图 ; 对设计是通用的; 针对特定的实现; 对类型有3种构造型; 对类型有任意数量的构造型 (依赖于实现语言); 不太形式化 ; 比较形式化; 开发费用较低; 开发费用较高; 层数少; 层数多; 动态的; 动态的(特别关注时序); 勾画系统的设计轮廓; 进行系统设计; 主要通过研讨会等方式创建; 设计模型和实现模型需双向 开发; 可能不需要在整个生命周期 在整个生命周期内都应该维护 内都做维护
2、OO设计原则 (1)封装 是将一个完整的概念组成一个独立的单元,然后通过一个名字来引用它。在OO系统的较高层次,将一些相关的应用问题封装在一个子系统中,对子系统的访问是通过访问子系统的接口实现的;在较低的层次将具体对象的属性和操作封装在一个对象类中,通过类的接口访问其属性。 (2)抽象 OO方法不仅支持过程抽象还支持数据抽象。类封装了数据和操作数据的方法,类是一种包含过程抽象的数据抽象,它对外提供的公共数据接口构成了类的规格说明(类的协议)。使用者无需知道类中的具体操作是如何实现的,无需了解内部数据的具体表现方式,只要搞清它的规格说明,就可通过接口定义的操作访问类的数据。
(3)信息隐蔽 信息隐蔽是通过对象的封装实现的。类的结构分离了接口和实现,对于类的使用者来说,属性的表示和操作的实现都是隐蔽的。 (4)强内聚 • 服务内聚: 一个服务完成且仅完成一个功能。 • 类内聚: 一个类的属性和操作全部都是完成某个任务所必须的,其中不包括无用的属性和操作。 • 层内聚: 把向用户或高层提供相关服务的功能放在一起,而将其他内容排除在外。为了保证适当的层内聚,往往有严格的层次结构,高层能够访问低层的服务,而低层却不能访问高层的服务(下图描述了这种关系)。 以下的相关服务可以放在同一层:计算服务、消息或数据传输服务、数据存储服务、管理安全服务、用户交互服务、访问操作系统服务、硬件交互服务等。•
处理应用协议 用户界面 处理连接 应用逻辑 处理包 访问操作系统 访问数据库 网络通信 传输和接收 应用程序的典型层次 通信系统中的简化层次
层向外界提供服务的过程和方法通常称为应用编程接口(Application Programming Interface, API)。API的规格说明必须描述高层用来访问服务的协议,还要描述每个服务的语义和副作用。 层内聚的优点如下: • 替换高层模块对低层模块没有影响。 • 可以用等价的层替换低层,但必须复制该层所有的API,这样高层才不受影响。 其他有关传统方法中的功能内聚、通信内聚、顺序内聚、时间内聚等概念及提高内聚的原则在OO设计中仍然适用。
(5)弱耦合 OO设计中,耦合主要指不同对象(包括类、包)之间相互关联的程度,如果一个对象过多地依赖于其它对象来完成自己的工作,不仅使系统的可理解性下降,还会增加测试、修改的难度,同时降低了类的可复用性和可移植性。但对象不可能完全孤立,当两个对象必须相互联系时,只通过类的公共接口实现耦合,不应该依赖于类的具体实现细节。 设计时尽量减少对象之间发送的消息数(Meyer建议的少接口),减少消息中的参数个数(小接口),对象之间以明显和直接的方式通信,减少通信的复杂程度(显式的接口)。传统方法中有关降低耦合的原则在OO方法中仍然适用。
(6)可复用 为了提高工作效率、减少错误、降低成本,就要充分考虑软件的复用性。复用有两个方面的含义:一是尽量使用已有的类,包括开发环境提供的类库和已有的相似类。二是创建新类时考虑将来的可复用性。 类有三种复用方式: • 实例复用 由于类的封装特性,使用者不需要了解内部的实现细节就可用适当的构造函数创建需要的实例,然后向所创建的实例发送适当的消息,启动相应的服务,完成需要的任务。 设计一个可复用性好的类是一件很困难的事情,因为,类提供的服务太多,会增加接口的复杂度,降低类的可理解性;提供的服务太少,则可能会降低复用性。在设计时,需要根据具体的应用环境和以往的经验来综合考虑,设计出合适的类构件。
• 继承复用 当已有的类构件不能通过实例复用满足要求时,可以通过继承复用对已有的类构件进行修改,使它满足要求。在设计时,关键是要设计一个合理的、具有一定深度的类构件的继承层次结构。每个子类在继承父类的属性和服务的基础上,加入少量的新属性和新服务,这样做的好处是父子类的耦合度比较适当,接口简单,易于理解。 • 多态复用 多态是一种特性,这种特性使得一个属性或变量在不同的时期可以表示不同的对象。利用多态性可以使对象的对外接口更加一般化,系统运行时,根据接收消息的对象类型,由多态机制启动正确的方法,响应一个一般化的消息。
例如出现下列情况: • 操作与数据结构大小有关。 •操作与外部设备特性有关。 •实现算法将来可能会改进。 为了克服与表示方法、数据结构或硬件特点相关的操作给复用带来的困难,可以设计一个基类把与上述操作有关的服务定义为纯虚函数,然后在复用时先派生出一个新类,在新类中重新定义上述操作的算法。 设计一个可复用的软件比设计一个普通软件的代价要高,但随着这些软件被复用的次数的次数的增加,分摊到它的设计和实现成本就会降低。
(7)简洁化设计 一个软件60%的工作量是维护工作。为了便于维护,现代软件工程越来越重视软件的简洁和易于理解。做好以下几点: • 设计简单的类。避免定义太多的属性和服务。一个类的职责要清晰,易于理解也有助于复用。 • 使用简单的协议。对象之间的关联是通过消息触发的,消息过于复杂,说明对象之间的耦合程度太紧,不利于维护。 • 设计结果简洁明了。设计结果(如文档)描述用词准确、清晰、容易理解。
9.2 系统设计 和传统设计的目的相同,是为实现系统需求而对软件体系结构进行的设计。系统设计过程包括以下活动: • 划分分析模型为子系统。 • 标识问题的并发性。 • 选择软件体系结构的风格并分配子系统到处理器。 • 设计用户界面。 • 选择实现数据管理的基本策略。 • 标识全局资源及访问它们所需的控制机制。 • 为系统定义合适的控制流机制。 • 考虑边界条件。 • 评审并考虑权衡。
1、划分分析模型 以定义类、关系和行为的内聚集合,将这些设计元素包装为子系统。 定义子系统时应该遵循以下标准: • 子系统应该具有定义良好的接口,通过接口和系统的其余部分通信。 • 除了少数的“通信类”,在子系统中的类只和该子系统中的其他类协作。 • 子系统的数量不应太多。 • 子系统可以内部划分以降低复杂性。
当两个子系统互相通信时,可建立客户/服务器(C/S)结构或对等结构(P2P)。在C/S结构中,每个子系统只承担一个由客户端或服务器端隐含的角色,服务只是单向地从服务器端流向客户端;在P2P结构中,服务可以双向流动。当两个子系统互相通信时,可建立客户/服务器(C/S)结构或对等结构(P2P)。在C/S结构中,每个子系统只承担一个由客户端或服务器端隐含的角色,服务只是单向地从服务器端流向客户端;在P2P结构中,服务可以双向流动。 在划分子系统时,往往进行分层设计。系统的每一层包含一个或多个子系统,表示了完成系统功能所需的功能性的不同抽象层次。抽象级别由与其相关的处理对用户的可见程度来确定。(如PPt第6页应用程序的典型层次结构)。 Buschmann及其同事提出以下分层设计方法: • 建立分层的标准。即决定子系统将如何被组合成层 次的体系结构。
• 确定层的数量。太多使系统复杂,太少降低子系统的功能独立性。 • 命名层并将子系统分配到某个层。确信同层的子系统间的通信以及和其他层的子系统间的通信遵循软件体系结构设计思想。 • 定义每个层的接口。 • 精化子系统以建立每个层的类结构。 • 定义层间通信的消息模型。 • 评审层设计以保证层间的耦合度最小。 • 迭代以精化分层设计。
2、并发性与基于体系结构风格的子系统分配 当系统有许多并发行为时,要划分任务,简化并发行为的设计与编码。一个任务指系统中的一个过程,有时就是进程的同义词。并发任务可通过检查每个对象的状态图而定义,如果事件和变换流指明在任意时刻只有单个对象是活跃的,则是一个控制线程。即使一个对象向另一个对象发送消息,只要第一个对象等待响应,控制线程就继续。如果第一个对象不等待,则控制线程分叉。 OO系统中的任务是通过孤立控制线程而设计的。例如当SafeHome系统正在监控其传感器时,它也可以拨号到监控站以检验连接。涉及这两个行为的对象(传感器、监控站)是同时活跃的,每个对象参与一个独立的控制线程并被定义为独立的任务。如果监控和拨号活动顺序地发生,则只设计单个任务。
对象-行为模型对分析类间或子系统间的并发性提供了支持。如果类和子系统不是同时活动的,则不需要并发处理,它们可以实现在同一个处理器硬件上;如果类和子系统必须异步地或同时作用于事件,则被视为并发的。这时有两种选择:分配每个子系统到各自独立的处理器;分配子系统到同一处理器并通过操作系统特性提供并发支持。 分布式系统中,软件体系结构风格对系统分布方案具有决定性的影响。选择体系结构风格应考虑以下因素:
• 根据被开发系统的特点:如系统类型、用户需求、系统规模、使用方式等; • 网络协议:不同的网络协议支持不同的体系结构风格; • 可用的软件产品:包括网络软件、OS、DBMS、现有的数据服务器等; • 成本及其他:购置硬件及软件成本、新开发软件成本、系统的安装与维护成本。此外,如开发人员对所选择体系结构风格下实现技术的熟练程度及开发期限等。 根据以上因素选择合适的体系结构,然后将子系统分配到体系结构的节点上。
3、任务管理设计 当一个节点上有多个控制流存在时,需要设计一个对这些控制流进行协调和管理的控制流(即进程或任务)。如设计一个进程,由它负责系统的启动和初始化、其他进程的创建与撤销、资源的分配、优先级的授予等工作。 用以下几步完成任务管理的设计: • 确定要执行的任务并识别它的特征。 • 确定任务的优先级。 • 创建协调任务来协调所有其他任务。 • 为每个任务设计对象,并定义它们之间的关系。 任务应该用模版详细描述,包括任务名、描述、优先级、服务、由谁管理、如何通信以及在层次中的位置,便于编程人员实现。
4、全局资源管理 全局资源包括物理资源(磁盘驱动器、处理器、通信线路)或逻辑资源(数据库、对象)。不但有访问权限的问题,还有访问冲突的问题。所以,应该标识全局资源,并制定访问它们的策略。一般的情况下,如果资源是物理对象,则通过建立协议实现并发系统的访问;如果资源是逻辑对象, Rumbaugh建议对每个资源可创建一个“保护者”对象,控制对该资源的访问(鉴别身份、协调冲突)。
5、选择全局控制流机制 控制流是一个在处理机上顺序执行的动作序列。在分析过程中,一般不考虑控制流问题,因为假定所有的对象都能同时运行并在任何需要的时候就能执行它们的操作。系统设计的时候,就要考虑不是每个对象都能奢侈到在自己的处理器上运行。有3种可能的控制流机制: • 过程驱动控制: 控制来自程序代码中,如程序等待输入。这种控制流大多用于遗留系统并且使用过程化语言编写。当使用面向对象语言,操作的先后顺序分散在许多对象中,通过观察代码来决定输入的顺序将很困难。 • 事件驱动控制: 主循环等待外部事件,一旦事件到达就把与事件相关的信息分配给适当的对象。缺陷是错误过程会阻塞整个应用。
• 线程: 系统可以创建任意数量个线程,每个线程对应于不同的事件。如果某个线程需要更多的数据,就等待来自操作者的输入。这种控制流机制最直接,但需要比较成熟的支持线程的开发工具,特别是调试和测试工具。 一旦选定了控制流机制,就可用一组控制对象来实现它。控制对象的职责就是记录外部事件,存储它们的临时状态,并给出与外部事件相关的边界对象和实体对象的正确的操作次序。 6、数据管理设计 如何存储那些连续的、需要经常重新计算的对象?选择什么样的存储管理模式?
3种存储管理机制: (1)普通文件 由操作系统提供的存储机制,数据按字节流存储,数据操纵功能简单,适合于存储大容量的图形、图像、视频、音频等多媒体数据。数据存储时,每个类对应于一个文件,每个对象实例对应文件的一个纪录。 应用系统 文件 记录1记录2 … 记录n 对象 对象 对象 … 数据接口 用文件存储对象
数据接口部分如何设计?主要设计为其他对象提供基本保存与恢复功能的对象类。 对象存取器 从关键字换算出记录位置再保存或存取 以某种快速算法查找与关键字相符的记录 用于某些类的对象实例不便于按关键字的值排序 类-文件对照表 对象保存()对象恢复() 换算型对象存取器 查找型对象存取器 索引型对象存取器 索引表 文件记录索引 查找记录指针() *对象保存() *对象恢复() *对象保存() *对象恢复() *对象保存() *对象恢复() 文件系统数据接口的设计
问题域部分的对象通过请求数据接口部分提供的服务实现对象的存取,这些永久性对象需要增加一些属性(如类名、关键字)和请求保存与恢复的操作,往往需定义一个在较高层次上的作为存储协议的抽象类。因此对原有的对象模型要作一些修改与调整。 (2)关系数据库 提供了对数据存取、数据共享、数据完整性维护、故障恢复、事务处理等功能实现的数据操纵语言。数据以表的形式存储,表的每一列标识一个属性,每行把一个数据项标识成一个属性值的元组。不同表中的多个元组用来表示单个对象的属性。关系型数据库技术成熟,适合于大的数据集以及对属性数据的复杂查询。但关系数据库不适合储存多媒体数据和经过压缩处理过的数据(因为要求至少满足第一范式—每个属性必须是原子的,不再含有内部结构)
应用系统 由于关系数据库要求存入其中的数据符合一定的规范,因此需要对永久类的数据进行规范化(要花费代价),以消除关系中的函数依赖所带来的数据更新异常并减少数据冗余。 为了给数据查询、更新等操作带来方便,需要对永久类确定关键字,使能够唯一的确定该类的每个对象实例(该表的每个元组)的属性。 对象 对象 对象 … 数据接口 RDBMS 关系数据库 … 表1 表n 表2 用关系数据库存储对象
数据接口的实现类似文件系统,也需要提供“对象保存”和“对象恢复”的服务。执行这些服务需要知道被保存或恢复的对象的下述信息: • 它在内存中是哪个对象(以便知道从何处取得被保存的对象,或者把数据恢复到何处); • 它属于哪个类(以便知道该对象应保存在哪个数据库表中); • 它的关键字(以便知道该对象对应数据库表的哪个元组)。 (3)OO数据库 将对象和关系作为数据储存。提供了继承和抽象数据类型,不需要对象和存储实体之间的格式转换,不需要另外设计数据接口。需熟悉OODBMS提供的ODL、DML,实现对类和对象的定义以及对数据库的访问。 下图显示了使用关系数据库对类的实例的存储。
两个多对多关系的类映射为3个表(维护表、车辆零件表、零件表)。两个多对多关系的类映射为3个表(维护表、车辆零件表、零件表)。
7、确定边界条件 设计中的大部分工作都与系统稳定的状态行为有关。但必须考虑边界条件:系统如何启动、初始化、关闭以及故障处理。 初始化包括:常量、参数、全局变量、任务及保护独享的处置设置。系统关闭时,应该释放所拥有的全部资源。并发系统中必须通知其他任务,系统要关闭。 运行中出现故障的原因可能是: • 用户错误,系统应帮助用户纠正错误。 • 硬件错误,网络连接故障等情况需要保存临时状态。 • 软件故障,在程序中多设计出现故障后的出口。 系统设计是不断迭代和演化的过程,要保证设计模型是正确的、完整的、一致的、现实的、易读的。
8、评审 如果分析模型与设计模型映射(如:每个子系统都能追溯到一个用例或一个非功能需求),则设计模型是正确的; 如果每个需求和每个系统设计问题都提到了,则模型是完整的; 如果一个模型不包括任何冲突,则它是一致的; 如果模型能够实现,则它是现实的; 由非系统设计人员能够看懂模型,则模型是易读的。
9.3 对象设计 系统设计相当于大楼的建筑平面图,规定了每个房间的用途,以及房间与房间之间、房间与外部环境之间的连接机制。对象设计着重于每个房间的内部细节。 对象设计的主要任务是: • 定义对象完整的接口 • 设计对象内部结构 • 构件选择 • 重组及优化 系统分析确定了问题域对象,以及它们之间的关系、相关的属性、操作。系统设计确定了子系统和大多数重要的求解域对象。对象设计要精细这些对象(这里的对象包括子系统),并可能定义其他的求解域对象。
1、定义对象的接口 对象的接口也称为对象的协议、对象的界面。它通过定义对象可以接收的每个消息和当对象接收到该消息后完成的相关服务来描述。接口提供了一种方法,把对象基于操作的功能说明与具体实现区分开来,使得任何依赖和使用接口的客户不必依赖于接口的具体实现,有利于接口实现的替换。 接口描述可以用UML中类图一样的符号,省略属性部分,《interface》要包含在类名部分中。比较多的人喜欢用程序设计语言来定义接口,以便用编译器来发现接口描述中的错误和不一致。 下图给出了“转账” 的Java接口描述。
//provided interfaces: Public interface Transfers{ public Account create (Customer owner, Money balance, AccountNumber account_id); public void Deposit (Money amount ,String reason); public void Withdraw (Money amount, String reason); } 要确定某个对象完整的接口,必须考察与它有关的所有用例,将与它有关的所有消息抽取出来,形成该对象完整的界面。 对于包或构件,当有依赖关系指向它的时候,就有可能表示该包或构件需要提供一个接口。
2、设计对象内部结构 • 确定遗漏的属性和操作: 系统分析和设计时集中考虑应用域,忽略与实现相关的细节,这时就应该增加上。 • 指定类型,声明可见性: 属性:确定类型、数据结构。除了分析活动中确定的属性,还包括一些其他属性,这些属性用来表示和其他类的对象关联的对象引用(关联的实现)。 操作:确定参数、返回值及类型。 为了确定每个属性和操作的访问权限,UML定义了3种可见性符号,即在属性和操作的说明前加上前缀:
-:私有的, 只能由定义它的类访问,子类和其他类 都不能访问。 +:公有的, 任何类都可以访问。公有的操作确定了 对象的接口。 #:保护的, 可以由定义它的类以及该类的子类访问。 • 设计关联: OOPL一般不提供“关联”的直接实现,一般用指针或对象引用来实现关联。(有UML建模工具自动完成关联到引用的转换)。 单向一对一关联: ZoomIn MapArea ZoomIn MapArea 1 1 -Target: MapArea
对于双向一对一关联的实现,则在MapArea中设置一个引用ZoomIn的属性。并增加相应的操作,修改属性并避免无穷循环。 一对多关联: Element Layer 1 * Element Layer -Containe: Layer -LayerEle: Set 双向一对多关联的实现 Layer的Set取决关系的约束条件。如层中的元素要排序,则用Array或Vector代替Set。
Element Record * * 多对多关联: Element Record -RecordEle: Set -Containe: Set 双向多对多关联的实现 对于多对多关联,可以将 “关联” 作为一个独立的类,形成两个二元关系,可降低重数,方便实现。
• 设计操作的算法 分析类的状态图,从每个状态转移前后的动作说明获取每个方法体的逻辑结构。而顺序图中的消息一般对应状态图中引起状态转移的事件或动作。 消息 方法1 方法1过程体 类名 可见性:属性列表 可见性:操作1(参数表) 可见性:操作2(参数表) …… 消息 方法2 方法2过程体 … 消息 方法n 方法n过程体
3、构件选择 选择系统运行的软、硬件平台,包括商品构件(更可靠、有效、健壮)、DBMS、中间件、企业应用程序框架(特定的应用)等,目的是尽可能多地减少需要开发的自定义对象的数量。由于商品构件支持大多数系统,较为复杂,需要学习的投入,可能还要作适应性修改。 4、重组与优化 (1)提高可复用性 对象设计给出了开发阶段中再次检查应用程序和求解对象间继承层次的机会。 设计完善的继承层次的主要优点是: • 可以复用更多的代码,产生较少的冗余。 • 代码是可扩展的,可用来创建更特别的类。
但是通过继承复用是有代价的,开发人员需要正确的预见所创建的类的哪些行为需要共享、哪些行为需要由以后的新类细化,通常还不会知道以后所有可能的新类。另外,一旦开发人员为共享代码定义了继承层次,抽象类的接口难以改变,因为许多子类和客户类都依赖它们。但是通过继承复用是有代价的,开发人员需要正确的预见所创建的类的哪些行为需要共享、哪些行为需要由以后的新类细化,通常还不会知道以后所有可能的新类。另外,一旦开发人员为共享代码定义了继承层次,抽象类的接口难以改变,因为许多子类和客户类都依赖它们。 设计通过继承层次复用的方法是: • 检查大量相似的类,抽取出它们的共同行为。 • 给出一层抽象概念,并从预期的变化中抽取出一个具体类。如AbstractFactory等设计模式(见下一节“设计模式”)都使用了继承来防止预期的变化。
(2)优化访问路径 效率低的系统性能的常见原因是访问必须的信息时对多个关系的重复遍历。为了识别较低的访问路径,Rumbaugh建议对象设计者应该考虑下列问题: • 对于每个操作:需要多少次遍历?遍历哪些关系?常用的操作不应该有许多遍历,应该直接通信。如果缺少直接通信,应该在两个对象间增加另外的关系。有一个Demeter法则,称“只同你的直接朋友对话”,指在软件设计中,一个方法只与由关联连接的相邻对象通信。好处:易理解、易修改、效率高。 • 对于每个关系:有“多重”关系?重数是必需的吗?检索过程中,关系中的“多”端是否经常出现?如果有,应该试着将“多”减少为“1”。否则,应为“多”端排序或建立索引以改进访问时间。
效率低的另一个原因是过多的建模。分析时确定了许多类结构,但设计时发现没有任何意义的信息。因此对象设计者应该问: • 对于每个属性:哪些操作用到了这个属性?只有 set()、get()操作吗?如果是,能否移到调用它的对象中去?如果某些类有很少的属性和行为,并且与其它类相关,可将这些类退化成属性(减少了类的数目)。这样做的目的是使模型变得简单、直接。
9.4 设计模式 1、概述 模式 (Pattern)是解决特定领域问题的经验,可以帮助人们在软件开发过程中对于经常重复出现的问题制定成功解决的方案。 模式的概念最初来自于建筑学领域,用模式描述建筑物的建筑元素(Alexander,1979),它合并了被认为是好的设计的实践经验。90年代中期,软件设计人员认识到了这些重复出现的软件设计问题。94年Gamma等4人(简称“Gang of Four”)合著的《设计模式:可复用面向对象软件的基础》提出了用设计模式进行解决,并对设计模式进行了分类描述和解释。96年由Buschmann等5人合著的《面向模式的软件体系结构》将模式跨越不同的抽象层次,提出了高层的体系结构模式、中层的设计模式和低层的习惯用法。本章主要针对设计模式进行讨论。
Gamma提出:设计模式解决特定的设计问题,并使得面向对象设计更灵活、优美和可复用。它们通过将新的设计基于以前的经验之上而帮助设计者复用成功的设计。熟悉这样的模式的设计者可以立即应用它们到设计问题中,而不需要重新去发现它们。Gamma提出:设计模式解决特定的设计问题,并使得面向对象设计更灵活、优美和可复用。它们通过将新的设计基于以前的经验之上而帮助设计者复用成功的设计。熟悉这样的模式的设计者可以立即应用它们到设计问题中,而不需要重新去发现它们。 因此,在OOD过程中,开发人员应积极去选择并应用现存的可复用的设计模式,而不是试图创建新的设计模式。 每个模式都有伴随定义的语境和强度。语境解释了模式适用的情况。强度是语境中的元素,有某种程度上的不同。如果问题的环境与模式的语境和强度相匹配,该模式就适合于你的应用。如果模式限制必须有灵活的环境,使用模式设计就要付出代价。
由于设计模式的复杂性和抽象性,软件设计人员一般从以下几方面考虑选择适合的设计模式: • 考虑设计模式解决设计问题的步骤,从中借鉴良好的设计经验。 • 考虑设计模式所要解决的问题,将之与自己的问题匹配,从而做出选择。 • 从更高一层着眼,分析所有的设计模式之间的关系,研究目的相似的模式(要求设计人员对设计模式非常的熟悉)。 • 考虑设计中哪些是可变性和可扩充性。 设计模式在软件设计中的应用主要取决于设计人员的主观意识和熟悉模式的程度。
2、设计模式的分类 根据“Gang of Four”的分类准则,按模式的使用目的(即“用来完成什么工作”)来划分,可分为以下几种类型: 创建型:创建对象 结构型:处理类和对象的组合 行为型:对类或对象如何交互、如何分配职责进行描述 (1)创建型模式 • Abstract Factory(抽象工厂):提供了创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。 • Builder(生成器):将一个复杂对象的构建与它的表示分开,使得同样的构建过程可以得出不同的表示。 • Factory Method(工厂方法):定义了一个创建对象的接口,让子类决定将哪个类实例化。该方法使一个类的实例化延迟到其子类。
• Prototype(原型):用原型实例指定创建对象的种类,并通过复制原型来创建新的对象。 • Singleton(单件):一个类仅有一个实例,提供对它全局访问。 (2)结构型模式 • Adapter(适配器):将一个类的接口转换成客户希望的另一个接口。该模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 • Bridge(桥接):将抽象部分与它的实现部分分离,使它们都可以独立的变化。 • Composite(组合):将对象组织成“整体-部分”的层次结构。该模式使得客户机对单个对象和复合对象的使用具有一致性(进行同样的交互)。 • Decorator(装饰):动态的给一个对象添加一些额外的功能。就扩展功能而言,该模式比生成子类方式更为灵活。
• Facade(外观):为子系统中的一组接口提供了一个统一的界面。该模式有助于为复杂子系统提供一个简单接口,使得子系统容易使用。 • Flyweight(享元):运用共享技术有效的支持大量细粒度的对象。 • Proxy(代理):使一个对象(如组件的客户机)与一个对象代表而不是对象本身通信。 (3)行为型模式 • Chain of Responsibility(职责链):为避免请求的发送者与其接受者耦合在一起,给予多个对象都有处理这个请求的机会。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 • Command(命令):将请求分装为对象(将请求与其执行分开),允许系统以不同请求、队列或日志请求作为参数来表示客户,并支持无法执行的操作。
• Interpreter(解释器):给定一种语言(如脚本语言),定义语法的表示方法和解释器,解释器使用该方法来解释语言。 • Iterator(迭代器):提供了连续访问一个聚集对象中各个元素的方法,而不需要暴露该对象的内部表示。 • Mediator(中介者):定义一个中介对象,来封装一组对象交互的方式。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且还允许对象的交互独立变化。 • Memento(备忘录):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,使其以后能够恢复。 • Observer(观察者):在对象间定义的一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。