软件体系结构复习

概论(5分左右)

软件体系结构的意义

体系结构提供一种方法:解决共同的问题,确保建筑、桥梁、乐曲、书籍、计算机、网络或系统在完成后具有某些属性或行为

  • 体系结构既是所构建系统的计划,确保得到期望的特性,同时也是所构建系统的描述。

软件体系结构的意义:

  • 体系结构,有助于确保系统能够满足其利益相关人的关注点,构想、计划、构建、维护系统时,体系结构有助于处理复杂性。
  • 开发一个具有一定规模和复杂性的软件系统和编写一个简单的程序是不一样的

软件体系结构的意义

软件体系结构 = 组件 + 连接件 + 约束
Software Architecture = Components + Connectors + Constrain

  • 组件:具有某种功能的可重用的软件模块单元,表示了系统中主要的计算单元数据存储
  • 连接件:表示了组件之间的交互
  • 约束:表示了组件和连接件的拓扑逻辑约束

国内定义:软件体系结构 = 构件 + 连接件 + 约束
软件体系结构包括构件、连接件和约束,它是可预制可重构的软件框架结构。

  • 构件是可预制和可重用的软件部件,是组成体系结构的基本计算单元或数据存储单元
    • 构件是指一个计算单元或者数据存储单元,可以是一个处理过程或数据元素
    • 构件是用于实现计算和状态的单元,可以工作在:客户端、服务器端、数据库或层等。
    • 构件可简单可复杂:复杂构件描述一个系统,一个体系结构由一些描述系统的复杂构件组成。
  • 连接件也是可预制和可重用的软件部件,是构件之间的连接单元
    • 连接件用于建模:构件之间的相互作用、控制这些相互作用的规则
  • 构件和连接件之间的关系用约束来描述
    • 约束描述了体系结构的配置和拓扑要求,配置或拓扑
    • 用于描述软件体系结构的构成,确定了体系结构的构件与连接件之间的连接关系:正确的连接性、并发和分布性、符合设计的启发和风格规则

软件体系结构的发展史

软件体系结构的研究活动-软件体系结构要解决的问题:

  • 软件体系结构的建模与表示(Architecture Modeling and Documenting)
  • 软件体系结构风格的研究(Software Architecture Styles)
  • 体系结构描述语言(Architecture Description Language,ADL)
  • 软件体系结构的评价方法(Architecture Evaluation)
  • 软件产品线及特定领域软件框架的研究(Product line and DSSA)
  • 动态软件体系结构

软件体系结构的优势

  • 容易理解:从高层设计的抽象层次来表征一个系统,简化了我们理解庞大系统的能力
  • 重用:重用大的构件、重用一些集成构件的框架、特定领域的软件体系结构、设计模式
  • 控制成本:系统维护者可以更好的理解变更带来的影响,因而可以更加精确的估算变更所需的成本
  • 可分析性:对系统的一致性检查提供高层次的视图

软件架构师关键关注点:功能性、可变性、性能、容量、生态系统、模块化、可构建性、产品化、安全性

软件体系结构风格

概述

  • 多年来,人们在开发某些类型软件过程中积累起来的组织规则和结构,形成了软件体系结构风格
  • 软件体系结构设计的一个核心问题是,能否使用重复的体系结构模式,即能否达到体系结构级的软件重用
  • 软件体系结构风格是描述某一特定应用领域中系统组织方式的惯用模式
    • 体系结构风格定义一个系统家族,即一个体系结构定义一个词汇表和一组约束
    • 体系结构风格,反映了领域中中众多系统所共有的结构和语义特征,并指导如何将各个模块和子系统有效地组织成一个完整的系统

体系结构风格的最关键的四要素

  1. 提供一个词汇表
  2. 定义一套配置规则
  3. 定义一套语义解释原则
  4. 定义对基于这种风格的系统所进行的分析

管道-过滤器风格的体系结构

管道-过滤器模式下,每个功能模块都有一组输入和输出,功能模块从输入集合读入数据流,并在输出集合产生输出数据流,即功能模块对输入数据流进行增量计算得到输出数据流。功能模块称作过滤器(Filter),功能模块间的连接可以看作输入、输出数据流之间的通路,所以称作管道(Pipe)。

  1. 管道-过滤器体系结构模式,把系统任务分成几个序贯的处理步骤。这些步骤,通过系统的数据流连接,一个步骤的输出是下一个步骤的输入
  2. 系统的输入,由诸如文本文件等数据源提供
  3. 实现相连处理步骤间的数据流动。通过管道联合的过滤器序列叫做处理流水线(pipeline)
  4. 过滤器是独立运行的部件,不受其它过滤器运行的影响

优点:

  1. 由于每个构件的行为不受其他构件的影响,因此,整个系统的行为比较易于理解
  2. 支持功能模块的复用
  3. 具有较强的可维护性、可扩展性
  4. 支持特殊的分析
  5. 支持并发执行

缺点:

  1. 往往会导致系统处理过程的成批操作
  2. 在处理两个独立但又相关的数据流时,可能会遇到困难
  3. 需要对数据传输进行特定的处理时,导致对于每个过滤器的解析输入和格式化输出要做更多的工作,带来系统复杂性的上升
  4. 并行处理获得的效率,往往是一种假象

数据抽象和面向对象风格

抽象数据类型概念,对软件系统有着重要作用,目前,软件界已普遍转向使用面向对象系统

  • 这种风格建立在数据抽象和面向对象的基础上,数据的表示方法和相应操作,封装在一个抽象数据类型或对象中
  • 这种风格的构件是对象,或者说,是抽象数据类型的实例

优点:

  1. 改变一个对象的表示,而不影响其他对象
  2. 设计者可将一些数据存取操作的问题分解成一些交互的代理程序的集合

缺点:

  1. 为了使对象间通过过程调用等进行交互,必须知道对象的标识
  2. 连锁反应:必须修改所有显示调用它的其他对象,并消除由此带来的副作用

基于事件的隐式调用风格(事件驱动)

  • 基于事件的隐式调用风格的思想,是构件不直接调用一个过程,而是触发或广播一个或多个事件
  • 从体系结构上说,这种风格的构件是一些模块,这些模块既可以是一些过程,又可以是一些事件的集合
  • 基于事件的隐式调用风格的主要特点,是事件的触发者并不知道哪些构件会被这些事件影响

优点:

  • 为软件重用提供了强大的支持
  • 为改进系统带来了方便

缺点:

  • 构件放弃了对系统计算的控制
  • 数据交换的问题
  • 既然过程的语义必须依赖于被触发事件的上下文约束,关于正确性的推理,就存在问题

总结:

  • 构件不直接调用一个过程,而是触发或广播一个或多个事件
  • 系统中的其他构件中的过程在一个或多个事件中注册,当一个事件被触发,系统自动调用在这个事件中注册的所有过程。
  • 这种风格的构件是一个模块,这些模块可以是一些过程,又可以是一些事件的集合。
  • 不变量:事件的触发者并不知道哪些构件会被这些事件影响(观察者模式-Observer)
  • 实例:数据库管理系统,用户界面

分层系统风格

分层式体系结构,是按层次组织软件的一种软件体系结构

  • 一个分层风格的系统按照层次结构组织,每一层向它的上层提供服务,同时又是它的下层客户
  • 连接件可以用层次间的交互协议来定义。每个独立层都要防止较高层直接访问较低层

优点:

  1. 系统易于改进和拓展
  2. 每一层的软件都易于重用,并可为某一层次提供多种可互换的具体实现
  3. 分层系统所支持的设计体现了不断增加的抽象层次
  4. 标准化支持
  5. 局部依赖性
  6. 可替换性

缺点:

  1. 应当如何界定层次间的划分是一个较为复杂的问题
  2. 更改行为的重叠
  3. 降低效率
  4. 不必要的工作
  5. 难以认可层的正确粒度

仓库风格和黑板风格(数据共享风格)

  • 仓库风格的体系结构由两个构件组成。一个中央数据结构,表示当前状态,一个独立构件的集合,对中央数据结构进行操作

  • 在问题求解过程中,黑板上保存了所有的部分解,代表了问题求解的不同阶段

  • “黑板”模式即让专业们坐在真实黑板并一起工作,来解决一个问题

  • 设计问题:黑板系统传统是应用在需要对数据做出复杂解释的信号处理中,这类系统包括语音和模式识别领域

  • 解决方案:黑板体系结构实现的基本出发点,是已经存在一个对公共数据结构进行协同操作的独立程序集合。因此,黑板结构存在一个中心控制部件。

标准的黑板型仓库模式系统:

  • 知识源
  • 中央数据单元
  • 控制单元

优点:

  • 便于多客户共享大量数据,不用关心数据是何时出现的、谁提供的、怎样提供的
  • 既便于添加新的作为知识源代理的应用程序,也便于扩展共享的黑板数据结构
  • 可重用的知识源
  • 支持容错性和健壮性

缺点:

  • 不同的知识源代理
  • 需要一定的同步锁机制
  • 测试困难
  • 不能有好的求解方案
  • 低效
  • 开发成本高

仓库系统:

  • 构件:中心数据结构(仓库)和一些独立构件的集合
  • 仓库和在系统中很重要的外部构件之间的相互作用
  • 实例:需要使用一些复杂表征的信号处理系统

模型-视图-控制器风格

MVC结构是为那些需要为同样的数据提供多个视图的应用程序而设计的,很好地实现了数据层与表示层的分离,通常用于分布式应用系统的设计、分析中,以及用于确定系统各部分间的组织关系。对于界面设计可变性的需求,MVC把交互系统的组成分解成模型、视图、控制器三种部件。

  1. MVC模式
    • MVC是许多交互和界面系统的构成基础,微软的MFC基础类,也遵循了MVC的思想
    • 作为一种软件设计典范,MVC用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑
  2. 模型、视图和控制类

MVC的实现:

  1. 分析应用问题,对系统进行分离
  2. 设计和实现每个视图
  3. 设计和实现每个控制器
  4. 使用可安装和卸载的控制器

解释器风格(虚拟机风格)

解释器风格通常被用于建立一种虚拟机,以弥合程序的语义与作为计算引擎的硬件的间隙。解释器风格适用于这样的应用程序,应用程序并不能直接运行在最合适的机器上,或者不能直接以最适合的语言执行

优点:

  • 有助于应用程序的可移植性与程序设计语言的跨平台能力。
  • 可以对未实现的硬件进行仿真。
    缺点:额外的间接层次带来的系统性能的下降

案例:上下文关键字

C/S风格——任务分配


网络通信软件的主要作用,是完成数据库服务器和客户端应用程序之间的数据传输。

优点:

  • 强大的数据操作和事务处理能力,模型思想简单,易于理解接受
  • 系统的客户应用程序和服务器构件分别运行在不同的计算机上,系统中每台服务器都可以适合各构件的要求,这对于硬件和软件的变化显示出极大的适应性和灵活性,而且易于对系统进行扩充和缩小
  • 在C/S体系结构中,系统中的功能构件充分隔离,客户应用程序的开发集中于数据的显示和分析,而数据库服务器的开发则集中于数据的管理,不必在每一个新的应用程序中都要对一个DBMS进行编码。将大的应用处理任务分布到许多通过网络连接的低成本计算机上,以节约大量费用

缺点:

  • 开发成本较高
  • 客户端程序设计复杂
  • 信息内容和形式单一
  • 用户界面风格不一
  • 软件移植困难
  • 软件维护与升级困难
  • 新技术不能轻易应用

三层客户/服务器结构风格

传统的二层C/S结构局限:

  1. 单一服务器且以局域网为中心,难以拓展至大型企业广域网或因特网
  2. 软、硬件的组合及集成能力有限
  3. 客户机的负荷太重,难以管理大量的客户机,系统的性能容易变差
  4. 数据安全性不好


与二层C/S结构相比,在三层C/S体系结构中,增加了一个应用服务器,可以将整个应用逻辑驻留在应用服务器上,而只有表示层存在于客户机上。这种结构,被称为“瘦客户机”。

  • 表示层:应用的用户接口部分,担负着用户与应用之间的对话功能
    • 在变更用户界面时,只需改写显示控制和数据检查程序,而不影响其他两层
  • 功能层:应用的本体,用于将具体的业务处理逻辑编入程序
    • 功能层相当于应用的本体,用于将具体的业务处理逻辑编入程序
    • 表示层与功能层之间的数据交往,要尽可能简洁
  • 数据层
    • 数据层就是数据库管理系统,负责管理对数据库数据的读写。数据库管理系统必须能迅速执行大量数据的更新和检索
    • 三层C/S的解决方案,是对这三层进行明确分割,并在逻辑上使其独立

优点:

  • 允许合理地划分三层结构的功能,使之在逻辑上保持相对独立性,从而使整个系统的逻辑结构更为清晰,能提高系统与软件的可维护性和可扩展性
  • 允许更灵活有效地选用相应的平台与硬件系统,使之在处理负荷能力上与处理特性上分别适应于结构清晰的三层,并且这些平台与各个组成部分可以具有良好的可升级性与开放性
  • 三层C/S结构中,应用的各层可以并行开发,各层也可以选择各自最适合的开发语言,使之能并行地而且是高效率地进行开发,达到较高的性能价格比,对每一层的处理逻辑的开发和维护也会更容易些
  • 允许充分利用功能层有效地隔离开表示层与数据层,未授权的用户难以绕过功能层而利用数据库工具或黑客手段去非法地访问数据层,这就为严格的安全管理奠定了坚实的基础,整个系统的管理层次也更加合理和可控制

注意点:

  • 各层间的通信效率不高
  • 设计时必须慎重考虑三层间的通信方法、通信频率及数据量

浏览器/服务器风格

浏览器/Web服务器/数据库服务器

优点:

  • 系统安装、修改和维护全在服务器端解决,用户在使用时仅需要浏览器便可运行全部的模块,可以在运行时自动升级
  • 提供了异种机、异种网、异种应用服务器的联机、联网、统一服务的最现实的开放性基础

缺点:

  • 缺乏对动态页面的支持能力,没有集成有效的数据库处理功能
  • 系统扩展能力差,安全性难以控制
  • 在数据查询等响应速度上,要远远低于C/S体系结构
  • 的数据提交一般以页面为单位,数据的动态交互性不强,不利于在线事务处理(OLTP)应用

C/S与B/S混合风格结构

B/S与C/S混合软件体系结构是一种典型的异构体系结构。

UML

UML图之间的关系:需求模型与设计模型

UML建模原则:

  • 适当增加注释
  • 画图应简化系统理解而不是增加工作量
  • 注意折衷
  • 保持图的一致性
  • 不要画多余的图

软件体系结构建模概述

  • 统一建模语言(Unified Modeling Language,UML)是一种为面向对象系统的产品进行说明、可视化、编制文档的一种标准语言,是非专利的第三代建模和规约语言。
  • UML具有广泛的建模能力,在消化、吸收、提炼现有的软件建模语言的基础上提出,集百家之长,是软件建模语言的集大成者

软件体系结构模型

UML概述

UML:

  • 标准的工业化设计语言
  • 把复杂的问题分解成易于理解的小问题
  • 建模是开发优秀软件的所有活动中核心部分之一,实现对系统的结构的可视化控制

  • 用例图
  • 类图
  • 包图
  • 顺序图
  • 状态图
  • 活动图
  • 组件图
  • 部署图

UML的结构:

  • 模型元素(Model element)
    • 模型元素包括事物以及事物与事物之间的联系
    • 每一个模型元素都有一个与之相对应的图形元素
    • 无论在哪个图中,同一个模型元素都保持相同的意义和符号
  • 通用机制(General mechanism)
    • 额外的注释、修饰和语义
    • 包括规格说明、修饰、公共分类和扩展机制四种

UML的特点和用途

  • 统一标准
  • 面向对象的特性
  • 提出了新的概念
  • 独立于过程
  • 对系统的逻辑模型和实现模型,都能清晰的表示,可以用于复杂软件系统的建模

UML中的结构建模

  • 结构图显示建模系统的静态结构。关注系统的元件,无需考虑时间,在系统内,静态结构通过显示类型和实例进行传播
  • 除了显示系统类内部结构型和它们的实例,结构图至少也显示了这些元素间的一些关系

类的定义:

  • 类(Class)封装了数据和行为,是面向对象的重要组成部分,它是具有相同属性、操作、关系的对象集合的总称
  • 在系统中,每个类具有一定的职责,职责指的是类所担任的任务,即类要完成什么样的功能,要承担什么样的义务。一个类可以有多种职责,设计得好的类一般只有一种职责(单一职责原则),在定义类的时候,将类的职责分解成为类的属性和操作(即方法)。
  • 类的属性即类的数据职责,类的操作即类的行为职责

类图定义:类图使用需要出现在系统内的不同的类来描述系统的静态结构,类图包含类和它们之间的关系,它描述系统内所声明的类,但它没有描述系统运行时类的行为

类图表示:

定义属性:

定义操作:

关联关系
  • 关联关系(Association)是类与类之间最常用的一种关系,它是一种结构化关系,用于表示一类对象与另一类对象之间有联系
  • 在UML类图中,用实线连接有关联的对象所对应的类,在使用Java、C#和C++等编程语言实现关联关系时,通常将一个类的对象作为另一个类的属性
  • 在使用类图表示关联关系时可以在关联线上标注角色名

关联关系:

双向关联:默认情况下,关联是双向的

单向关联:类的关联关系也可以是单向的,单向关联用带箭头的实线表示

自关联:一些类的属性对象类型为该类本身

多重性关联:多重性关联关系又称为**重数性关联关系(Multiplicity)**,表示一个类的对象与另一个类的对象连接的个数。在UML中多重性关系可以直接在关联直线上增加一个数字表示与之对应的另一个类的对象的个数。

一个表单包含一个按钮,一个按钮可以被零到多个表单包含

聚合关系
  • 聚合关系(Aggregation)表示一个整体与部分的关系。通常在定义一个整体类后,再去分析这个整体类的组成结构,从而找出一些成员类,该整体类和成员类之间就形成了聚合关系。
  • 在聚合关系中,成员类是整体类的一部分,即成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。在UML中,聚合关系用带空心菱形的直线表示

组合关系
  • 组合关系(Composition)也表示类之间整体和部分的关系,但是组合关系中部分和整体具有统一的生存期。一旦整体对象不存在,部分对象也将不存在,部分对象与整体对象之间具有同生共死的关系。
  • 在组合关系中,成员类是整体类的一部分,而且整体类可以控制成员类的生命周期,即成员类的存在依赖于整体类。在UML中,组合关系用带实心菱形的直线表示

依赖关系
  • 依赖关系(Dependency)是一种使用关系,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系。大多数情况下,依赖关系体现在某个类的方法使用另一个类的对象作为参数
    • 方法参数、局部变量、调用静态方法
  • 在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。

泛化关系
  • 泛化关系(Generalization)也就是继承关系,也称为“is-a-kind-of”关系,泛化关系用于描述父类与子类之间的关系,父类又称作基类或超类,子类又称作派生类。在UML中,泛化关系用带空心三角形的直线来表示。
  • 在代码实现时,使用面向对象的继承机制来实现泛化关系,如在Java语言中使用extends关键字、在C++/C#中使用冒号“:”来实现。

接口与实现关系

接口之间也可以有与类之间关系类似的继承关系和依赖关系,但是接口和类之间还存在一种实现关系(Realization),在这种关系中,类实现了接口,类中的操作实现了接口中所声明的操作。在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示。

实例分析

某C/S系统需要提供注册功能,注册用例基本描述如下:

  • 用户输入新帐号,系统检测该帐号是否已存在,如果不存在则可注册成功,否则提示“帐号已存在”,用户再次输入帐号;
  • 用户输入其他个人信息;
  • 系统保存用户个人信息;
  • 用户个人信息包括帐号、密码、姓名、性别、年龄、电话、电子邮箱。

类图

  • 类图是UML中最基本也是最重要的一种视图,用来刻画软件中类等元素的静态结构和关系
  • 在大多数UML模型中,这些类型包括类,接口、数据类型、组件
  • 类(Class)是来描述具有相同特征、约束、语义的一类对象,这些对象具有共同的属性和操作
    • 类图中的一个类。可以简单地只给出类名,也可以具体列出该类拥有的成员变量和方法,甚至更详细地描述可见性、方法参数、变量类型等信息。
  • 类的UML表示,是一个长方形,垂直地分为三个区。当图描述仅仅用于显示分类器间关系的高层细节时,下面的两个区域是不必要的。
    • 第一层显示类的名称,如果是抽象类就要用斜体显示。
    • 第二层是类的特性,通常就是字段和属性。
    • 第三层是类的操作,通常是方法和行为。
抽象类

抽象类(Abstract class)是指一个类只提供操作明,而不对其进行实现。

  • 对这些操作的实现,可以由其子类进行,并且不同的子类可以对同一操作具有不同的实现。
  • 抽象类和类的符号区别,在于抽象类的名称用斜体字符表示

接口
  • “飞翔”矩形框表示一个接口图,与类图的区别,主要是顶端有接口(Interface)的显示,第一行是接口名称,第二行是接口方法。
  • 接口还有另一种表示方法,俗称棒棒糖表示法,就是唐老鸭类实现了“讲人话”的接口。

关联关系
  • 关联关系(Association)描述了类的结构之间的关系
    • 具有方向、名字、角色和多重性等信息。当关联是双向的,那么就可以用无向连线表示。
  • 一般的关联关系语义较弱,也有两种语义较强,分别是聚合与组合

依赖关系

两个类之间存在依赖关系(Dependency),表明一个类使用或需要知道另一个类中包含的信息

    • 有多种表现形式,例如,绑定(bind)、友元(friend)等。
    • 模板类Stack定义了栈相关的操作;IntStack将参数T与实际类型int绑定,使得所有操作都针对int类型的数据。
  • 依赖关系使用虚线箭头,表示“动物”、“氧气”与“水”之间。
    • 动物有几大特征,比如有新陈代谢,能繁殖。而动物要有生命,需要氧气,水
      以及食物等。
    • 也就是说,动物依赖于氧气和水。之间是依赖关系(dependency),用虚线箭头来表示。

聚合关系
  • 聚合关系(Aggrengation)表明,两个类的实例之间存在一种拥有或属于关系,可以看作是一种较弱的整体-部分关系。
    • 在一个聚合关系中,子类实例可以比父类存在更长的时间。
    • 为了表现一个聚合关系,画一条从父类到部分类的实线,并在父类的关联末端画一个未填充棱形。
  • 例如,“大雁”和“雁群”这两个类。大雁是群居动物,每只大雁都属于一个雁群,一个雁群可以有多只大雁。所以,它们之间就满足聚合关系。
    • 聚合表示一种弱的“拥有”关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。
    • 聚合关系用空心的菱形加上实线箭头表示

合成关系
  • 合成(Composition)是一种强的“拥有”关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。
    • 合成关系用实心的菱形加上实线箭头来表示。另外,合成关系的连线两端,还有一个数字“1”和数字“2”,这被称为基数。
  • 表明这一端的类可以有几个实例。“鸟”和“翅膀”这两个类中,鸟和翅膀类似整体和部分的关系,并且翅膀和鸟的生命周期是相同的,在这里“鸟”类和其“翅膀”类就是合成关系。
    • 一个鸟应该有两支翅膀。如果一个类可能有无数个实例,则就用“n”来表示。关联关系,聚合关系也可以有基数的。

泛化关系

泛化关系(Generalization)在面向对象中一般称为继承关系,存在于父类与子类、父接口与子接口之间

包图

  • 包是一种把元素组织到一起的通用机制,包可以嵌套于其他包中。
  • 包图用于描述包与包之间的关系,包的图标是一个带标签的文件夹。

两种组包方式:

  • 根据系统分层架构组包(推荐使用);
  • 根据系统业务功能模块组包
引入关系
  • 一个包中的类可以被另一个指定包(以及嵌套于其中的那些包)中的类引用
  • 引入关系是依赖关系的一种,需要在依赖线上增加一个**<>**衍型,包之间一般依赖关系都属于引入关系。

泛化关系

表示一个包继承了另一个包的内容,同时又补充自己增加的内容。

嵌套关系

一个包中可以包含若干个子包,构成了包的嵌套层次结构。

组件图

  • 组件图又称为构件图(Component Diagram) 。组件图中通常包括组件、接口,以及各种关系。组件图显示组件以及它们之间的依赖关系,它可以用来显示程序代码如何分解成模块或组件。一般来说,组件就是一个实际文件,可以有以下几种类型:
    • 源代码组件:一个源代码文件或者与一个包对应的若干个源代码文件。
    • 二进制组件:一个目标码文件,一个静态的或者动态的库文件。
    • 可执行组件:在一台处理器上可运行的一个可执行的程序单位,即所谓可执行程序。
  • 组件图可以用来显示编译、链接或执行时组件之间的依赖关系,以及组件的接口和调用关系
  • 组件间的关系有两种:泛化关系和依赖关系,如果两个不同组件中的类存在泛化关系或依赖关系,那么两个组件之间的关系就表示为泛化关系或依赖关系。

组件图组成元素:

  • 组件:系统中可以替换的部分,一般对应一个实际文件,如exe、jar、dll等文件,它遵循并提供了一组接口的实现。
  • 接口:一组操作的集合,它指明了由类或组件所请求或者所提供的服务。
  • 部件:组件的局部实现
  • 端口:被封装的组件与外界的交互点,遵循指定接口的组件通过它来收发消息。
  • 连接件:在特定语境下组件中两个部件之间或者两个端口之间的通信关系

供(Provided)接口与需(Required)接口:

绘制技巧:

  • 当需要把系统分成若干组件(构件),希望借助接口或组件将系统分解为低层结构并表示其相互关系时需要使用组件图。
  • 在绘制组件图时,应该注意侧重于描述系统的静态实现视图的一个方面,图形不要过于简化,应该为组件图取一个直观的名称,在绘制时避免产生线的交叉。
  • 注意组件的粒度,粒度过细的构件将导致系统过于庞大,会给版本管理带来问题。
组件图分析实例

在某销售终端系统中,客户端收银机可以通过销售消息接口与销售服务器相连。考虑到网络可能不可靠,需要提供一个消息队列组件。在网络环境畅通时收银机直接与服务器相连;如果网络不可靠则与消息队列交互,当网络可用时队列再与服务器交互。服务器分解为两个主要组件,主要包括事务处理组件和记账驱动组件,记账驱动组件需要和记账系统交互。绘制系统组件图。

组件图小结
  • 构件图用于静态建模,是表示构件类型的组织以及各种构件之间依赖关系的图。
  • 由于基于构件的软件开发日益普及和应用,UML对构件图进行了较大的改进。
  • 构件(Component)是系统中遵从一组接口且提供其实现的物理的、可替换的部分
  • 随着面向对象技术的引用,软件系统被分成若干个子系统、构件。

部署图

  • 部署图(Deployment Diagram),也称为实施图,它和组件图一样,是面向对象系统的物理方面建模的两种图之一。组件图是说明组件之间的逻辑关系的,而部署图则是在此基础上更进一步,描述系统硬件的物理拓扑结构及在此结构上执行的软件。部署图可以显示计算节点的拓扑结构和通信路径、节点上运行的软件组件。
  • 在UML中,部署图显示了系统的硬件和安装在硬件上的软件,以及用于连接异构计算机之间的中间件。部署图通常被认为是一个网络图或者物理架构图

部署图实例:

部署图元素:

  • 节点和连接:节点(Node)代表一个物理设备。在 UML 中,使用一个立方体表示一个节点。节点之间的连线表示系统之间进行交互的通信路径,在 UML 中被称为连接
  • 组件:在部署图中,组件代表可执行的物理代码模块,如一个可执行程序,逻辑上它可以与类或包对应。

绘制技巧:

  • 部署图用于表示何者部署于何处,任何复杂的部署都可以使用部署图描述。
  • 一个部署图只是系统静态部署视图的一个图形表示,在单个部署图中不必捕获系统部署视图的所有内容
  • 部署图一般用于:
    • 嵌入式系统建模(硬件之间的交互)
    • 客户端/服务器系统建模(用户界面与数据的分离)
    • 分布式系统建模(多级服务器)
部署图实例分析

某大型商场的信息管理系统是由一个数据库服务器、中央服务器、每个楼层的楼层服务器、各柜台的收款机和各个部门的计算机终端组成的局域网络,它们分别负责商场数据存储、数据的汇总与分析、当日数据的保存与整理、销售信息录入和进销存信息处理等各种业务处理。用部署图描述该系统在不同硬件上的配置情况。

部署图小结
  • 部署图(Component Diagram)描述的是系统运行时的结构,展示了硬件的配置其软件如何部署到网络结构中
    • 一个系统模型,只有一个部署图,部署图通常用来帮助理解分布式系统。
  • 部署图用于静态建模,是表示运行时过程节点结构、描述软件与硬件是如何映射的、构件实例及其对象结构的图。

UML中的行为建模

  • 行为建模被称为动态建模,主要用来刻画系统中的动态行为、过程、步骤
  • UML行为建模中提供的视图,可以从不同的侧面来描述软件系统的动态过程
    • 例如,业务或算法过程与步骤、多个对象为完成一个场景而进行的交互、消息传递的过程、一个对象在生存周期中根据收到的不同事件进行响应的过程等。

用例建模技术

  • 用例图是被称为参与者的外部用户所能观察到的系统功能的模型图

  • 用例图列出系统中的用例和系统外的参与者,并示哪个参与者参与了哪个用例的执行(或称为发起了个用例)。

    • 用例图多用于静态建模阶段(主要是业务建模和需求建模)
  • 用例建模(Use Case Modeling)是使用用例的方法来描述系统的功能需求的过程,用例建模促进并鼓励了用户参与,这是确保项目成功的关键因素之一。

  • 用例建模主要包括以下两部分内容:

    • 用例图(Use Case Diagram)
    • 用例描述文档 (Use Case Specification)

用例建模步骤:

  1. 识别执行者
    • 执行者——Actor
    • 定义:在系统之外,透过系统边界与系统进行有意义交互任何事物
    • 引入执行者的目的:帮助确定系统边界
  2. 识别用例
    • 用例是在系统中执行一系列动作,这些动作将生成特定执行者可见的价值结果。一个用例定义一组用例实例
  3. 绘制用例图
  4. 书写用例文档
  5. 检查用例模型
用例图

用例要点:

  • 有意义的目标
  • 价值结果由系统生成
  • 业务语言,用户观点
  • 注意用例的命名
  • 用例的“粒度

执行者与用例之间的关联关系/通信关系:在用例图中,执行者和用例之间进行交互,相互之间的关系用一根直线来表示

执行者之间的泛化关系:

用例之间的关系:

  • 包含关系:描述在多个用例中都有的公共行为,由用例A指向用例B,表示用例A中使用了用例B中的行为或功能,包含关系是通过在依赖关系上应用**<>**构造型(衍型)来表示的
  • 拓展关系:在扩展(extend)关系中,基础用例(Base)中定义有一至多个已命名的扩展点,扩展关系是指将扩展用例(Extension)的事件流在一定的条件下按照相应的扩展点插入到基础用例(Base)中。
    • 扩展用例可以在基用例之上添加新的行为,但是基用例必须声明某些特定的“扩展点”,并且扩展用例只能在这些扩展点上扩展新的行为。
    • 扩展关系是通过在依赖关系上应用**<>**构造型(衍型)来表示的。
  • 泛化关系:
    • 当多个用例共同拥有一种类似的结构和行为的时候,可以将它们的共性抽象成为父用例,其他的用例作为泛化关系中的子用例。
    • 在用例的泛化关系中,子用例是父用例的一种特殊形式,子用例继承了父用例所有的结构、行为和关系
    • 泛化关系一般很少使用。
实例:酒店

某酒店订房系统描述如下:

  1. 顾客可以选择在线预订,也可以直接去酒店通过前台服务员预订;
  2. 前台服务员可以利用系统直接在前台预订房间
  3. 不管采用哪种预订方式,都需要在预订时支付相应订金
  4. 前台预订可以通过现金或信用卡的形式进行订金支付,但是网上预订只能通过信用卡进行支付;
  5. 利用信用卡进行支付时需要和信用卡系统进行通信;
  6. 客房部经理可以随时查看客房预订情况每日收款情况
    构造该系统的用例模型。

解决方案:加粗为执行者,斜体为用例

用例文档
  • 用例是文本文档,而非图形
  • 用例建模主要是编写文本的活动,而非制图

用例的内容:

  • 用例编号
  • 用例名
  • 执行者
  • 前置条件
  • 后置条件
  • 涉众利益
  • 基本路径
    1. ××××
    2. ××××
    3. ××××
  • 扩展路径
  • 2a.××××:
    • 2a1….×××××
  • 字段列表
  • 业务规则
  • 非功能需求
  • 设计约束

前置、后置条件:

  • 开始用例前所必需的系统及其环境的状态
  • 用例成功结束后系统应该具备的状态
  • 注意:系统必须能检测到

基本路径:客户最想看到、最关心的路径。把基本路径单独分离,凸显用例的核心价值。

  • 只书写“可观测”的(说人话)
  • 使用主动语句
  • 句子必须以执行者或系统作为主语
  • 每一句都要朝目标迈进
  • 分支和循环
  • 不要涉及界面细

拓展路径:注意意外和分支

  • 替换路径
  • 异常路径

顺序图

  • UML顺序图一般用于确认和丰富一个使用情境的逻辑
  • 一个使用情境的逻辑或是一个用例的一部分;或是一条扩展路径;或是一个贯穿单个用例的完整路径,例如动作基本过程的逻辑描述;或是动作的基本过程的一部分再加上一个或多个的备用情境的逻辑描述;或是包含在几个用例中的路径
  • 顺序图将交互关系表现为一个二维图,纵向是时间轴,时间沿竖线向下延伸。横向轴代表了在协作中各独立对象的类元角色,类元角色的活动用生命线表示。

示例:

组成元素:

  • 生命线用一条纵向虚线表示
  • 在UML中,对象表示为一个矩形,其中对象名称标有下划线
  • 激活是过程的执行,包括等待过程执行的时间。在顺序图中激活部分替换生命线,使用长条的矩形表示
  • 消息是对象之间的通信,是两个对象之间的单路通信,是从发送者到接收者之间的控制信息流。消息在顺序图中由有标记的箭头表示,箭头从一个对象的生命线指向另一个对象的生命线,消息按时间顺序在图中从上到下排列。
  • 在顺序图中,对象安排在X轴。启动交互的对象放在最左边,随后放入消息的对象放在启动交互对象的右边。交互中对象发送和接收的消息沿着Y轴以时间增加的次序放置。在顺序图中,有的消息对应于激活,表示它将会激活一个对象,这种消息称为**调用消息(Call Message);如果消息没有对应激活框,表示它不是一个调用消息,不会引发其他对象的活动,这种消息称为发送消息(SendMessage)**。

消息:




  • 对象间的通信,用对象生命线之间的水平消息线来表示,消息箭头的形状,表明消息的类型(同步、异步或简单)。
    • 当收到消息时,接收对象立即开始执行活动,即对象被激活了。
    • 激活用对象生命线上的细长矩形框表示。消息通常用消息名和参数表来标识。
  • 消息还可以带有条件表达式,用以表示分支或决定是否发送消息。
    • 如果用条件表达式表示分支,则会有若干个互斥的箭头,就是说,在某一时刻仅可发送分支中的一个消息。
    • 一个顺序图显示了,一系列的对象和这些对象之间发送和接收的消息。

交互片段:一个复杂的顺序图可以划分为几个小块,每一个小块称为一个交互片段。每个交互片段由一个大方框包围,其名称显示在方框左上角的间隔区内,表示该顺序图的信息。常用操作符如下:

  • alt:多条路径,条件为真时执行。
  • opt:任选,仅当条件为真时执行。
  • par:并行,每一片段都并发执行。
  • loop:循环,片段可多次执行。
  • critical:临界区,只能有一个线程对它立即执行。

顺序图作用:

  • 对于业务人员,顺序图可显示不同的业务对象如何交互,对于交流当前业务如何进行很有用。除记录组织的当前事件外,一个业务级的顺序图能被当作一个需求文件使用,为实现一个未来系统传递需求。
  • 对于需求分析人员,顺序图能通过提供一个深层次的表达,把用例带入下一层次。通常用例被细化为一个或者更多的顺序图。顺序图的主要用途之一,是把用例表达的需求,转化为进一步、更深层次的精细表达。
  • 对于技术人员,顺序图在记录一个未来系统的行为应该如何表现时非常有用。在设计阶段,架构师和开发者能使用顺序图挖掘出系统对象间的交互,进一步完善整个系统的设计。

顺序图绘制技巧:

  • 以用例为单位创建顺序图,针对每个用例,考察为完成它所描述的功能需要哪些对象的操作参与执行,并且进一步考察这些操作的执行需要通过消息而引起其他哪些对象操作的执行。把这些对象以及参与交互的执行者组织到一个顺序图中。
  • 理论上需要为每一个用例创建一个顺序图,但是如果一个用例的交互对象很简单可以不需要创建顺序图
  • 如果需要考察单个用例内部多个对象的行为可以使用顺序图
  • 如果需要考察单个对象的行为就需要使用状态图
  • 如果需要考察跨用例或者跨线程的行为就需要考虑使用活动图
顺序图分析实例

绘制图书管理系统“借书”用例的顺序图(业务模型)。图书管理员打开借书界面,输入借书信息并提交借书请求;系统验证借书卡状态,如果借书卡未借书则记录借书信息且修改图书状态和借书卡状态,并提示借书成功;否则提示借书失败。

通信图

  • 通信图是由协作图发展而来的。与顺序图不同,通信图主要关注,参与交互的对象通过连接组成的结构。
  • 通信图的对象没有生命线,其消息以及方向都附属于对象间的连接,并通过编号表示消息的顺序。

交互概览图

  • 交互概览图通过类似于活动图方式,描述交互之间的流程,给出交互控制流的概览。
  • 在交互概览图中,节点不像活动图中那样是动作,而是一个交互图或是对交互图的引用。交互概览图,有两种形式:
    • 一种是以活动图为主线,对活动图中某些重要活动节点进行细化,即用一些小的顺序图对重要活动节点进行细化,描述活动节点内的对象之间的交互。
    • 另一种是以顺序图为主线,用活动图细化顺序图中某些重要对象,即用活动图描述重要对象的活动细节。

时序图

  • 时序图(Sequence Diagram)是显示对象之间交互的图,这些对象是按时间顺序排列的。
    • 顺序图中显示的,是参与交互的对象及其对象之间消息交互的顺序。时序图中包括的建模元素,有对象(Actor)、生命线(Lifeline)、控制焦点(Focus of control)、消息(Message)等等。
  • 时序图最常应用到实时或嵌入式系统的开发中,但并不局限于此。
    • 被建模的系统类型,对交互的准确时间进行建模,是非常必要的。
  • 时序图中,每个消息都有与其有关联的信息,准确描述了何时发送消息,消息的接收对象会花多长时间收到该消息,以及消息的接收对象需要多少时间处于某个特定状态。

状态图

状态图(State Diagram)用来描述一个特定对象的所有可能状态及其引起状态转移的事件。通常用状态图来描述单个对象的行为,它确定了由事件序列引出的状态序列,但并不是所有的类都需要使用状态图来描述它的行为,只有那些具有重要交互行为的类,才会使用状态图来描述。一个状态图包括一系列对象的状态及状态之间的转换

状态图组成元素:

  • 初始状态

    • 状态图用初始状态(Initial State)表示对象创建时的状态,每一个状态图一般只有一个初始状态,用实心的圆点表示。
  • 终止状态

    • 每一个状态图可能有多个终止状态(Final State),用一个实心圆外加一个圆圈表示。
  • 状态

    • 状态图中可有多个状态框,每个状态框中有两格:上格放置状态名称,下格说明处于该状态时,系统或对象要进行的活动(Action)。
  • 转移

    • 从一个状态到另一个状态之间的连线称为转移(Transition)。状态之间的转移可带有标注,由三部分组成(每一部分都可省略),其语法为:事件名 [条件] / 动作名
  • 守护条件

    • 事件有可能在特定的条件下发生,在UML中这样的条件称为守护条件(Guard Condition)。
  • 事件

    • 状态之间的过渡事件(Event)对应对象的动作或活动(Action)。
  • 动作

    • 发生的事件可通过对象的动作(Action)进行处理。
  • 简单状态:不包含其他状态的状态称为简单状态。

  • 复合状态:又称为组合状态,可以将若干状态组织在一起可以得到一个复合状态,包含在一个复合状态中的状态称为子状态。

  • 状态图适合用于表述在不同用例之间的对象行为
  • 系统设计中可能有多个对象,但并不需要给出每个对象的状态图,实际的做法是把注意力集中在整体系统或少数关键的对象上,特别是那些状态比较多的对象
状态图实例分析

某信用卡系统账户具有使用状态和冻结状态,其中使用状态又包括正常状态和透支状态两种子状态。如果账户余额小于零则进入透支状态,透支状态时既可以存款又可以取款,但是透支金额不能超过5000元;如果余额大于零则进入正常状态,正常状态时既可以存款又可以取款;如果连续透支100天,则进入冻结状态,冻结状态下既不能存款又不能取款,必须要求银行工作人员解冻。用户可以在使用状态或冻结状态下请求注销账户。根据上述要求,绘制账户类的状态图。

状态图总结

状态图使用有穷状态变迁图的方式,刻画系统或元素的离散行为,可以用来描述一个类的实例、子系统甚至整个系统的在其生存周期内,所处状态如何随着外部激励而发生变化。

活动图

  • 活动图(Activity Diagram)用来表示系统中各种活动的次序,它的应用非常广泛,既可用来描述用例的工作流程,也可以用来描述类中某个方法的操作行为
  • 活动图依据对象状态的变化来捕获动作(将要执行的工作或活动)与动作的结果。活动图中一个活动结束后将立即进入下一个活动(在状态图中状态的变迁可能需要事件的触发)。
  • 利用文本描述用例的事件流是很有用的,但如果事件流的逻辑复杂且有许多其他事件流,则文本形式可能较难阅读和理解,这时可使用活动图来描述事件流
  • 活动图是UML中的流程图,它是事件流的另一种建模方式。活动图用于以图形化的方式描述一个业务过程或者一个用例的活动的顺序流,它也可以用于建模一个操作要执行的动作,以及那些动作的结果。
  • 活动图是一种描述工作流的方式,它用来描述采取何种动作、做什么(对象状态改变)、何时发生(动作序列)以及在何处发生(泳道)。

作用:

  • 描述业务流程
  • 描述用例路径
  • 描述方法执行流程(程序流程图)

实例:

组成元素:

  • 起始活动显式地表示活动图工作流程的开始,用实心圆饼来表示,在一个活动图中,只有一个起始活动。
  • 终止活动表示一个活动图的最后和终结活动,一个活动图中可以有0个或多个终止活动,终止活动用实心圆点外加一个小圆圈来表示。
  • 活动图中的活动用一个圆角矩形表示,其内部的文本串用来说明采取的动作。活动是指一组动作,它是实现操作的一个步骤,活动之间的转移用箭头来表示,称为转移或流,转移是由事件的发生所引起的活动的改变,用带有箭头的实线表示。箭头上可能还带有守护条件,发送短句和动作表达式。
  • 守护条件用来约束转移,守护条件为真时转移才可以开始
  • 用菱形符号来表示判定,判定符号可以有一个或多个进入转移,两个或更多的带有守护条件的发出转移。
  • 对象流是活动与对象之间的依赖关系,可以将与活动涉及的对象放在活动图中,用一个依赖将其连接到相应的活动中,对象的这种使用方法构成了对象流。在活动图中,对象流使用带箭头的虚线表示,对象用矩形表示,矩形内是该对象的名称,名称下的方括号表示该对象此时的状态。
  • 可以将一个转移分解成两个或更多的转移,从而导致并发的动作。所有的并行转移在合并之间必须被执行。一条粗黑线表示将转移分解成多个分支(fork),同样用粗黑线来表示分支的合并(join),这种粗黑线称为同步条
  • 泳道用于划分活动图,有助于更好地理解执行活动的场所。泳道划分负责活动的对象,明确地表示哪些活动是由哪些对象进行的,每个活动只能明确地属于一个泳道。
  • 在活动图中,泳道一般用垂直实线绘出,垂直线分隔的区域就是泳道。

绘制技巧:

  • 使用活动图来描述用例路径更加直观
  • 对面向对象建模而言,用活动图描述业务流程并不是对每个系统都必不可少的工作
  • 不要把描述业务流程的活动图看成可编程的模型,它与系统的实际构造情况和执行情况有很大的差距。
  • 首先要对主要的业务流建模,然后再标出分支、合并和对象流。
  • 尽量减少交叉线,如果图形较为复杂,适当使用颜色和注释
活动图实例分析

活动图总结
  • 活动图是UML用于对系统的动态行为建模的另一种常用工具,描述活动的顺序,展现从一个活动到另一个活动的控制流。
    • 本质上,活动图是一种流程图。活动图着重表现从一个活动到另一个活动的控制流,是内部处理驱动的流程。
  • 活动图主要用于,业务建模时,用于详述业务用例,描述一项业务的执行过程,设计时描述操作的流程。
    • UML的活动图中包含的图形元素有动作状态、活动状态、动作流、分支与合并、分叉与汇合、泳道和对象流等。
  • 活动图与流程图的区别为,活动图描述系统使用的活动、判定点和分支,看起来和流程图没什么两样,并且传统的流程图所能表示的内容。
  • 大多数情况下也可以使用活动图表示,但是两者是有区别的,不能将两个概念混淆。

软件质量

引言

  • 作为在软件生命周期早期保障软件质量的重要手段之一,软件体系结构评估技术,是软件体系结构研究中的一个重要组成部分。
    • 用户最关注软件系统的质量,尤其是大规模的复杂软件系统。
    • 软件体系结构,对于确保最终系统的质量,有重要意义。
  • 对软件的体系结构进行评估,是为了在系统被构建之前预测质量,通过分析体系结构对于系统质量的影响,进而提出改进意见。
    • 因此,软件体系结构评估的目的,是分析软件体系结构潜在的风险,并检验设计中提出的质量需求。
    • 针对体系结构评估,许多研究组织在会议和杂志上提出了众多结构化的评估方法,对于评估的方法的改进和实践工作仍在进行中。

质量定义

  • 软件质量在IEEE 1061中定义:体现了软件拥有所期望的属性组合的程度

  • ISO/IEC Draft9126-1标准定义了软件质量模型。

  • 依照这个模型,共有六种特征:功能性、可靠性、可用性、有效性、可维护性和可移植性,并且被分成子特征,根据各个软件系统外部的可见特征来定义这些属性。

  • 高质量的软件:具有质量的软件是那些与它们的最初目的相一致的软件

  • 符合商业目标用户需求

  • 正确的功能优良的属性

质量目标与商业目标

  • 商业目标

  • 企业的根本目标是为了获取尽可能多的利润,而不是生产完美无缺的产品。如果企业销售出去的软件的质量比较差,轻则挨骂,重则被退货甚至被索赔,因此为了提高用户对产品的满意度,企业必须提高产品的质量。

  • 商业目标决定质量目标

  • 但是企业不可能为了追求完美的质量而不惜一切代价,当企业为提高质量所付出的代价超过销售收益时,这个产品已经没有商业价值了,还不如不开发。

  • 企业必须权衡质量、效率和成本,产品质量太低了或者太高了,都不利于企业获取利润。

  • 企业理想的质量目标不是“零缺陷”是恰好让广大用户满意,并且将提高质量所付出的代价控制在预算之内。

  • 质量属性

    • 质量属性需求来源于商业和产品目标
    • 关键的质量属性必须刻画系统的细节特征
    • 质量属性场景是用于描述质量属性和表达项目干系人观点的强有力的工具。
  • 外部质量对于用户而言是可见的

    • 正确性、健壮性、可靠性、性能、安全性、易用性、兼容性等。
  • 内部质量只有开发人员关心,可以帮助开发人员实现外部质量

    • 易理解性、可测试性、可维护性、可扩展性、可移植性、可复用性等

外部质量

正确性

正确性:正确性是指软件按照需求正确执行任务的能力

  • 功能性是系统完成所期望工作的能力。一项任务的完成,需要系统中许多或大多数构件的相互协作。
  • 功能性可以细化成完备性、正确性。
    • 完备性:是与软件功能完整、齐全有关的软件属性。如果软件实际完成的功能,少于或不符合研制任务书所规定的明确或隐含的那些功能,则不能说该软件的功能是完备的
    • 正确性:是与能否得到正确或相符的结果或效果有关的软件属性。软件的正确性,在很大程度上,与软件模块的工程模型(直接影响辅助计算的精度与辅助决策方案的优劣)和软件编制人员的编程水平有关。

可靠性

  • 可靠性是指在一定的环境下,在给定的时间内,系统不发生故障(可以正常运行)的概率

  • 软件可靠性分析通常采用统计方法【度量指标】,遗憾的是目前可供第一线开发人员使用的成果很少见,大多数文章限于理论研究。

  • 口语中的可靠性含义宽泛,几乎囊括了正确性、健壮性。

    • 只要人们发现系统有毛病,便归结为可靠性差。从专业角度讲,这种说法是确切的。
    • 时隐时现的错误一般都属于可靠性问题,纠错的代价很高。
  • 软件可靠性问题主要是在编程时候埋下的祸害(很难测试出来)

    • 应当提倡规范化程序设计,预防可靠性祸害。
  • 可靠性是软件无故障执行一段时间的概率。健壮性和有效性,有时可看成是可靠性的一部分。

    • 衡量软件可靠性的方法,包括正确执行操作所占的比例,在发现新缺陷之前系统运行的时间长度和缺陷出现的密度。
    • 根据如果发生故障对系统有多大影响和对于最大的可靠性的费用是否合理,来定量地确定可靠性需求。
    • 如果软件满足了可靠性需求,那么,即使该软件还存在缺陷,也可认为达到其可靠性目标。要求高可靠性的系统也是为高可测试性系统设计的。
  • 根据相关的软件测试与评估要求,可靠性可以细化为成熟性、稳定性、易恢复性。

    • 对于软件的可靠性评价,主要采用定量评价方法。即选择合适的可靠性度量因子(可靠性参数),然后,分析可靠性数据而得到参数具体值,最后进行评价。

可靠性参数:

  1. 可用度
  2. 初期故障率
  3. 偶然故障率
  4. 平均失效前时间
  5. 平均失效间隔时间
  6. 缺陷密度
  7. 平均失效恢复时间

性能

  • 性能通常是指软件的“时间-空间”效率,而不仅是指软件的运行速度
  • 人们总希望软件的运行速度高,并且占用资源少。性能优化的关键工作是找出限制性能的“瓶颈”。程序员可以通过优化数据结构、算法和代码来提高软件的性能。例如数据库程序的优化。分析算法的复杂度是很好的方法,可以达到“未卜先知”的功效。
  • 性能优化就好像从海绵里挤水一样,你不挤,水就不出来,越用力去挤海绵越干。有些程序员认为现在的计算机不仅速度越来越高,而且内存越来越大,因此软件性能优化的必要性下降了。这种看法是不对的,殊不知随着机器的升级,软件系统也越来越庞大了和复杂了,性能优化仍然大有必要。

安全性

  • 安全性是指防止系统被非法入侵的能力,既属于技术问题又属于管理问题。
  • 信息安全是一门比较深奥的学问,其发展是建立在正义与邪恶的斗争之上。这世界似乎不存在绝对安全的系统,连美国军方的系统都频频遭黑客入侵。如今全球黑客泛滥,真是“道高一尺,魔高一丈”啊!开发商和客户愿意为提高安全性而投入的资金是有限的,他们要考虑值不值得。
  • 究竟什么样的安全性是令人满意的呢?一般地,如果黑客为非法入侵花费的代价(考虑时间、费用、风险等因素)高于得到的好处,那么这样的系统可以认为是安全的。对于普通软件,并不一点要追求很高的安全性,也不能完全忽视安全性,要先分析黑客行为。

易用性

  • 易用性是指用户使用软件的容易程度
  • 现代人的生活节奏快,做什么事都想图个方便。所以把易用性作为重要的质量属性对待无可非议。
  • 导致软件易用性差的根本原因 :
    • 理工科大学教育存在缺陷:没有开设人机工程学、美学、心理学这些必修课,大部分开发人员不知道如何设计易用的软件产品。
    • 开发人员犯了“错位”的毛病:他以为只要自己用起来方便,用户也就会满意。软件的易用性要让用户来评价。
    • 当用户真的感到软件很好用时,一股温暖的感觉油然而生,于是就用“界面友好”、“方便易用”等词来评价软件产品。
  • “客户第一”,“以用户为中心”……

兼容性

  • 兼容性是指不同产品(或者新老产品)相互交换信息的能力。例如两个字处理软件的文件格式兼容,那么它们都可以操作对方的文件,这种能力对用户很有好处。兼容性又称为互操作性
  • 兼容性商业规则:弱者设法与强者兼容,否则无容身之地;强者应当避免被兼容,否则市场将被瓜分。
    • WPS一定要与Word兼容,否则活不下去,但是Word绝对不会主动与WPS兼容,除非WPS在中国占有一定优势。
    • 纵向兼容、横向兼容

内部质量

  • 内部质量只有开发人员关心
  • 它们可以帮助开发人员实现外部质量
  • 包括易理解性、可测试性、可维护性、可扩展性、可移植性、可复用性等

易理解性

  • 易理解性是开发人员理解软件产品的能力,意味着所有的工作成果要易读、易理解,可以提高团队开发效率,降低维护代价
  • 开发人员只有在自己思路清晰的时候才可能写出让别人易读、易理解的程序和文档。可理解的东西通常是简洁的。一个原始问题可能很复杂,但高水平的人就能够把软件系统设计得很简洁。如果软件系统臃肿不堪,它迟早会出问题。所以简洁是人们对工作“精益求精”的结果,而不是潦草应付的结果。

可测试性

  • 可测试性指的是测试软件组件或集成产品时查找缺陷的简易程度,又称为可验证性。
  • 如果产品中包含复杂的算法和逻辑,或如果具有复杂的功能性的相互关系,那么对于可测试性的设计就很重要。如果经常更改产品,那么可测试性也是很重要的,因为将经常对产品进行回归测试来判断更改是否破坏了现有的功能性。

可维护性

  • 可维护性表明了在软件中纠正一个缺陷或做一次更改的简易程度
  • 可维护性取决于理解软件、更改软件和测试软件的简易程度,可维护性与灵活性密切相关。高可维护性对于那些经历周期性更改的产品或快速开发的产品很重要。你可以根据修复一个问题所花的平均时间和修复正确的百分比来衡量可维护性。

可扩展性

  • 可扩展性反映软件适应“变化”的能力
  • 在软件开发过程中,“变化”是司空见惯的事情,如需求、设计的变化,算法的改进,程序的变化等等。
  • 现代软件产品通常采用“增量开发模式”,不断推出新版本,获取增值利润。可扩展性越来越重要。可扩展性是系统设计阶段重点考虑的质量属性。谈到软件的可扩展性,开发人员首先想到的是怎样提高可扩展性,于是努力去设计很好的体系结构来提高可扩展性,却不考虑该不该做这件事。从商业角度考虑,如果某个软件将不断地推出新版本,那么可扩展性很重要。但是如果软件永远都不会有下个版本(一次性买卖),那么根本无需提高可扩展性。

可移植性

  • 可移植性指的是软件不经修改或稍加修改就可以运行于不同软硬件环境(CPU、OS和编译器)的能力,主要体现为代码的可移植性。
  • 编程语言越低级,用它编写的程序越难移植,反之则越容易。这是因为,不同的硬件体系结构(例如Intel CPU和SPARC CPU)使用不同的指令集和字长,而OS和编译器可以屏蔽这种差异,所以高级语言的可移植性更好。Java程序号称“一次编译,到处运行”,具有100%的可移植性。为了提高Java程序的性能,最新的Java标准允许人们使用一些与平台相关的优化技术,这样优化后的Java程序虽然不能“一次编译,到处运行”,仍然能够 “一次编程,到处编译”。软件设计时应该将“设备相关程序”与“设备无关程序”分开,将“功能模块”与“用户界面”分开。

可复用性

  • 可复用性是指一个软件的组成部分可以在同一个项目的不同地方甚至在不同的项目中重复使用的能力
  • 传统的软件复用技术包括代码的复用、算法的复用和数据结构的复用等,但这些复用有时候会破坏系统的可维护性,可以通过设计模式、面向对象的继承和多态等机制提高软件的可复用性。

过程质量

  • 如果想保持一如既往的开发高质量的产品,过程必须是可靠
  • 如果想适应无法预计的工具或环境改变,过程必须是稳健
  • 过程的执行必须是高效
  • 如果想适应新的管理方式或组织形式,过程必须是可扩展
  • 如果想跨项目和组织来使用,过程必须是可重用
  • 过程质量与开发活动相关
  • 产品通过过程来进行开发
    • 如开发效率,时间控制等
  • 过程改进指南
    • CMM:Capability Maturity Model for Software (软件能力成熟度模型)
    • CMM由美国卡内基-梅隆大学软件工程研究所(CMU-SEI)研制。
    • CMM是用于衡量软件过程能力的事实上的标准,同时也是目前软件过程改进最好的参考标准

软件体系结构评估

评估的必要性

软件架构是软件工程早期设计阶段的产物,对软件系统或软件项目的开发,具有深远的影响,主要表现在以下两个方面。

  • 不恰当的架构,会给软件系统或软件项目的开发带来灾难。
    • 如果软件架构不恰当,就无法满足系统的性能要求,则系统的安全性也就无法实现。
    • 当客户要求提高可用性时,开发小组将会因忙于修改发现错误,而影响开发的进度、预算,这样,会使整个系统或项目的成本大幅提高,甚至会使整个软件系统或软件项目的开发因成本太高而终止。
  • 架构决定着项目的结构。
    • 如配置控制库、进度与预算、性能指标、开发小组结构、文档组织、测试、维护活动,都是围绕着架构展开的。
    • 假如在开发过程中发现错误,中途修改架构,会使整个项目的工作陷入混乱。
    • 鉴于以上架构对项目和系统的影响,需要对软件架构进行评估,这是降低项目和系统成本及避免灾难的有效手段。

软件架构评估可以在架构生命周期的任何阶段进行,一般的时机,有早期和后期两
种情况。

  • 早期:
    • 通常把早期实施的评估称为发现性评审,目的是找出较难实现的需求并划分其优先级,分析在实施这一评估时已有的“原型架构”。进行发现性评审时,一定要保证:
    • 在系统尚未最终确定、设计师已经比较清楚,应采用什么方案的情况下实施;
    • 风险承担者小组中,要有有权做出需求决策的人员;
    • 评审结果中,要有一组按优先级排列的需求,以备在不易满足所有需求的情况下使用。
  • 后期:
    • 这种评估是在架构已经完全确定后实施的,适用于开发组织有老系统的情况。
    • 评估老系统的架构,和新系统的架构所用的方法相同,通过评估,可以使用户明确是否可以通过改进老系统,来满足新的质量及功能需求。

基于场景的评估方法

  • 该方法的基本观点是,大多数软件质量属性极为复杂,根本无法用一个简单的尺度来衡量。
    • 同时,质量属性并不是处于隔离状态,只有在一定的上下文环境中才能做出关于质量属性的有意义的评判。
      • 利用场景技术可以具体化评估的目标,代替对质量属性(可维护性、可修改性、健壮性、灵活性等)的空洞表述,使对软件体系结构的测试成为可能。
  • 所以,场景对于评估具有非常关键的作用,整个评估过程,就是论证软件体系结构对关键场景的支持程度。

基于场景的软件架构分析方法步骤

  1. 分析问题域,建立功能场景库。
    • 针对具体项目在应用领域中的定位,展开需求分析,汇总系统预期功能并按对功能进行分类,以确保每项功能都能够得到详细描述,并为每个功能定义相应的场景,建立功能场景库。
  2. 通过功能场景库测试评价软件架构对各功能的支持度,并针对支持度差的功能展开架构分析。
    • 支持度的评价涉及到,架构是否满足功能场景、是否容易扩展该功能等。
    • 一旦发现支持度差的功能,进一步分析是否是由架构设计导致的,从中发现可能的架构设计缺陷和不足。
  3. 建立非功能指标参数树。
    • 选择一组感兴趣的非功能性指标,如可移植性、安全性、性能,并详细定义每一个指标的衡量属性、期望值、相应的场景。
  4. 应用指标参数树,对软件架构进行非功能性分析。
    • 通过比较架构在场景中的实际输出值、期望值,来评价架构对各个指标的各个属性的支持度,并在该过程中,发现软件架构的缺陷,找出风险决策、无风险决策、敏感点、权衡点。

不足:

  1. 评估的效果,对评估师经验的依赖程度较高。
  2. “重量级”的评估技术,成本较高。
  3. 没有考虑知识的积累和应用问题,造成资源的浪费。
  4. 缺乏实用的评估信息管理工具。

SAAM软件架构分析方法

  1. 特定目标
    • 对描述应用程序属性的文档,验证基本的体系结构假设和原则
    • 此外,该分析方法有利于评估体系结构固有的风险。SAAM指导对体系结构的检查,使其主要关注潜在的问题点,如需求冲突,或仅从某一参与者的观点出发的不全面的系统设计。
    • SAAM不仅能够评估体系结构对于特定系统需求的使用能力,也能被用来比较不同的体系结构。
  2. 评估技术
    • 场景技术。场景代表了描述体系结构属性的基础,描述了各种系统必须支持的活动和将要发生的变化。
  3. 质量属性
    • 这一方法的基本特点,是把任何形式的质量属性都具体化为场景,但可修改性是SAAM分析的主要质量属性。
  4. 风险承担者
    • SAAM协调不同参与者所感兴趣的方面,作为后续决策的基础,提供了对体系结构的公共理解。
  5. 体系结构描述
    • SAAM用于体系结构的最后版本,但早于详细设计。体系结构的描述形式应当被所有参与者理解。功能、结构、分配被定义为描述体系结构的三个主要方面。
  6. 方法活动
    • SAAM的主要输入问题是问题描述、需求声明和体系结构描述。

一般步骤:

  1. 场景形成。
    • 通过集体讨论,风险承担者提出反映自己需求的场景。
    • 在形成场景的过程中,要注意全面捕捉系统的主要用途、系统用户类型、系统将来可能的变更、系统在当前及可预见的未来,必须满足的质量属性等信息。只有这样,形成的场景才能代表与各种风险承担着者相关的任务。
    • 例如,对于某个变更,开发人员关心的,是实现该变更的难度和对性能的影响,而系统管理员则关系此变更对体系结构的可集成性的影响。
  2. 描述软件体系结构。
    • SAAM定义了功能性、结构、分配三个视角,来描述软件体系结构。功能性指示系统做了些什么,结构由组件和组件间的连接组成,从功能到结构的分配则描述了域上的功能性是如何在软件结构中实现的。场景的形成与软件体系结构的描述通常是相互促进的,并且需要重复的进行。
    • 软件体系结构设计师应该采用参加评估的所有人员都能充分理解的形式,对待评估的体系结构进行适当的描述。
    • 场景的形成和对体系结构的描述,通常是相互促进的。
  3. 场景的分类和优先级划分。
    • 分析过程中,需要确定一个场景是否需要修改该体系结构。不需要修改的场景称为直接场景,需要修改的场景则称为间接场景。另一方面,需要对场景设置优先级,保证在评估的有限时间内考虑最重要的场景。
    • 这一般可以通过演示现有的体系结构在执行此场景的表现来确定。在SAAM评估方法中,称这样的场景为直接场景。
    • 评估人员通过对场景设置优先级,可保证在评估的有限时间内考虑最重要的场景。
    • 这里的重要完全,是由风险承担者及其所关心的问题确定的。
    • 一般来说,基于SAAM的评估方法关心的,是诸如可修改性的质量属性,所以,在划分优先级之前,要对场景进行分类。
  4. 间接场景的单独评估。
    • 主要针对间接场景,列出为支持该场景所需要对体系结构做出的修改,估计出这些修改的代价。对于直接场景,只需弄清体系结构是如何实现这些场景的。
    • 一旦确定了要考虑的一组场景,就要把这些场景与体系结构的描述对应起来。
    • SAAM评估也使评估人员和风险承担者更清楚地认识,体系结构的组成及各个构件的动态交互情况。
    • 对每一个间接场景,必须列出为支持该场景而需要对体系结构所做的改动,估算出这些变更的代价。
    • 这一步快要结束时,应该给出全部场景的总结性列表。
  5. 评估场景交互。
    • 两个或多个间接场景,要求更改体系结构的同一个组件就称为场景交互。对场景交互的评估,能够暴露设计中的功能分配。
    • 当两个或多个间接场景要求更改体系结构的同一个构件时,我们就称,这些场景在这一组构件上相互作用。
    • 其次,场景的相互作用,能够暴露出体系结构设计文档未能充分说明的结构分解。
  6. 形成总体评估。
    • 按照相对重要性,为每个场景及场景交互设置一个权值,根据权值得出总体评价。
    • 如果要比较多个体系结构,或者针对同一体系结构提出多个不同的方案,可通过权值的确定,来得出总体评价。
    • 同一体系结构,对于有不同目的的组织来说,会得到一个不同的评价结果。

ATAM体系结构权衡分析方法

软件体系结构折中分析方法(Architecture Tradeoff Analysis Method,ATAM),是评估软件架构的一种综合全面的方法。

参与人员:

  1. 评估小组
    • 是评估体系结构项目外部的小组。通常由3到5个人组成。
    • 评估期间,该小组的每个成员,都要扮演大量的特定的角色。评估小组可能是一个常设小组,其中,要定期执行体系结构评估,其成员也可能是为了应对某次评估,从了解体系结构的人中挑选出来的。
    • 他们可能与开发小组为相同的组织工作,也可能是外部的咨询人员。任何情况下,他们都应该是有能力、没有偏见、专职的外部人员。
  2. 项目决策者
    • 对开发项目具有发言权,并有权要求进行某些改变。
    • 他们通常包括项目管理人员。如果有承担开费用的可以确定的客户,也应该列入其中。设计师肯定要参与评估,这是由软件体系结构评估的基本准则决定的。
  3. 涉众(Stakeholder)
    • 是软件项目的既得利益者,他们完成工作的能力与支持可修改性、安全性、高可靠性等特性的体系结构密切相关。
    • 涉众包括开发人员、测试人员、集成人员、维护人员、性能工程师、用户、正在分析系统交互的系统构建人员等。
    • 评估期间,他们的工作职责是清晰明白的阐述,体系结构应该满足的具体质量属性目标,使所开发的系统能够取得成功。

结果:

  1. 简洁的体系结构描述
    • 通常认为,体系结构文档是由对象模型、接口及其签名的列表或其他冗长的列表组成的。但ATAM的要求,就是在一个小时内表述体系结构,这样,就得到了一个简洁而且通常是可理解的体系结构表述。
  2. 表述清楚的业务目标
    • 开发小组的某些成员,通常是在ATAM评估上第一次看到表述清楚的业务目标。
  3. 用场景集合捕获的质量需求
    • 业务目标导致质量需求,一些重要的质量需求是用场景的形式捕获的。
  4. 体系结构决策到质量需求的映射
    • 可以根据体系结构决策所支持或阻碍的质量属性,来解释体系结构决策。对于在ATAM期间分析的每个质量场景,确定那些有助于实现该质量场景的体系结构决策。
  5. 所确定的敏感点和权衡点集合
    • 这些是对一个或多个质量属性具有显著影响的体系结构决策。
    • 例如,采用一个备份数据库,很明显是一个体系结构决策,影响了可靠性,是一个关于可靠性的敏感点。然而,保持备份将消耗系统资源,影响系统性能。因此,是可靠性和性能之间的权衡点。该决策是否有风险,取决于在体系结构的质量属性需求的上下文中。
  6. 有风险决策和无风险决策
    • ATAM中有风险决策的定义是,根据所陈述的质量属性需求,可能导致不期望的体系结构决策。无风险决策的定义与此类似,根据分析被认为是安全的体系结构决策。确定的风险,可以形成体系结构风险移植计划的基础。
  7. 风险主题的集合
    • 分析完成时,评估小组将分析所发现风险的集合,以寻找确定体系结构甚至体系结构过程和小组中的系统弱点。如果不采取相应的措施,这些风险主题,将影响项目的业务目标。
  • 评估的结果,用于建立一个最终书面报告。该报告概述ATAM方法,总
    结评估会议记录,捕获场景及其分析,对得到的结果进行分类。
    • 评估还会产生一些副结果。通常情况下,为评估准备的体系结构描述,可能比已经存在的任何体系结构都要清晰。
    • 这个额外准备的文档,经受住了评估的考验,可能会与项目一起保留下来。
    • 此外,参与人员创建的场景是业务目标和体系结构需求的表示,可以用来指导架构的演变。最后,可以把最终报告中的分析内容,作为对制定某些体系结构决策的依据。
  • ATAM评估还有一些无形的结果。
    • 这些结果,包括能够使涉众产生社群感,可以为设计师和涉众提供公开交流的渠道,使体系结构的所有参与者更好地理解体系结构及其优势和弱点。
    • 尽管这些结果很难度量,但是,重要性不亚于其他结果,而且,这些结果通常是存在时间最长的。

步骤:

评估方法比较

场景的生成方式不同:

  • SAAM方法采用头脑风暴(brainstorming)技术构建场景,要求风险承担者列举出若干场景,并将场景分为直接场景和间接场景两类,分别支持对体系的静态分析和动态分析。
    • 对间接场景,设计师应说明,需要对体系结构作哪些修改,才能适应间接场景的要求,并估计出这些更改的代价。
  • ATAM在具体评估中将场景分为三类,要求软件设计师必须清楚地向风险承担者表达,软件结构设计中所使用任何明确的体系结构方法,风险承担者根据确定使用的体系结构方法,采用“刺激-环境-响应”来生成三类场景。
    1. 用例场景(Use Case Scenario)。描述用户的期望与正在运行的系统交互,
      用于信息的获取。
    2. 生长场景(Growth Scenario)。预期的系统变更与质量属性关系。
    3. 探索场景(Exploratory Scenario)。

风险承担者商业动机表述方式不同:

  • ATAM建立在SAAM的基础上,借助于效用树将风险承担者的商业目标转换成质量属性需求,再转换成代表自己商业目标的场景。
  • 效用树的理论基础,是管理学中的“需要理论”,即通过刺激及其所产生期望响应来描述场景,根据期望的迫切程度确定场景优先级别,在理想情况下,所有场景都以“刺激-环境-响应”的形式表述。

软件体系结构的描述方式不同:

  • 在评估之前,首席软件设计师需要对软件体系结构作详略适当的讲解,这种信息讲解的表达透彻程度将直接影响体系结构的分析质量。
  • ATAM方法中软件体系结构的描述,采用Philippe Kruchten的“4+1”视图模型,即从五个不同的视角描述系统的体系结构,四个视图模型,从特定的不同方面描述软件的体系结构,忽略与此无关的实体。
  • 将四个视图有机地联系起来,场景视图不仅可以描述一个特定的视图内的构件关系,而且,可以描述不同视图间的构件关系。在四个视图中,逻辑视图、开发视图主要用来描述系统的静态结构;进程视图、物理视图主要用来描述系统的动态结构。ATAM在实际运用中并非每个系统都必须将五个视图都画出来,而是各有侧重。

思考与练习

某软件公司欲为某电子商务企业开发一个在线交易平台,支持客户完成网上购物活动中的在线交易。在系统开发之初,企业对该平台提出了如下要求:

  • 在线交易平台必须在1s内完成客户的交易请求。
  • 该平台必须保证客户个人信息和交易信息的安全。
  • 当发生故障时,该平台的平均故障恢复时间必须小于10s。
  • 由于企业业务发展较快,需要经常为该平台添加新功能或进行硬件升级。添加新功能或进行硬件升级必须要在6小时内完成。
    针对这些要求,该软件开发公司决定采用基于架构的软件开发方法,以架构为核心进行在线交易平台的设计与实现。
    请对该在线交易平台的4个要求进行分析,指出每个要求对应何种软件质量属性;并针对每种软件质量属性,各给出2种实现该质量属性的架构设计策略。
  1. 性能(Performance) - 在1秒内完成客户的交易请求。
    • 设计策略1:使用缓存技术。通过缓存常用数据或预先计算的结果,减少数据库的查询次数,提高响应速度。
    • 设计策略2:负载均衡。通过负载均衡器分配请求到多个服务器,确保在高流量下系统仍然能够快速响应。
  2. 安全性(Security) - 保证客户个人信息和交易信息的安全。
    • 设计策略1:数据加密。对敏感数据如个人信息和交易数据进行加密,确保数据在传输和存储过程中的安全。
    • 设计策略2:使用安全协议。如采用HTTPS协议保证数据传输过程的安全,防止中间人攻击。
  3. 可靠性(Reliability) - 故障恢复时间必须小于10秒。
    • 设计策略1:冗余部署。通过部署备用系统或组件,当主系统出现故障时,可以迅速切换到备用系统,减少故障恢复时间。
    • 设计策略2:自动故障检测与恢复。实现系统的自动监控与故障检测,一旦发现异常,自动启动恢复流程。
  4. 可维护性(Maintainability) - 快速添加新功能或进行硬件升级。
    • 设计策略1:模块化设计。采用模块化的架构设计,使得添加或更新某个功能模块不会影响到整个系统的其他部分。
    • 设计策略2:自动化部署和测试。使用自动化工具来管理部署和测试过程,使新功能的集成和硬件升级过程更加高效和可靠。

设计模式概述

模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案
A pattern is a successful or efficient solution to a recurring problem within a context.
每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心,通过这种方式,人们可以无数次地重用那些已有的解决方案,无须再重复相同的工作。

软件模式:在一定条件下的软件开发问题及其解法

  • 问题描述
  • 前提条件(环境或约束条件)
  • 解法
  • 效果

大三律(Rule of Three):只有经过3个以上不同类型(或不同领域)的系统的校验,一个解决方案才能从候选模式升格为模式

设计模式(Design Pattern):设计模式是在特定环境下为解决某一通用软件设计问题提供的一套定制的解决方案,该方案描述了对象和类之间的相互作用。

  • 一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结
  • 是一种用于对软件系统中不断重现的设计问题的解决方案进行文档化的技术
  • 是一种共享专家设计经验的技术
  • 目的:为了可重用代码、让代码更容易被他人理解、提高代码可靠性

设计模式一般包含模式名称、问题、目的、解决方案、效果、实例代码和相关设计模式等基本要素,4个关键要素如下:

  • 模式名称 (Pattern Name)
  • 问题 (Problem)
  • 解决方案 (Solution)
  • 效果 (Consequences)

根据目的(模式是用来做什么的)可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三类:

  • 创建型模式主要用于创建对象
  • 结构型模式主要用于处理类或对象的组合
  • 行为型模式主要用于描述类或对象如何交互和怎样分配职责

根据范围,即模式主要是处理类之间的关系还是处理对象之间的关系,可分为类模式和对象模式两种:

  • 类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是一种静态关系
  • 对象模式处理对象间的关系,这些关系在运行时变化,更具动态性

设计模式的优点:

  • 融合了众多专家的经验,并以一种标准的形式供广大开发人员所用
  • 提供了一套通用的设计词汇和一种通用的语言,以方便开发人员之间进行沟通和交流,使得设计方案更加通俗易懂
  • 让人们可以更加简单方便地复用成功的设计和体系结构
  • 使得设计方案更加灵活,且易于修改
  • 将提高软件系统的开发效率和软件质量,且在一定程度上节约设计成本
  • 有助于初学者更深入地理解面向对象思想,方便阅读和学习现有类库与其他系统中的源代码,还可以提高软件的设计水平和代码质量

面向对象设计原则

原则概述

  • 可维护性(Maintainability):指软件能够被理解、改正、适应及扩展的难易程度

  • 可复用性(Reusability):指软件能够被重复使用的难易程度

  • 面向对象设计的目标之一在于支持可维护性复用,一方面需要实现设计方案或者源代码的复用,另一方面要确保系统能够易于扩展和修改,具有良好的可维护性

  • 面向对象设计原则为支持可维护性复用而诞生

  • 指导性原则,非强制性原则

  • 每一个设计模式都符合一个或多个面向对象设计原则,面向对象设计原则是用于评价一个设计模式的使用效果的重要指标之一

单一职责原则

单一职责原则定义:

  • 单一职责原则是一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
    • 最简单的面向对象设计原则,用于控制类的粒度大小
    • 就一个类而言,应该仅有一个引起它变化的原因
  • 原则分析
    • 一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小
    • 当一个职责变化时,可能会影响其他职责的运作
    • 将这些职责进行分离,将不同的职责封装在不同的类中
    • 将不同的变化原因封装在不同的类中
    • 单一职责原则是实现高内聚、低耦合的指导方针

单一职责原则实例

某软件公司开发人员针对CRM(Customer Relationship Management,客户关系管理)系统中的客户信息图表统计模块提出了如图所示的初始设计方案。

在图中,getConnection()方法用于连接数据库,findCustomers()用于查询所有的客户信息,createChart()用于创建图表,displayChart()用于显示图表。
现使用单一职责原则对其进行重构。

开闭原则

开闭原则定义

  • 开闭原则:软件实体应当对扩展开放,对修改关闭
    • 开闭原则是面向对象的可复用设计的第一块基石,是最重要的面向对象设计原则
    • 在开闭原则的定义中,软件实体可以是一个软件模块、一个由多个类组成的局部结构或一个独立的类
    • 开闭原则是指软件实体应尽量在不修改原有代码的情况下进行扩展
  • 原则分析
    • 抽象化是开闭原则的关键
    • 相对稳定的抽象层 + 灵活的具体层
    • **对可变性封装原则(Principle of Encapsulation of Variation, EVP)**:找到系统的可变因素并将其封装起来

里氏代换原则

里氏代换原则:所有引用基类的地方必须能透明地使用其子类的对象。
原则分析:

  • 在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立。如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象
  • 在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型

依赖倒转原则

依赖倒转原则:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

  • 针对接口编程,不要针对实现编程
  • 在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等
  • 在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中

原则分析:
针对抽象层编程,将具体类的对象通过**依赖注入(Dependency Injection, DI)**的方式注入到其他对象

  • 构造注入
  • 设值注入(Setter注入)
  • 接口注入

OCP【开闭】/LSP【里氏代换】/DIP【依赖倒转】综合实例

某软件公司开发人员在开发CRM系统时发现:该系统经常需要将存储在TXT或Excel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。在客户数据操作类CustomerDAO中将调用数据格式转换类的方法来实现格式转换,初始设计方案结构如图2-3所示:

图2-3 初始设计方案结构图
在编码实现图2-3所示结构时,该软件公司开发人员发现该设计方案存在一个非常严重的问题,由于每次转换数据时数据来源不一定相同,因此需要经常更换数据转换类,例如有时候需要将TXTDataConvertor改为ExcelDataConvertor,此时需要修改CustomerDAO的源代码,而且在引入并使用新的数据转换类时也不得不修改CustomerDAO的源代码,系统扩展性较差,违反了开闭原则,现需要对该方案进行重构。

接口隔离原则

接口隔离原则:客户端不应该依赖那些它不需要的接口

原则分析

  • 当一个接口太大时,需要将它分割成一些更细小的接口
  • 使用该接口的客户端仅需知道与之相关的方法即可
  • 每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干
  • “接口”定义(1):一个类型所提供的所有方法特征的集合。一个接口代表一个角色,每个角色都有它特定的一个接口,“角色隔离原则
  • “接口”定义(2):狭义的特定语言的接口。接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口,每个接口中只包含一个客户端所需的方法,“定制服务

接口隔离原则示例

某软件公司开发人员针对CRM系统的客户数据显示模块设计了如图2-5所示CustomerDataDisplay接口,其中方法readData()用于从文件中读取数据,方法transformToXML()用于将数据转换成XML格式,方法createChart()用于创建图表,方法displayChart()用于显示图表,方法createReport()用于创建文字报表,方法displayReport()用于显示文字报表。

图2-5 初始设计方案结构图
在实际使用过程中开发人员发现该接口很不灵活,例如:如果一个具体的数据显示类无须进行数据转换(源文件本身就是XML格式),但由于实现了该接口,不得不实现其中声明的transformToXML()方法(至少需要提供一个空实现);如果需要创建和显示图表,除了需要实现与图表相关的方法外,还需要实现创建和显示文字报表的方法,否则程序在编译时将报错。
现使用接口隔离原则对其进行重构。

合成复用原则

合成复用原则:优先使用对象组合,而不是继承来达到复用的目的。

  • 合成复用原则又称为组合/聚合复用原则(Composition/ Aggregate Reuse Principle, CARP)

原则分析:

  • 合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分
  • 新对象通过委派调用已有对象的方法达到复用功能的目的
  • 复用时要尽量使用组合/聚合关系(关联关系),少用继承
  • 继承复用:实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。(“白箱”复用 )
  • 组合/聚合复用:耦合度相对较低,有选择性地调用成员对象的操作;可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。(“黑箱”复用 )

实例

某软件公司开发人员在初期的CRM系统设计中,考虑到客户数量不多,系统采用Access作为数据库,与数据库操作有关的类,例如CustomerDAO类等都需要连接数据库,连接数据库的方法getConnection()封装在DBUtil类中,由于需要重用DBUtil类的getConnection()方法,设计人员将CustomerDAO作为DBUtil类的子类,初始设计方案结构如图2-7所示。

图2-7 初始设计方案结构图
随着客户数量的增加,系统决定升级为Oracle数据库,因此需要增加一个新的OracleDBUtil类来连接Oracle数据库,由于在初始设计方案中CustomerDAO和DBUtil之间是继承关系,因此在更换数据库连接方式时需要修改CustomerDAO类的源代码,将CustomerDAO作为OracleDBUtil的子类,这将违背开闭原则。当然也可以直接修改DBUtil类的源代码,这同样也违背了开闭原则。
现使用合成复用原则对其进行重构。

迪米特法则

迪米特法则:每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

  • 迪米特法则又称为最少知识原则(Least Knowledge Principle, LKP)
  • 迪米特法则要求一个软件实体应当尽可能少地与其他实体发生相互作用
  • 应用迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系

分析:

  • 迪米特法则要求在设计系统时,应该尽量减少对象之间的交互
  • 如果两个对象之间不必彼此直接通信,那么这两个对象就不应该发生任何直接的相互作用
  • 如果其中一个对象需要调用另一个对象的方法,可以通过“第三者”转发这个调用
  • 通过引入一个合理的“第三者”来降低现有对象之间的耦合度

实例

某软件公司所开发CRM系统包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应。例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可以简化为如图2-9所示的结构。

图2-9 初始设计方案结构图
在图2-9中,由于界面控件之间的交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除控件。
现使用迪米特法则对其进行重构。

简单工厂模式

创建型模式

  • 创建型模式(Creational Pattern)关注对象的创建过程
  • 创建型模式对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离,对用户隐藏了类的实例的创建细节
  • 创建型模式描述如何将对象的创建和使用分离,让用户在使用对象时无须关心对象的创建细节,从而降低系统的耦合度,让设计方案更易于修改和扩展

简单工厂模式概述

简单工厂模式基本实现流程

  • 具体产品类:将需要创建的各种不同产品对象的相关代码封装到具体产品类中
  • 抽象产品类:将具体产品类公共的代码进行抽象和提取后封装在一个抽象产品类中
  • 工厂类:提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入参数的不同创建不同的具体产品对象
  • 客户端:只需调用工厂类的工厂方法并传入相应的参数即可得到一个产品对象

简单工厂模式 (Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类

  • 在简单工厂模式中用于创建实例的方法通常是静态(static)方法,因此又被称为静态工厂方法(Static Factory Method)模式
  • 要点:如果需要什么,只需要传入一个正确的参数,就可以获取所需要的对象,而无须知道其创建细节

结构与实现


抽象产品类:

1
2
3
4
5
6
7
8
9
public abstract class Product {
//所有产品类的公共业务方法
public void methodSame() {
//公共方法的实现
}
 
//声明抽象业务方法
public abstract void methodDiff();
}

具体产品类:

1
2
3
4
5
6
public class ConcreteProduct extends Product{
//实现业务方法
public void methodDiff() {
//业务方法的实现
}
}

工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Factory {
//静态工厂方法
public static Product getProduct(String arg) {
Product product = null;
if (arg.equalsIgnoreCase("A")) {
product = new ConcreteProductA();
//初始化设置product
}
else if (arg.equalsIgnoreCase("B")) {
product = new ConcreteProductB();
//初始化设置product
}
return product;
}
}

客户端

1
2
3
4
5
6
7
8
public class Client {
public static void main(String args[]) {
Product product;
product = Factory.getProduct("A"); //通过工厂类创建产品对象
product.methodSame();
product.methodDiff();
}
}

实例

某软件公司要基于Java语言开发一套图表库,该图表库可以为应用系统提供多种不同外观的图表,例如柱状图(HistogramChart)、饼状图(PieChart)、折线图(LineChart)等。该软件公司图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,通过设置不同的参数即可得到不同类型的图表,而且可以较为方便地对图表库进行扩展,以便能够在将来增加一些新类型的图表。
现使用简单工厂模式来设计该图表库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
 
public class XMLUtil {
//该方法用于从XML配置文件中提取图表类型,并返回类型名
public static String getChartType() {
try {
//创建文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("src//designpatterns//simplefactory//config.xml"));

//获取包含图表类型的文本结点
NodeList nl = doc.getElementsByTagName("chartType");
Node classNode = nl.item(0).getFirstChild();
String chartType = classNode.getNodeValue().trim();
return chartType;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}

创建对象与使用对象

Java语言创建对象的几种方式

  • 使用new关键字直接创建对象
  • 通过反射机制创建对象
  • 通过克隆方法创建对象
  • 通过工厂类创建对象


引入工厂类UserDAOFactory

  • 如果UserDAO的某个子类的构造函数发生改变或者需要添加或移除不同的子类,只要维护UserDAOFactory的代码,不会影响到LoginAction
  • 如果UserDAO的接口发生改变,例如添加、移除方法或改变方法名,只需要修改LoginAction,不会给UserDAOFactory带来任何影响

两个类A和B之间的关系应该仅仅是A创建B或者是A使用B,而不能两种关系都有。将对象的创建和使用分离,使得系统更加符合单一职责原则,有利于对功能的复用和系统的维护。

将对象的创建与使用分离的其他好处

  • 防止用来实例化一个类的数据和代码在多个类中到处都是,可以将有关创建的知识搬移到一个工厂类中,解决代码重复、创建蔓延的问题
  • 构造函数的名字都与类名相同,从构造函数和参数列表中大家很难了解不同构造函数所构造的产品的差异->将对象的创建过程封装在工厂类中,可以提供一系列名字完全不同的工厂方法,每一个工厂方法对应一个构造函数,客户端可以以一种更加可读、易懂的方式来创建对象

何时不需要工厂?

  • 无须为系统中的每一个类都配备一个工厂类
  • 如果一个类很简单,而且不存在太多变化,其构造过程也很简单,此时就无须为其提供工厂类,直接在使用之前实例化即可
  • 否则会导致工厂泛滥,增加系统的复杂度
  • 例如:java.lang.String

简单工厂模式的简化

将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中

优缺点与适用环境

模式优点

  • 实现了对象创建和使用的分离
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可
  • 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性

模式缺点

  • 工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响
  • 增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度
  • 系统扩展困难,一旦添加新产品不得不修改工厂逻辑
  • 由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构,工厂类不能得到很好地扩展

适用环境

  • 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂
  • 客户端只知道传入工厂类的参数,对于如何创建对象并不关心

工厂方法模式

概述

  • 使用简单工厂模式设计的按钮工厂
  • 使用工厂方法模式改进后的按钮工厂
  • 分析:
    • 工厂方法模式不再提供一个按钮工厂类来统一负责所有产品的创建,而是将具体按钮的创建过程交给专门的工厂子类去完成
    • 如果出现新的按钮类型,只需要为这种新类型的按钮定义一个具体的工厂类就可以创建该新按钮的实例

工厂方法模式:定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类

  • 简称为工厂模式(Factory Pattern)
  • 又可称作虚拟构造器模式(Virtual Constructor Pattern)多态工厂模式(Polymorphic Factory Pattern)
  • 工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象
  • 目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类

结构与实现

工厂方法模式包含以下4个角色:

  • Product(抽象产品)
  • ConcreteProduct(具体产品)
  • Factory(抽象工厂)
  • ConcreteFactory(具体工厂)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 抽象工厂类
public interface Factory {
public Product factoryMethod();
}

// 具体工厂类
public class ConcreteFactory implements Factory {
public Product factoryMethod() {
return new ConcreteProduct();
}
}

// 客户端片段
……
Factory factory;
factory = new ConcreteFactory(); //可通过配置文件和反射机制实现
Product product;
product = factory.factoryMethod();
……

实例

某系统运行日志记录器(Logger)可以通过多种途径保存系统的运行日志,例如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。
为了更好地封装记录器的初始化过程并保证多种记录器切换的灵活性,现使用工厂方法模式设计该系统。

在未使用配置文件和反射机制之前,更换具体工厂类需修改客户端源代码,但无须修改类库代码

反射机制与配置文件

Java反射机制(Java Reflection)

  • Java反射(Java Reflection)是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包括实例的创建和实例类型的判断
  • Class类的实例表示正在运行的Java应用程序中的类和接口,其forName(String className)方法可以返回与带有给定字符串名的类或接口相关联的Class对象,再通过Class对象的newInstance()方法创建此对象所表示的类的一个新实例,即通过一个类名字符串得到类的实例

java反射机制

1
2
3
4
//通过类名生成实例对象并将其返回
Class c=Class.forName(“java.lang.String");
Object obj=c.newInstance();
return obj;

配置文件:

1
2
3
4
5
<!— config.xml -->
<?xml version="1.0"?>
<config>
<className>designpatterns.factorymethod.FileLoggerFactory</className>
</config>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package designpatterns.factorymethod;

//XMLUtil.java
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;

public class XMLUtil {
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
public static Object getBean() {
try {
//创建DOM文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("src//designpatterns//factorymethod//config.xml"));

//获取包含类名的文本结点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();

//通过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}

1
2
3
4
5
6
7
8
9
10
11
package designpatterns.factorymethod;

public class Client {
public static void main(String args[]) {
LoggerFactory factory;
Logger logger;
factory = (LoggerFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,需要进行强制类型转换
logger = factory.createLogger();
logger.writeLog();
}
}

增加新产品的步骤

  1. 增加一个新的具体产品类作为抽象产品类的子类
  2. 增加一个新的具体工厂类作为抽象工厂类的子类,该工厂用于创建新增的具体产品对象
  3. 修改配置文件,用新的具体工厂类的类名字符串替换原有工厂类类名字符串
  4. 编译新增具体产品类和具体工厂类,运行客户端代码,即可完成新产品的增加和使用

工厂方法的重载

结构图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface LoggerFactory {
public Logger createLogger();
public Logger createLogger(String args);
public Logger createLogger(Object obj);
}

public class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//使用默认方式连接数据库,代码省略
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}

public Logger createLogger(String args) {
//使用参数args作为连接字符串来连接数据库,代码省略
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}

public Logger createLogger(Object obj) {
//使用封装在参数obj中的连接字符串来连接数据库,代码省略
Logger logger = new DatabaseLogger();
//使用封装在参数obj中的数据来初始化数据库日志记录器,代码省略
return logger;
}
}
//其他具体工厂类代码省略

工厂方法的隐藏

目的:为了进一步简化客户端的使用
实现:在工厂类中直接调用产品类的业务方法,客户端无须调用工厂方法创建产品对象,直接使用工厂对象即可调用所创建的产品对象中的业务方法

抽象工厂类LoggerFactory示意代码:

1
2
3
4
5
6
7
8
9
10
//将接口改为抽象类
public abstract class LoggerFactory {
//在工厂类中直接调用日志记录器类的业务方法writeLog()
public void writeLog() {
Logger logger = this.createLogger();
logger.writeLog();
}

public abstract Logger createLogger();
}

客户端代码:

1
2
3
4
5
6
7
public class Client {
public static void main(String args[]) {
LoggerFactory factory;
factory = (LoggerFactory)XMLUtil.getBean();
factory.writeLog(); //直接使用工厂对象来调用产品对象的业务方法
}
}

模式优缺点与适用环境

模式优点

  • 工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节
  • 能够让工厂自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部
  • 在系统中加入新产品时,完全符合开闭原则

模式缺点:

  • 系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,会给系统带来一些额外的开销
  • 增加了系统的抽象性和理解难度

适用环境:

  • 客户端不知道它所需要的对象的类(客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体产品对象由具体工厂类创建)
  • 抽象工厂类通过其子类来指定创建哪个对象

抽象工厂模式

产品等级结构与产品族

  • 工厂方法模式
    • 每个具体工厂只有一个或者一组重载的工厂方法,只能生产一种产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销
  • 抽象工厂模式
    • 一个工厂可以生产一系列产品(一族产品),极大减少了工厂类的数量

概念

  • 产品等级结构:产品等级结构即产品的继承结构
  • 产品族:产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品

模式动机

  • 当系统所提供的工厂生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构、属于不同类型的具体产品时就可以使用抽象工厂模式
  • 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

  • 对象创建型模式
  • 又称为**工具(Kit)**模式
  • 抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品
  • 当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率

结构与实现

抽象工厂模式的结构

  • AbstractFactory(抽象工厂)
  • ConcreteFactory(具体工厂)
  • AbstractProduct(抽象产品)
  • ConcreteProduct(具体产品)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 抽象工厂类
public interface AbstractFactory {
public AbstractProductA createProductA(); //工厂方法一
public AbstractProductB createProductB(); //工厂方法二
……
}

// 具体工厂类
public class ConcreteFactory1 implements AbstractFactory {
//工厂方法一
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}

//工厂方法二
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
……
}

实例

某软件公司要开发一套界面皮肤库,可以对基于Java的桌面软件进行界面美化。用户在使用时可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素,例如春天(Spring)风格的皮肤将提供浅绿色的按钮、绿色边框的文本框和绿色边框的组合框,而夏天(Summer)风格的皮肤则提供浅蓝色的按钮、蓝色边框的文本框和蓝色边框的组合框,其结构示意图如下图所示:

该皮肤库需要具备良好的灵活性和可扩展性,用户可以自由选择不同的皮肤,开发人员可以在不修改既有代码的基础上增加新的皮肤。
现使用抽象工厂模式来设计该界面皮肤库。

更换皮肤,只需修改配置文件

1
2
3
4
<?xml version="1.0"?>
<config>
<className>designpatterns.abstractfactory.SpringSkinFactory</className>
</config>

存在一个非常严重的问题:如果在设计之初因为考虑不全面,忘记为某种类型的界面组件(以单选按钮 RadioButton 为例)提供不同皮肤下的风格化显示,那么在往系统中增加单选按钮时将发现非常麻烦,无法在满足开闭原则的前提下增加单选按钮,原因是抽象工厂SkinFactory 中根本没有提供创建单选按钮的方法,如果需要增加单选按钮,首先需要修改抽象工厂接口 SkinFactory,在其中新增声明创建单选按钮的方法,然后逐个修改具体工厂类,增加相应方法以实现在不同的皮肤库中创建单选按钮,此外还需要修改客户端,否则单选按钮无法应用于现有系统。

开闭原则的倾斜性

  • 增加产品族
    • 对于增加新的产品族,抽象工厂模式很好地支持了开闭原则,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改
  • 增加新的产品等级结构
    • 对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了开闭原则
    • 正因为抽象工厂模式存在开闭原则的倾斜性,它以一种倾斜的方式来满足开闭原则,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够考虑全面,不会在设计完成之后再向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。

优缺点与适用环境

模式优点

  • 隔离了具体类的生成,使得客户端并不需要知道什么被创建
  • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象
  • 增加新的产品族很方便,无须修改已有系统,符合开闭原则

模式缺点

  • 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了开闭原则

模式适用环境

  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节
  • 系统中有多于一个的产品族,但每次只使用其中某一产品族
  • 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来
  • 产品等级结构稳定,在设计完成之后不会向系统中增加新的产品等级结构或者删除已有的产品等级结构

建造者模式——自学

建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

  • 将客户端与包含多个部件的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可
  • 关注如何逐步创建一个复杂的对象,不同的建造者定义了不同的创建过程

结构:

建造者模式的结构:

  • Builder(抽象建造者)
  • ConcreteBuilder(具体建造者)
  • Product(产品)
  • Director(指挥者)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 复杂对象类
public class Product {
private String partA; //定义部件,部件可以是任意类型,包括值类型和引用类型
private String partB;
private String partC;

//partA的Getter方法和Setter方法省略
//partB的Getter方法和Setter方法省略
//partC的Getter方法和Setter方法省略
}

// 抽象建造者类
public abstract class Builder {
//创建产品对象
protected Product product=new Product();
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();

//返回产品对象
public Product getResult() {
return product;
}
}

// 具体建造者类
public class ConcreteBuilder1 extends Builder{
public void buildPartA() {
product.setPartA("A1");
}

public void buildPartB() {
product.setPartB("B1");
}

public void buildPartC() {
product.setPartC("C1");
}
}

// 指挥者类
public class Director {
private Builder builder;

public Director(Builder builder) {
this.builder=builder;
}

public void setBuilder(Builder builder) {
this.builder=builer;
}

//产品构建与组装方法
public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}

// 客户类片段
……
Builder builder = new ConcreteBuilder1(); //可通过配置文件实现
Director director = new Director(builder);
Product product = director.construct();
……

实例

某游戏软件公司决定开发一款基于角色扮演的多人在线网络游戏,玩家可以在游戏中扮演虚拟世界中的一个特定角色,角色根据不同的游戏情节和统计数据(例如力量、魔法、技能等)具有不同的能力,角色也会随着不断升级而拥有更加强大的能力。
作为该游戏的一个重要组成部分,需要对游戏角色进行设计,而且随着该游戏的升级将不断增加新的角色。通过分析发现,游戏角色是一个复杂对象,它包含性别、面容等多个组成部分,不同类型的游戏角色,其性别、面容、服装、发型等外部特性都有所差异,例如“天使”拥有美丽的面容和披肩的长发,并身穿一袭白裙;而“恶魔”极其丑陋,留着光头并穿一件刺眼的黑衣。
无论是何种造型的游戏角色,它的创建步骤都大同小异,都需要逐步创建其组成部分,再将各组成部分装配成一个完整的游戏角色。现使用建造者模式来实现游戏角色的创建。

结果及分析

  • 如果需要更换具体角色建造者,只需要修改配置文件
  • 当需要增加新的具体角色建造者时,只需将新增具体角色建造者作为抽象角色建造者的子类,然后修改配置文件即可,原有代码无须修改,完全符合开闭原则
1
2
3
4
<?xml version="1.0"?>
<config>
<className>designpatterns.builder.AngelBuilder</className>
</config>

深入讨论

省略Director

  • 将Director和抽象建造者Builder合并
  • 将construct()方法中的参数去掉,直接在construct()方法中调用buildPartX()方法

钩子方法的引入
钩子方法(Hook Method):返回类型通常为boolean类型,方法名一般为isXXX()

优缺点与适用环境

模式优点

  • 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  • 每一个具体建造者都相对独立,与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,扩展方便,符合开闭原则
  • 可以更加精细地控制产品的创建过程

模式缺点

  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,不适合使用建造者模式,因此其使用范围受到一定的限制
  • 如果产品的内部变化复杂,可能会需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加了系统的理解难度和运行成本

模式适用环境

  • 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员变量
  • 需要生成的产品对象的属性相互依赖,需要指定其生成顺序
  • 对象的创建过程独立于创建该对象的类。在建造者模式中通过引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类和客户类中
  • 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

原型模式

原型模式:使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象

  • 工作原理:将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现创建过程
  • 创建新对象(也称为克隆对象)的工厂就是原型类自身工厂方法由负责复制原型对象的克隆方法来实现
  • 通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,每一个克隆对象都是独立的
  • 通过不同的方式对克隆对象进行修改以后,可以得到一系列相似但不完全相同的对象
  • 由于在软件系统中经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在软件开发中具有较高的使用频率

结构与实现

原型模式的结构

  • Prototype(抽象原型类)

  • ConcretePrototype(具体原型类)

  • Client(客户类)

  • 浅克隆(Shallow Clone):当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制

  • 深克隆(Deep Clone):除了对象本身被复制外,对象所包含的所有成员变量也将被复制

通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,同时将相关的参数传入新创建的对象中,保证它们的成员变量相同。示意代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Prototype {
public abstract Prototype clone();
}

public class ConcretePrototype extends Prototype {
private String attr;

public void setAttr() {
this.attr = attr;
}

public String getAttr() {
return this.attr;
}

// 克隆方法
public Prototype clone() {
Prototype prototype = new ConcretePrototype(); // 创建新对象
prototype.setAttr(this.attr);
return prototype;
}
}

在客户端类中只需要创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象,例如:

1
2
3
4
5
...
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAttr("Sunny");
ConcretePrototype copy = (ConcretePrototype)prototype.clone();
...

在原型模式的通用实现方法中,可通过手工编写clone()方法来实现浅克隆和深克隆。对于引用类型的对象,可以在clone()方法中通过赋值的方式来实现复制,这是一种浅克降实现方案;如果在clone()方法中通过创建一个全新的成员对象来实现复制,则是一种深克隆实现方案。

在 Java 语言中,所有的 Java 类均继承自 java.lang.Object 类,Object 类提供了一个 clone() 方法,可以将一个 Java 对象复制一份。因此在 Java 中可以直接使用 Object 提供的 clone() 方法来实现对象的浅克隆。
需要注意的是能够实现克隆的 Java 类必须实现一个标识接口 Cloneable ,表示这个 Java 类支持被复制。如果一个类没有实现这个接口但是调用了 lone() 方法, Java 编译器将抛出一个 CloneNotSupportedException 异常。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcretePrototype implements Cloneable {
...
public Prototype clone() {
Object object = null;

try {
object = super.clone(); // 浅克隆
} catch(CloneNotSupportedException exception) {
System.err.println("Not support cloneable");
}

return (Prototype) object;
}
...
}

Java 语言中的 clone()方法满足以下几点:

  1. 对任何对象 x ,都有 x.clone()!=x ,即克隆对象与原型对象不是同一个对象
  2. 对任何对象 x ,都有 x.clone().getClass()==x.getClass() ,即克隆对象与原型对象的类型一样。
  3. 如果对象 x 的 equals() 方法定义恰当,那么 x.clone().equals(x)应该成立
    为了获取对象的一个克隆,可以直接利用 Object 类的 clone()方法,具体步骤如下:
  4. 在派生类中覆盖基类的 clone() 方法,并声明为 public。
  5. 在派生类的 clone() 方法中调用 super.clone()。
  6. 派生类需实现 Cloneable 接口。
    此时,Object 类相当于抽象原型类,所有实现了 Cloneable 接口的类相当于具体原型类

实例

在使用某OA系统时,有些岗位的员工发现他们每周的工作都大同小异,因此在填写工作周报时很多内容都是重复的,为了提高工作周报的创建效率,大家迫切希望有一种机制能够快速创建相同或者相似的周报,包括创建周报的附件。
试使用原型模式对该OA系统中的工作周报创建模块进行改进。

浅克隆:周报不同,附件相同

为了能够在复制周报的同时也能够复制附件对象,需要采用深克隆机制。在 Java 语言中可以通过序列化(Serialization)等方式来实现深克隆。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个复制,而原对象仍然存在于内存中。通过序列化实现的复制不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现 Serializable 接口,否则无法实现序列化操作。
下面使用深克隆技术来实现工作周报和附件对象的复制,由于要将附件对象和工作周报对象都写人流中,因此两个类均需要实现 Serializable 接口。

深克隆:周报附件均不同

原型管理器

原型管理器(Prototype Manager)将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。

在实际开发中可以将 PrototypeManager 设计为单例类,确保系统中有且仅有一个 PrototypeManager 对象,这样既有利于节省系统资源,还可以更好地对原型管理器对象进行控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class PrototypeManager {
private Hashtable prototypeTable = new Hashtable(); //使用Hashtable存储原型对象

public PrototypeManager(){
prototypeTable.put("A", new ConcretePrototypeA());
prototypeTable.put("B", new ConcretePrototypeB());
}

public void add(String key, Prototype prototype){
prototypeTable.put(key, prototype);
}

public Prototype get(String key){
Prototype clone = null;
clone = ((Prototype) prototypeTable.get(key)).clone(); //通过克隆方法创建新对象
return clone;
}
}

优缺点与适用环境

模式优点

  • 简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率
  • 扩展性较好
  • 提供了简化的创建结构,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品
  • 可以使用深克隆的方式保存对象的状态,以便在需要的时候使用,可辅助实现撤销操作

模式缺点

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了开闭原则
  • 实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦

模式适用环境

  • 创建新对象成本较大,新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量稍作修改
  • 系统要保存对象的状态,而对象的状态变化很小
  • 需要避免使用分层次的工厂类来创建分层次的对象
  • Ctrl + C -> Ctrl + V

习题

A D C

单例模式

如何保证一个类只有一个实例并且这个实例易于被访问?

  • 全局变量:可以确保对象随时都可以被访问,但不能防止创建多个对象
  • 让类自身负责创建和保存它的唯一实例,并保证不能创建其他实例,它还提供一个访问该实例的方法

单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

  • 某个类只能有一个实例
  • 必须自行创建这个实例
  • 必须自行向整个系统提供这个实例

  • 对于 Singleton(单例),在单例类的内部创建它的唯一实例,并通过静态方法 getInstance() 让客户端可以使用它的唯一实例
  • 为了防止在外部对单例类实例化,**将其构造函数的可见性设为 private **;
  • 在单例类内部定义了一个 Singleton 类型的静态对象作为供外部共享访问的唯一实例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private class Singleton {
    private static Singleton instance = null; // 静态私有变量

    private Singleton {} // 私有构造函数,无法通过 new 来实例化

    // 静态公有方法,返回唯一实例
    public static Singleton getInstance() {
    if(instance == null) {
    instance = new Singleton();
    }

    return instance;
    }
    }

实例

某软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高了系统的整体处理能力,缩短了响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键,试使用单例模式设计服务器负载均衡器。

在图中将负载均衡器 LoadBalancer 设计为单例类,其中包含一个存储服务器信息的集合 serverList,每次在 serverList 中随机选择一台服务器来响应客户端的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// LoadBalancer:负载均衡类
public class LoadBalancer {
// 私有静态成员变量,存储唯一实例
private static LoadBalancer instance = null;
// 服务器集合
private List serverList = null;

// 私有构造函数
private LoadBalancer() {
serverList = new ArrayList();
}

// 公有静态成员方法,返回唯一实例
public static LoadBalancer getLoadBalancer() {
if (instance == null) {
instance = new LoadBalancer();
}

return instance;
}

// 增加服务器
public void addServer(String server) {
ServerList.add(server);
}

// 删除服务器
public void deleteServer(String server) {
serverList.remove(server);
}

// 使用 Random 类随机获取服务器
public String getServer() {
Random random = new Random();
int i = random.nextInt(serverList.size());
return (String)serverList.get(i);
}
}

// Client:客户端测试类
public class Client {
public static void main(String args[]) {
// 创建4个 LoadBalancer 对象
LoadBalancer balancer1, balancer2, balancer3, balancer4;
balancer1 = LoadBalancer.getLoadBalancer();
balancer2 = LoadBalancer.getLoadBalancer();
balancer3 = LoadBalancer.getLoadBalancer();
balancer4 = LoadBalancer.getLoadBalancer();

// 判断服务器均衡负载器是否相同
if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4) {
System.out.println("服务器均衡负载器具有唯一性!");
}

// 增加服务器
balancer1.addServer("Server 1");
balancer1.addServer("Server 2");
balancer1.addServer("Server 3");
balancer1.addServer("Server 4");

// 模拟客户端请求的分发
for(int i = 0; i < 10; i++) {
String server = balancer1.getServer();
System.out.println("分发请求至服务器:" + server);
}
}
}

饿汉式单例与懒汉式单例

饿汉式单例类(Eager Singleton):

1
2
3
4
5
6
7
8
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton();

public static EagerSingleton getInstance() {
return instance;
}
}

当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。

懒汉式单例类(Lazy Singleton):构造函数也是私有的,但被加载时不会将自己实例化,而在第一次调用getInstance()方法时实例化,在类自行加载时并不自行实例化,称为**延迟加载(Lazy Load)**技术,即需要时再加载实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazySingleton() {
private volatile static LazySingleton instance = null;

private LazySingleton() {}

public static LazySingleton getInstance() {
// 双重检查锁定
if (instance == null) {
// 锁定代码段,防止多个线程同时访问
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}

return instance;
}
}
  • 饿汉式单例类:无须考虑多个线程同时访问的问题;调用速度和反应时间优于懒汉式单例;资源利用效率不及懒汉式单例;系统加载时间可能会比较长

  • 懒汉式单例类:实现了延迟加载;必须处理好多个线程同时访问的问题;需通过双重检查锁定等机制进行控制,将导致系统性能受到一定影响

  • 使用静态内部类

  • 使用枚举类

优缺点与适用环境

模式优点

  • 提供了对唯一实例的受控访问
  • 可以节约系统资源,提高系统的性能
  • 允许可变数目的实例(多例类

模式缺点

  • 扩展困难(缺少抽象层)
  • 单例类的职责过重
  • 由于自动垃圾回收机制,可能会导致共享的单例对象的状态丢失

适用环境

  • 系统只需要一个实例对象,或者因为资源消耗太大而只允许创建一个对象
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例

习题

B B B

适配器模式

结构型模式概述

  • 结构型模式(Structural Pattern)关注如何将现有类或对象组织在一起形成更加强大的结构

  • 不同的结构型模式从不同的角度组合类或对象,它们在尽可能满足各种面向对象设计原则的同时为类或对象的组合提供一系列巧妙的解决方案

  • 类结构型模式

    • 关心类的组合,由多个类组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系
  • 对象结构型模式

    • 关心类与对象的组合,通过关联关系,在一个类中定义另一个类的实例对象,然后通过该对象调用相应的方法
  • 合成复用原则

    • 大部分结构型模式是对象结构型模式

适配器模式概述

适配器模式:将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作

  • 别名为包装器(Wrapper)模式
  • 定义中所提及的接口是指广义的接口,它可以表示一个方法或者方法的集合

  • Target(目标抽象类):定义客户所需的接口,可以是一个抽象类或接口,也可以是具体类
  • Adapter(适配器类):可以调用另一个接口,作为一个转换器,对 Adaptee 和 Target 进行适配
  • Adaptee(适配者类):被适配的角色,定义了一个已经存在的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 类适配器
public class Adapter extends Adaptee implements Target {
public void request() {
super.specificRequest();
}
}

// 对象适配器
public class Adapter extends Target {
// 维持一个对适配者对象的引用
private Adaptee adaptee;

public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

public void request() {
adaptee.specificRequest(); // 转发调用
}
}

实例

某公司欲开发一款儿童玩具汽车,为了更好地吸引小朋友的注意力,该玩具汽车在移动过程中伴随着灯光闪烁和声音提示。在该公司以往的产品中已经实现了控制灯光闪烁(例如警灯闪烁)和声音提示(例如警笛音效)的程序,为了重用先前的代码并且使得汽车控制软件具有更好的灵活性和扩展性,现使用适配器模式设计该玩具汽车控制软件。

缺省适配器

  • ServiceInterface(适配者接口)
  • AbstractServiceClass(缺省适配器类):使用空方法的形式实现了在 ServiceInterface 接口中声明的方法。通常将它定义为抽象类,因为对它进行实例化没有任何意义。
  • ConcreteServiceClass(具体业务类):缺省适配器类的子类,在没有引入适配器前它需要实现适配者接口,再有了缺省适配器之后可以直接继承该适配器类

缺省适配器类典型代码片段:

1
2
3
public abstract class AbstractServiceClass implements ServiceInterface {
public void serviceMethod() {} // 空方法
}

双向适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Adapter implements Target, Adaptee { 
private Target target;
private Adaptee adaptee;

public Adapter(Target target) {
this.target = target;
}

public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

public void request() {
adaptee.specificRequest();
}

public void specificRequest() {
target.request();
}
}

优缺点与适配环境

模式优点

  • 目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构
  • 增加了类的透明性和复用性,提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用
  • 灵活性和扩展性非常好
  • 类适配器模式:置换一些适配者的方法很方便
  • 对象适配器模式:可以把多个不同的适配者适配到同一个目标,还可以适配一个适配者的子类

模式缺点

  • 类适配器模式:
    1. 一次最多只能适配一个适配者类,不能同时适配多个适配者;
    2. 适配者类不能为最终类;
    3. 目标抽象类只能为接口,不能为类
  • 对象适配器模式:在适配器中置换适配者类的某些方法比较麻烦

习题

B C(Java不支持多继承)A A

桥接模式

概述

桥接模式:将抽象部分与它的实现部分解耦,使得两者都能够独立变化。

  • 对象结构型模式
  • 又被称为**柄体(Handle and Body)模式或接口(Interface)**模式
  • 抽象关联取代了传统的多层继承
  • 将类之间的静态继承关系转换为动态的对象组合关系

结构与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 实现类接口
public interface Implementor {
public void operationImpl();
}

// 具体实现类
public class ConcreteImplementor implements Implementor {
public void operationImpl() {
// 具体业务方法的实现
}
}

// 抽象类
public abstract class Abstraction {
protected Implementor impl; // 定义实现类接口对象

public void setImpl(Implementor impl) {
this.impl = impl;
}

public abstract void operation(); // 声明抽象业务方法
}

// 扩充抽象类(细化抽象类)
public class RefinedAbstraction extends Abstraction {
public void operation() {
// 业务代码
impl.operationImpl(); // 调用实现类的方法
// 业务代码
}
}

实例

某软件公司要开发一个跨平台图像浏览系统,要求该系统能够显示BMP、JPG、GIF、PNG等多种格式的文件,并且能够在Windows、Linux、UNIX等多个操作系统上运行。系统首先将各种格式的文件解析为像素矩阵(Matrix),然后将像素矩阵显示在屏幕上,在不同的操作系统中可以调用不同的绘制函数来绘制像素矩阵。另外,系统需具有较好的扩展性,以便在将来支持新的文件格式和操作系统。试使用桥接模式设计该跨平台图像浏览系统。

桥接模式与适配器模式的联用

  • 桥接模式:用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化
  • 适配器模式:当发现系统与已有类无法协同工作时

优缺点与适用环境

模式优点

  • 分离抽象接口及其实现部分
  • 可以取代多层继承方案,极大地减少了子类的个数
  • 提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,不需要修改原有系统,符合开闭原则

模式缺点

  • 增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程
  • 正确识别出系统中两个独立变化的维度并不是一件容易的事情

模式适用环境

  • 需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系
  • 抽象部分和实现部分可以以继承的方式独立扩展而互不影响
  • 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立地进行扩展
  • 不希望使用继承因为多层继承导致系统类的个数急剧增加的系统

习题

  1. B D
  2. C
  3. C

组合模式

概述

组合模式:组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。

  • 又称为“部分-整体”(Part-Whole)模式
  • 将对象组织到树形结构中,可以用来描述整体与部分的关系

  • Component(抽象构件):可以是接口或抽象类,为叶子构件和容器构件对象声明接口
  • Leaf(叶子构件)
  • Composite(容器构件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 抽象构件
public abstract class Component {
public abstract void add(Component c); //增加成员
public abstract void remove(Component c); //删除成员
public abstract Component getChild(int i); //获取成员
public abstract void operation(); //业务方法
}

// 叶子构件
public class Leaf extends Component {
public void add(Component c) {
//异常处理或错误提示
}

public void remove(Component c) {
//异常处理或错误提示
}

public Component getChild(int i) {
//异常处理或错误提示
return null;
}

public void operation() {
//叶子构件具体业务方法的实现
}
}

// 容器构件
public class Composite extends Component {
private ArrayList<Component> list = new ArrayList<Component>();

public void add(Component c) {
list.add(c);
}

public void remove(Component c) {
list.remove(c);
}

public Component getChild(int i) {
return (Component)list.get(i);
}

public void operation() {
//容器构件具体业务方法的实现,将递归调用成员构件的业务方法
for(Object obj:list) {
((Component)obj).operation();
}
}
}

实例

某软件公司欲开发一个杀毒(Antivirus)软件,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒。该杀毒软件还可以根据各类文件的特点,为不同类型的文件提供不同的杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差异。现使用组合模式来设计该杀毒软件的整体框架。

结果及分析

  • 如果需要更换操作节点,例如只对文件夹“文本文件”进行杀毒,客户端代码只需修改一行即可,例如将代码folder1.killVirus()改为folder3.killVirus()
  • 在具体实现时,可以创建图形化界面让用户来选择所需操作的根节点,无须修改源代码,符合开闭原则

透明安全&安全组合模式

安全组合模式

  • 抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法
  • 对于叶子对象,客户端不可能调用到这些方法
  • 缺点是不够透明,客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件

优缺点与适用环境

模式优点

  • 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,让客户端忽略了层次的差异,方便对整个层次结构进行控制
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
  • 增加新的容器构件和叶子构件都很方便,符合开闭原则
  • 树形结构的面向对象实现提供了一种灵活的解决方案

模式缺点

  • 在增加新构件时很难对容器中的构件类型进行限制

模式适用环境

  • 具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们
  • 在一个使用面向对象语言开发的系统中需要处理一个树形结构
  • 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型

习题

  1. B
  2. B
  3. C

装饰模式——自学

  • 可以在不改变一个对象本身功能的基础上给对象增加额外的新行为
  • 是一种用于替代继承的技术,它通过一种无须定义子类的方式给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系
  • 引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩展原有类的功能

装饰模式:动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案

  • 以对客户透明的方式动态地给一个对象附加上更多的责任
  • 可以在不需要创建更多子类的情况下,让对象的功能得以扩展

装饰模式的结构

  • Component(抽象构件)
  • ConcreteComponent(具体构件)
  • Decorator(抽象装饰类)
  • ConcreteDecorator(具体装饰类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 抽象构建类
public abstract class Component {
public abstract void operation();
}

// 具体构件类
public class ConcreteComponent extends Component {
public void operation() {
//实现基本功能
}
}

// 抽象装饰类
public class Decorator extends Component {
private Component component; //维持一个对抽象构件对象的引用

//注入一个抽象构件类型的对象
public Decorator(Component component) {
this.component=component;
}

public void operation() {
component.operation(); //调用原有业务方法
}
}

// 具体装饰类
public class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component) {
super(component);
}

public void operation() {
super.operation(); //调用原有业务方法
addedBehavior(); //调用新增业务方法
}

//新增业务方法
public void addedBehavior() {
// ……
}
}

实例

某软件公司基于面向对象技术开发了一套图形界面构件库——VisualComponent,该构件库提供了大量基本构件,如窗体、文本框、列表框等,由于在使用该构件库时,用户经常要求定制一些特殊的显示效果,如带滚动条的窗体、带黑色边框的文本框、既带滚动条又带黑色边框的列表框等等,因此经常需要对该构件库进行扩展以增强其功能。
现使用装饰模式来设计该图形界面构件库。

透明/半透明装饰模式

透明装饰模式

  • 透明(Transparent)装饰模式:要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型
  • 对于客户端而言,具体构件对象和具体装饰对象没有任何区别
  • 可以让客户端透明地使用装饰之前的对象和装饰之后的对象,无须关心它们的区别
  • 可以对一个已装饰过的对象进行多次装饰,得到更为复杂、功能更为强大的对象
  • 无法在客户端单独调用新增方法addedBehavior()

半透明装饰模式

  • 半透明(Semi-transparent)装饰模式:用具体装饰类型来定义装饰之后的对象,而具体构件使用抽象构件类型来定义
  • 对于客户端而言,具体构件类型无须关心,是透明的;但是具体装饰类型必须指定,这是不透明的
  • 可以给系统带来更多的灵活性,设计相对简单,使用起来也非常方便
  • 客户端使用具体装饰类型来定义装饰后的对象,因此可以单独调用addedBehavior()方法
  • 最大的缺点在于不能实现对同一个对象的多次装饰,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象

优缺点与适用环境

模式优点

  • 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为
  • 可以对一个对象进行多次装饰
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,且原有类库代码无须改变,符合开闭原则

模式缺点

  • 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,在一定程度上影响程序的性能
  • 比继承更加易于出错,排错也更困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐

模式适用环境

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  • 不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式

思考

半透明装饰模式能否实现对同一个对象的多次装饰?为什么?

是可以的。半透明装饰模式允许对同一个对象应用多个装饰器,每个装饰器可以添加不同的职责或行为。装饰器本身在结构上通常是一个类,它包含了对原始对象的引用,并且提供了额外的行为。你可以将一个装饰器包装在另一个装饰器之上,每个装饰器都可以在不改变对象接口的前提下,为对象添加新的功能。
但是,由于半透明装饰模式中装饰器可能会引入新的方法,如果你多次装饰一个对象,可能会导致最终的对象类型变得复杂,客户端需要了解所有装饰器提供的额外方法才能充分利用。这也意味着客户端和具体的装饰器类之间的耦合度增加,这违背了面向对象设计原则中推荐的“对扩展开放,对修改封闭”的原则。
总结来说,半透明装饰模式能够实现对同一个对象的多次装饰,每次装饰可以增加新的行为或职责,但这种方式可能会增加系统的复杂性和耦合度。

外观模式

概述

分析

  • 一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出现
  • 引入一个新的外观类(Facade)来负责和多个业务类【子系统(Subsystem)】进行交互,而客户类只需与外观类交互
  • 为复杂子系统提供一个简单的访问入口:为多个业务类的调用提供了一个统一的入口,简化了类与类之间的交互

外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

  • 又称为门面模式
  • 是迪米特法则的一种具体实现
  • 通过引入一个新的外观角色降低原有系统的复杂度,同时降低客户类与子系统的耦合度
  • 所指的子系统是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统

结构与实现

  • Facade(外观角色)
  • SubSystem(子系统角色)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 子系统类
public class SubSystemA {
public void methodA() {
//业务实现代码
}
}

public class SubSystemB {
public void methodB() {
//业务实现代码
}
}
public class SubSystemC {
public void methodC() {
//业务实现代码
}
}

// 外观类
public class Facade {
private SubSystemA obj1 = new SubSystemA();
private SubSystemB obj2 = new SubSystemB();
private SubSystemC obj3 = new SubSystemC();

public void method() {
obj1.method();
obj2.method();
obj3.method();
}
}

// 客户端
public class Client {
public static void main(String args[]) {
Facade facade = new Facade();
facade.method();
}
}

实例

某软件公司要开发一个可应用于多个软件的文件加密模块,该模块可以对文件中的数据进行加密并将加密之后的数据存储在一个新文件中,具体的流程包括3个部分,分别是读取源文件、加密、保存加密之后的文件,其中,读取文件和保存文件使用流来实现,加密操作通过求模运算实现。这3个操作相对独立,为了实现代码的独立重用,让设计更符合单一职责原则,这3个操作的业务代码封装在3个不同的类中。
现使用外观模式设计该文件加密模块。

抽象外观类

动机:

  • 在标准的外观模式的结构图中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码,这将违背开闭原则
  • 可以通过引入抽象外观类对系统进行改进,在一定程度上解决该问题。在引人抽象外观类之后,客户端可以针对抽象外观类进行编程
    • 对于新的业务需求,不需要修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象
    • 同时通过修改配置文件来达到不修改任何源代码并更换外观类的目的。

外观模式与单例模式联用

优缺点与适用环境

模式优点

  • 对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易
  • 实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可
  • 一个子系统的修改对其他子系统没有任何影响,而且子系统的内部变化也不会影响到外观对象

模式缺点

  • 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性
  • 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则

模式适用环境

  • 要为访问一系列复杂的子系统提供一个简单入口
  • 客户端程序与多个子系统之间存在很大的依赖性
  • 在层次化结构中,可以使用外观模式的定义系统中每一层的入口,层与层之间不直接产生联系,而是通过外观类建立联系,降低层之间的耦合度

代理

概述

代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

  • 引入一个新的代理对象
  • 代理对象在客户端对象和目标对象之间起到中介的作用
  • 去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务

结构与实现

  • Subject(抽象主题角色):声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题
  • Proxy(代理主题角色):包含了对真实主题的引用,从而可以在任何时候操作真实对象
  • RealSubject(真实主题角色):定义了代理角色所代表的真实对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 抽象主题类
public abstract class Subject {
public abstract void request();
}

// 真实主题类
public class RealSubject extends Subject{
public void request() {
//业务方法具体实现代码
}
}

// 代理类
public class Proxy extends Subject {
//维持一个对真实主题对象的引用 
private RealSubject realSubject = new RealSubject();
public void preRequest() {
// ……
}

public void request() {
preRequest();
realSubject.request(); //调用真实主题对象的方法
postRequest();
}

public void postRequest() {
// ……
}
}

几种常见的代理模式

  • 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以在同一台主机中,也可以在另一台主机中,远程代理又称为大使(Ambassador)
  • 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建
  • 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限
  • 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
  • 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等

实例

某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下:

  1. 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统;
  2. 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。
    该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。
    现使用代理模式设计并实现该收费商务信息查询系统。

结果及分析

  • 保护代理智能引用代理
  • 在代理类ProxySearcher中实现对真实主题类的权限控制引用计数

代理方法

远程代理

动机

  • 客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速地响应并处理客户端的请求
  • 可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在
  • 客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用

虚拟代理

动机

  • 对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理
  • 在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象
  • 使用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能

Java 动态代理

  • 动态代理(Dynamic Proxy)可以让系统在运行时根据实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实主题类而且可以代理不同的方法
  • Java语言提供了对动态代理的支持,Java语言实现动态代理时需要用到位于java.lang.reflect包中的一些类

优缺点与适用环境

  • 模式优点
    • 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
    • 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性
  • 模式优点——逐个分析
    • 远程代理:可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高了系统的整体运行效率
    • 虚拟代理:通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
    • 缓冲代理:为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间
    • 保护代理:可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限

模式缺点

  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢(例如保护代理)
  • 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂(例如远程代理)

模式适用环境

  • 当客户端对象需要访问远程主机中的对象时可以使用远程代理
  • 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理
  • 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理
  • 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理
  • 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理

习题

  1. A
  2. B
  3. D

职责链模式

行为型模式概述

  • 在软件系统运行时对象并不是孤立存在的,它们可以通过相互通信协作完成某些功能,一个对象在运行时也将影响到其他对象的运行。

  • 行为型模式(Behavioral Pattern)关注系统中对象之间的交互,研究系统在运行时对象之间的相互通信与协作,进一步明确对象的职责

  • 行为型模式:不仅仅关注类和对象本身,还重点关注它们之间的相互作用和职责划分

  • 类行为型模式

    • 使用继承关系在几个类之间分配行为,主要通过多态等方式来分配父类与子类的职责
  • 对象行为型模式

    • 使用对象的关联关系来分配行为,主要通过对象关联等方式来分配两个或多个类的职责

职责链模式概述

职责链模式:避免将一个请求的发送者与接收者耦合在一起,让多个对象都有机会处理请求。将接收请求的对象连接成一条链,并且沿着这条链传递请求,直到有一个对象能够处理它为止。

  • 请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理
  • 客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上,将请求的发送者和请求的处理者解耦

结构与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 抽象处理者
public abstract class Handler {
//维持对下家的引用
protected Handler successor;

public void setSuccessor(Handler successor) {
this.successor=successor;
}

public abstract void handleRequest(String request);
}

// 具体处理者
public class ConcreteHandler extends Handler {
public void handleRequest(String request) {
if (请求满足条件) {
//处理请求
} else {
this.successor.handleRequest(request); //转发请求
}
}
}

// 客户端代码
……
Handler handler1, handler2, handler3;
handler1 = new ConcreteHandlerA();
handler2 = new ConcreteHandlerB();
handler3 = new ConcreteHandlerC();
//创建职责链
handler1.setSuccessor(handler2);
handler2.setSuccessor(handler3);
//发送请求,请求对象通常为自定义类型
handler1.handleRequest("请求对象"); …

实例

某企业的SCM(Supply Chain Management,供应链管理)系统中包含一个采购审批子系统。该企业的采购审批是分级进行的,即根据采购金额的不同由不同层次的主管人员来审批,主任可以审批5万元以下(不包括5万元)的采购单,副董事长可以审批5万元至10万元(不包括10万元)的采购单,董事长可以审批10万元至50万元(不包括50万元)的采购单,50万元及以上的采购单就需要开董事会讨论决定。
现使用职责链模式设计并实现该系统。

纯与不纯的职责链模式

纯的职责链模式

  • 一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家
  • 不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况
  • 一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况

不纯的职责链模式

  • 允许某个请求被一个具体处理者部分处理后向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求
  • 一个请求可以最终不被任何处理者对象所接收并处理

优缺点与适用场景

模式优点

  • 使得一个对象无须知道是其他哪一个对象处理其请求,降低了系统的耦合度
  • 简化对象之间的相互连接
  • 给对象职责的分配带来更多的灵活性
  • 增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可

模式缺点

  • 不能保证请求一定会被处理
  • 对于比较长的职责链,系统性能将受到一定影响,在进行代码调试时不太方便
  • 如果建链不当,可能会造成循环调用,将导致系统陷入死循环

模式适用环境

  • 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定
  • 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
  • 可动态指定一组对象处理请求

习题

  1. B
  2. A

命令模式

概述

动机

  • 将请求发送者和接收者完全解耦
  • 发送者与接收者之间没有直接引用关系
  • 发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求

命令模式:将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化对请求排队或者记录请求日志,以及支持可撤销的操作

  • 别名为动作(Action)模式或事务(Transaction)模式
  • “用不同的请求对客户进行参数化”
  • “对请求排队”
  • “记录请求日志”
  • “支持可撤销操作”

结构

  • 命令模式的本质是对请求进行封装
  • 一个请求对应于一个命令,将发出命令的责任和执行命令的责任分开
  • 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 抽象命令类
public abstract class Command {
public abstract void execute();
}

// 调用者(请求发送者类)
public class Invoker {
private Command command;

// 构造注入
public Invoker(Command command) {
this.command = command;
}

// 设值注入
public void setCommand(Command command) {
this.command = command;
}

// 业务方法,用于调用命令类的execute()方法
public void call() {
command.execute();
}
}

// 具体命令类
public class ConcreteCommand extends Command {
private Receiver receiver; //维持一个对请求接收者对象的引用

public void execute() {
receiver.action(); //调用请求接收者的业务处理方法action()
}
}

// 请求接收者类
public class Receiver {
public void action() {
//具体操作
}
}

实例

为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,例如功能键FunctionButton可以用于退出系统(由SystemExitClass类来实现),也可以用于显示帮助文档( 由DisplayHelpClass类来实现)。
用户可以通过修改配置文件来改变功能键的用途,现使用命令模式来设计该系统,使得功能键类与功能类之间解耦,可为同一个功能键设置不同的功能。

结果及分析

  • 如果需要更换具体命令类,无须修改源代码,只需修改配置文件,完全符合开闭原则
  • 每一个具体命令类对应一个请求的处理者(接收者),通过向请求发送者注入不同的具体命令对象可以使相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者

记录请求日志

  • 将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中
    • 为系统提供一种恢复机制
    • 可以用于实现批处理
    • 防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行
  • 将发送请求的命令对象通过序列化写到日志文件中
  • 命令类必须实现接口Serializable

宏命令

  • 宏命令(Macro Command)又称为**组合命令(Composite Command)**,它是组合模式和命令模式联用的产物
  • 宏命令是一个具体命令类,它拥有一个集合,在该集合中包含了对其他命令对象的引用
  • 当调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法。一个宏命令的成员可以是简单命令,还可以继续是宏命令
  • 执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理

优缺点与适用环境

模式优点

  • 降低系统的耦合度
  • 新的命令可以很容易地加入到系统中,符合开闭原则
  • 可以比较容易地设计一个命令队列或宏命令(组合命令)
  • 为请求的**撤销(Undo)和恢复(Redo)**操作提供了一种设计和实现方案

模式缺点

  • 使用命令模式可能会导致某些系统有过多的具体命令类(针对每一个对请求接收者的调用操作都需要设计一个具体命令类)

模式适用环境

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
  • 系统需要在不同的时间指定请求、将请求排队和执行请求
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
  • 系统需要将一组操作组合在一起形成宏命令

习题

  1. D
  2. C

观察者模式

概述

  • 软件系统:一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变,它们之间将产生联动
  • 观察者模式:
    • 定义了对象之间一种一对多的依赖关系,让一个对象的改变能够影响其他对象
    • 发生改变的对象称为观察目标,被通知的对象称为观察者
    • 一个观察目标可以对应多个观察者

观察者模式:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都得到通知并被自动更新

  • 发布-订阅(Publish/Subscribe)模式
  • 模型-视图(Model/View)模式
  • 源-监听器(Source/Listener)模式
  • 从属者(Dependents)模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 抽象目标类
public abstract class Subject {
//定义一个观察者集合用于存储所有观察者对象
protected ArrayList observers<Observer> = new ArrayList();

//注册方法,用于向观察者集合中增加一个观察者
public void attach(Observer observer) {
observers.add(observer);
}

//注销方法,用于在观察者集合中删除一个观察者
public void detach(Observer observer) {
observers.remove(observer);
}

//声明抽象通知方法
public abstract void notify();
}

// 具体目标类
public class ConcreteSubject extends Subject {
//实现通知方法
public void notify() {
//遍历观察者集合,调用每一个观察者的响应方法
for(Object obs:observers) {
((Observer)obs).update();
}
}
}

// 抽象观察者
public interface Observer {
//声明响应方法
public void update();
}

// 具体观察者
public class ConcreteObserver implements Observer {
//实现响应方法
public void update() {
//具体响应代码
}
}

// 客户端代码片段
……
Subject subject = new ConcreteSubject();
Observer observer = new ConcreteObserver();
subject.attach(observer); //注册观察者
subject.notify();//调用在其观察者集合中注册的观察者对象update()方法
……
  • 有时候在具体观察者类ConcreteObserver中需要使用到具体目标类ConcreteSubject中的状态(属性),会存在关联或依赖关系
  • 如果在具体层之间具有关联关系,系统的扩展性将受到一定的影响,增加新的具体目标类有时候需要修改原有观察者的代码,在一定程度上违背了开闭原则

实例

在某多人联机对战游戏中,多个玩家可以加入同一战队组成联盟,当战队中的某一成员受到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将做出响应。
现使用观察者模式设计并实现该过程,以实现战队成员之间的联动。

观察者模式与Java事件处理

  • 事件源对象充当观察目标角色
  • 事件监听器充当抽象观察者角色
  • 事件处理对象充当具体观察者角色

观察者模式与MVC

MVC(Model-View-Controller)架构

  • 模型(Model),视图(View)和控制器(Controller)
  • 模型可对应于观察者模式中的观察目标,而视图对应于观察者,控制器可充当两者之间的中介者
  • 当模型层的数据发生改变时,视图层将自动改变其显示内容

优缺点与适用环境

模式优点

  • 可以实现表示层和数据逻辑层的分离
  • 在观察目标和观察者之间建立一个抽象的耦合
  • 支持广播通信,简化了一对多系统设计的难度
  • 符合开闭原则,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便

模式缺点

  • 将所有的观察者都通知到会花费很多时间
  • 如果存在循环依赖时可能导致系统崩溃
  • 没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而只是知道观察目标发生了变化

模式适用环境

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用
  • 一个对象的改变将导致一个或多个其他对象发生改变,且并不知道具体有多少对象将发生改变,也不知道这些对象是谁
  • 需要在系统中创建一个触发链

习题

  1. D
  2. A
  3. C

策略模式

概述

  • 实现某个目标的途径不止一条,可根据实际情况选择一条合适的途径
  • 软件开发:
    • 多种算法,例如排序、查找、打折等
    • 使用**硬编码(Hard Coding)**实现将导致系统违背开闭原则,扩展性差,且维护困难
    • 可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法->策略类

策略模式:定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法可以独立于使用它的客户变化。

  • 又称为政策(Policy)模式
  • 每一个封装算法的类称之为策略(Strategy)类
  • 策略模式提供了一种可插入式(Pluggable)算法的实现方案

结构与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 抽象策略类
public abstract class Strategy {
public abstract void algorithm(); //声明抽象算法
}

// 具体策略类
public class ConcreteStrategyA extends Strategy {
//算法的具体实现
public void algorithm() {
//算法A
}
}

// 环境类
public class Context {
private Strategy strategy; //维持一个对抽象策略类的引用

//注入策略对象
public void setStrategy(Strategy strategy) {
this.strategy= strategy;
}

//调用策略类中的算法
public void algorithm() {
strategy.algorithm();
}
}

// 客户端
……
Context context = new Context();
Strategy strategy;
strategy = new ConcreteStrategyA(); //可在运行时指定类型,通过配置
文件和反射机制实现
context.setStrategy(strategy);
context.algorithm();
……

实例

某软件公司为某电影院开发了一套影院售票系统,在该系统中需要为
不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

  1. 学生凭学生证可享受票价8折优惠。
  2. 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元)。
  3. 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。
    该系统在将来可能还要根据需要引入新的打折方式。现使用策略模式
    设计该影院售票系统的打折方案。

结果及分析

  • 如果需要更换具体策略类,无须修改源代码,只需修改配置文件即可,完全符合开闭原则
  • 如果需要增加新的打折方式,原有代码均无须修改,只要增加一个新的折扣类作为抽象折扣类的子类,实现在抽象折扣类中声明的打折方法,然后修改配置文件,将原有具体折扣类的类名改为新增折扣类的类名即可,完全符合开闭原则。

优缺点与适用环境

模式优点

  • 提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为
  • 提供了管理相关的算法族的办法
  • 提供了一种可以替换继承关系的办法
  • 可以避免多重条件选择语句
  • 提供了一种算法的复用机制,不同的环境类可以方便地复用策略类

模式缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
  • 将造成系统产生很多具体策略类
  • 无法同时在客户端使用多个策略类

模式适用环境

  • 一个系统需要动态地在几种算法中选择一种
  • 避免使用难以维护的多重条件选择语句
  • 不希望客户端知道复杂的、与算法相关的数据结构,提高算法的保密性与安全性

习题

  1. B
  2. B
  3. A

模板方法模式

概述

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤

  • 是一种基于继承的代码复用技术(类行为型模式)
  • 最简单的行为型设计模式:只存在父类与子类之间的继承关系。
  • 通过使用模板方法模式可以将一些复杂流程的实现步骤封装在一系列基本方法
  • 在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果

结构与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 抽象类
public abstract class AbstractClass {
//模板方法
public void templateMethod() {
primitiveOperation1();
primitiveOperation2();
primitiveOperation3();
}

//基本方法—具体方法
public void primitiveOperation1() {
//实现代码
}

//基本方法—抽象方法
public abstract void primitiveOperation2();

//基本方法—钩子方法
public void primitiveOperation3()
{ }
}

// 具体子类
public class ConcreteClass extends AbstractClass {
public void primitiveOperation2() {
//实现代码
}

public void primitiveOperation3() {
//实现代码
}
}

实例

某软件公司要为某银行的业务支撑系统开发一个利息计算模块,利息的计算流程如下:

  1. 系统根据账号和密码验证用户信息,如果用户信息错误,则系统显示出错提示。
  2. 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账户和定期账户具有不同的利息计算公式)。
  3. 系统显示利息。
    现使用模板方法模式设计该利息计算模块。

优缺点与适用环境

模式优点

  • 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序
  • 提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为
  • 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行
  • 更换和增加新的子类很方便,符合单一职责原则和开闭原则

模式缺点

  • 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统会更加庞大,设计也更加抽象(可结合桥接模式)

模式适用环境

  • 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现
  • 各子类中公共的行为应被提取出来,并集中到一个公共父类中,以避免代码重复
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制

习题

  1. C
  2. D