面向对象分析与设计(Grady Booch 等著)

第1部分 概念

第1章 复杂性(已看)

第2章 对象模型(已看)

第3章 类与对象(已看)

第4章 分类

第2部分 方法

第5章 表示法

第6章 过程

第7章 实战

第3部分 应用

第8章 系统架构----基于卫星的导航(聚焦于系统架构)

第9章 控制系统----交通管理(聚焦于系统需求)

第10章 人工智能----密码分析(聚焦于分析)

第11章 数据采集----气象监测站(聚焦于分析到初步的设计)

第12章 Web应用----休假跟踪系统(聚焦于详细设计和实现)

附录A 面向对象编程语言

附录B 进一步阅读

注解

术语表

分类书目

 

第1章 复杂性

由于我们不能控制软件的复杂性,所以导致了项目延迟,超出预算以及在声称的需求中存在缺陷。我们常常把这种情况称为软件危机 

  1.1 复杂系统的结构

由于根本问题来自于软件固有的复杂性,所以我们建议先研究其他学科中复杂系统是如何组织的

    1.1.1 个人计算机的结构

从个人计算机中我们看到了复杂系统的层次化特征,个人计算机能正常发挥功能是因为它的每个主要部件之间协同工作。这些分离的部件形成一个逻辑整体。实际上,我们之所以能够理解计算机的工作方式,是因为可以将它分解为能够独立研究的部件。因此,我们可以独立地研究显示器的操作和硬盘驱动器的操作。类似地,我们可以在不考虑主存储器子系统的情况下研究ALU

复杂系统不仅仅是层次化的,而且这种层次化也代表了不同的抽象级别,一层构建于另一层之上,每一层都可以分开来理解。在每一个抽象层都可以发现有一组设备协作,为更高的抽象层提供服务。可选择某个抽象层来满足特定的需求。例如,如果我们追踪主存储器中的一个时钟问题,可能会查看计算机的逻辑门级架构,但是如果我们要找的是电子表格应用中的一个问题的根源,这个抽象层就不合适了 

    1.1.2 植物和动物的结构

同一层抽象中的所有部分之间以某种定义良好的方式进行交互

在给定层的内外之间总有清晰的边界

不同抽象层的不同部分之间,存在着清晰的分离关注 

    1.1.3 物质的结构

原子,电子,夸克 

    1.1.4 社会机构的结构

一群人聚在一起,完成一些个人无法完成的任务 

  1.2 软件固有的复杂性

Brooks曾指处:“爱因斯坦认为自然界必定存在着简单的解释,因为上帝不是反复无常或随心所欲。软件工程师没有这样的信仰来提供安慰。许多必须控制的复杂性是随心所欲的复杂性” 

    1.2.1 定义软件复杂性

工业级软件的特征是,单个开发者要理解其设计的所有方面非常困难,几乎是不可能的。武断地说,这些系统的复杂性超出了人类智能的范围。不幸的是,我们所说的这种复杂性似乎是所有大型软件系统的基本特征。从根本上说,我们可以掌握这种复杂性,但不能消除这种复杂性 

    1.2.2 为什么软件在本质上是复杂的

正如Brooks所指处的是:“软件的复杂性是一个基本特征,而不是偶然如此”。我们认为这种固有的复杂性有4个原因

  1. 问题域的复杂性

无限制的外部复杂性是导致Brooks所说的任意复杂的原因之一

这种外部复杂性通常源自于系统用户和系统开发者之间的“沟通困难”:用户常常发现很难用开发者能够理解的形式对他们的需求给处准确的表达。在某些情况下,用户只是对想要的软件系统有一个模糊的想法。这既不是系统用户的错,也不是系统开发者的错,出现这种情况是因为这些人都缺乏另一个领域的经验。用户和开发者对问题的本质有着不同的看法,并根据解决方案的本质做出了不同的假定

更麻烦的是,软件系统在开发过程中经常发生需求改变,主要是因为软件开发项目本身改变了问题的规则

因为大型的软件系统是一项投资,所以我们不能够忍受在每次需求发生变化时,就抛弃掉原有的系统。不管是否有计划,系统都会随时间的推移而演化,这种情况常常被错误地称为软件维护。准确地说,在我们修正错误时,这是维护;在我们应对改变的需求时,这是演化,当我们使用一些极端的手段来保持古老而陈腐的软件继续工作时,这是保护,不幸的是,事实表明相当一部分软件开发资源被用在了软件保护上

  2. 管理开发过程的困难性

软件开发团队的基本任务就是制造简单的假象----让用户与大量的,通常是任意的外部复杂性隔离开来

对于开发团队来说,主要的管理挑战总是维持设计的一致性和完整性

  3. 软件中随处可能出现的灵活性

一家造房屋的公司通常不会自己经营林场,砍伐树木以获取原木。我们也很少看见一家建筑公司建造一个现场的钢铁厂,为新的建筑提供定制的大梁。但在软件行业,这种情况却经常发生。软件提供了非常大的灵活性,所以开发者几乎有可能表达任何形式的抽象。但是,这种灵活性变成了一种难以置信的诱人的属性,因为它也迫使开发者打造几乎所有的初级构建模块,高层的抽象将建立在这些初级建构模块之上。建筑行业对原材料的品质有着统一的编码和标准,但软件行业却很少有这种标准。结果,软件行业还是一种劳动密集型的产业 

  4. 描述离散系统行为的问题

如果向空中抛出一个球,我们可以肯定地预测出它的路径,因为我们知道在正常的情况下,某些物理定律会起作用。如果因为我们在抛球时用的力大了一些,结果它就在飞行到一半的时候突然停下来,然后直接往上冲,那么我们将感到非常惊奇。但是在一个调试得不太好的模拟球的运动的软件中,类似这样的行为却很容易发生

在大型应用中,可能有成百上千个变量以及多个控制线程。系统中的这些变量,它们当前的值,当前的地址和每个过程的调用栈一起构成了应用当前的状态。因为我们是在数字计算机上执行软件,所以我们的系统具有离散的状态。与此形成对比的是,像抛球运动这样的模拟系统是连续的系统。Parnas指出:“当我们说系统是由连续函数描述的时候,我们是说它不会包含任何隐含的惊奇。输入中的小变化总是会导致输出中相应的小变化”。而在另一方面,离散系统从本质上来说具有有限数量的可能状态。在大的系统中,由于组合的缘故,导致可能状态的数目变得非常大。我们试图以分离关注的方式来设计我们的系统,这样,系统某部分的行为对其他部分行为的影响就能降到最低。但是有一个事实仍未改变,即离散系统中的状态转换不能够用连续函数来建模。软件系统之外的每个事件都有可能让系统进入一个新的状态,而且,状态与状态之间的转换关系并非总是确定的。在最坏的情况下,外部的事件可能会破坏系统的状态,因为它的设计者没有考虑到事件之间的相互作用。如果一艘船的推进系统由于计算溢出而失效,其原因是某人在维护系统中输入了错误的数据(一次真正的事故),我们就理解了这个问题的严重性。在地铁,汽车,卫星,航空交通控制,仓库等系统中,与软件相关的系统故障大量上升。在连续系统中,这类行为不太可能发生,但在离散系统中,所有外部事件都有可能影响系统内部状态的任何部分。当然,这是对系统进行大量测试的主要原因,但除了那些极其微不足道的系统之外,穷尽可能的测试是无法做到的。既然数学工具和我们的智能都不能够对大型离散系统的完整行为进行建模,关于系统的正确性,我们必须满足与可以接受的信心级别

  1.3 复杂系统的5个属性 

    1.3.1 层次结构

在Simon和Ando工作的基础上,Courtois提出:

”复杂性常常以层次结构的形式存在,复杂的系统由一些相关的子系统组成,这些子系统又有自己的系统,如此下去,直到达到某种最低层次的基本组件“

Simon指出,”许多复杂系统都有几乎可分解的层次结构,正是这一事实让我们能够理解,描述甚至‘看到’这样的系统和它们的组成部分”。确实,我们似乎只能理解那些有层次结构的系统

复杂系统的架构是它所有的组件以及这些组件之间的层次结构的函数,认识到这一点很重要。“所有系统都有子系统,所有系统都是更大系统的组成部分......一个系统所提供的价值肯定来自于各个组成部分之间的相互关系,而不是来自于单个的组成部分

    1.3.2 相对基础

关于复杂系统中基础组件的实质,我们的经验表明:

“选择哪些作为系统的基础组件相对来说比较随意,这在很大程度上取决于系统观察者的判断”

对于一个观察者来说很基础的东西,对另一个观察者可能具有很搞的抽象层次 

    1.3.3 分离关注

Simon将层次系统称为“可分解的”,因为它们可以被分成一些可标识的部分,他称它们是“几乎可分解的”,因为这些部分并不是完全独立的。这引出了所有复杂系统的另一个属性:

“组件内的联系通常比组件间的联系更强,这一事实实际上将组件中高频率的动作(涉及组件的内部结构)和低频率的动作(涉及组件间的相互作用)分离开来”

组件内部作用和组件间作用的差异让我们在系统的不同部分之间实现“分离关注”,让我们能够以相对隔离的方式来研究每个部分 

    1.3.4 共同模式

Simon指出:“层次结构通常只是由少数不同类型的子系统按照不同的组合和安排方式构成的”

换言之,复杂系统具有共同的模式。这些模式可能涉及小组件的复用,如细胞,或者大一些的结构如脉管系统,在植物和动物中都存在 

    1.3.5 稳定的中间形式

复杂的系统趋向于随时间而演变。准确地说,“如果存在为稳定的中间形式,从简单系统到复杂系统的演变将更快”。用更夸张的词来说:
“复杂系统毫无例外都是从能工作的简单系统演变而来的......从头设计的复杂系统根本不能工作,也不能通过打补丁的方式使其工作,必须从头开始,从能工作的简单系统开始

随着系统的演变,曾经被认为是复杂的对象就变成了基础对象,在这些对象的基础上构建更复杂的系统,而且,永远也不能够第一次就正确打造出这些基础对象,必须在上下文环境中使用它们,然后随着时间的推移不断地改进它们,因为我们对系统的真实行为了解得越来越多 

  1.4 有组织和无组织的复杂性

发现共同抽象和机制极大地促进了我们对复杂系统的理解 

    1.4.1 复杂系统的规范形式

大部分有趣的系统不只包含单一的层次结构,相反,我们发现同一个复杂系统中通常表现出不同的层次结构,相反,我们发现同一个复杂系统中通常表现出许多不同的层次结构

例如,研究飞行器时我们可以将它分解为推进系统,飞行控制系统等。这种分解代表了结构上或“组成部分(part of)”的层次结构

另外,我们可以按一种完全正交的方式来分解这个系统。例如,涡轮引擎是一种具体的喷气引擎,Pratt and Whitney TF30又是一种具体的涡轮引擎。换言之,喷气引擎代表了所有类型的喷气引擎的共同特性的抽象,涡轮引擎只是一种特殊类型的喷气引擎,它有一些特有的特征,可以和冲压喷气式引擎区分开来

第二种层次结构代表了“是一种(is a)”的层次结构。根据我们的经验,从这两种观点来看系统都很重要,应既研究它的“是一种”层次结构也研究“组成部分”层次结构。我们把这些层次结构分别称为“类结构”和“对象结构”

在这个例子里,当我们提到类结构和对象结构时,并不是指在编写软件时创建的类和对象。我们指的是更高抽象层的类和对象,它们组成了复杂的系统,如喷气引擎,飞机骨架,不同类型的座位,自动导航子系统等

图1-1所示为系统的两个正交的层次结构:它的类结构和它的对象结构。每个层次结构都是分层的,在许多基础类和对象之上构建了许多抽象类和对象。选择哪些类或对象作为基础类和对象,这与要解决的问题有关。深入每个层次,又会看到另一层的复杂型。特别是在对象结构的各个部中,同一抽象层上的对象之间存在着紧密的协作

 

将类结构和对象结构的概念与复杂系统的5种属性(层次结构,相对基础(如多层次的抽象),分离关注,模式和稳定的中间形式)结合起来,我们发现,基本上所有的复杂系统都具有相同的(规范的)形式,如图1-2所示。我们将系统的类结构和对象结构统称为它的“架构 ”

 

没有一种特定的架构是可以真正被认为是“正确的”。这也是使得系统架构具有挑战性的原因----在可能的许多种系统组件结构中,复杂系统的5种属性中以及系统的用户的需求中寻找平衡 

经验表明,最成功的复杂软件系统就是在设计中包含了深思熟虑的类结构和对象结构,并具备了前一节所描述的复杂系统的5种属性

    1.4.2 处理复杂性时人的能力的局限

我们面对的是一个根本难题:要开发的软件系统的复杂性在增加,而我们处理复杂性的能力却有局限。怎样才能解决这个困境?  

  1.5 从混沌到有序

这个世界上天才很少,没有理由相信软件工程师领域拥有大量的天才。尽管我们中间确实有一些天才,但是在工业级软件开发的领域,我们不能够总依赖个人的灵感引导我们前进。因此,必须考虑通过一些训练有素的方式来控制复杂性 

    1.5.1 分解的作用

“控制复杂性的机器我们从远古时代就知道了,即分而治之

      1. 算法分解

      2. 面向对象的分解

      3. 算法分解与面向对象分解

对复杂系统的分解,哪一种是正确的方法----按算法分解还是按对象分解?实际上,这个问题带有欺骗性,因为正确的答案是两种观点都有其各自的重要性。算法的观点强调了事件的顺序,面向对象的观点强调了一些代理,它们要么发出动作,要么是这些操作执行的对象

 

 

面向对象分解极大地降低了构建复杂系统的风险,因为它们的思路是从我们有信心的,较小的系统开始增量式地演进。而且,通过帮助我们明智地决定对巨大的状态空间进行分离关注,面向对象的分解直接关注了软件的内在复杂性

    1.5.2 抽象的作用

我们(人类)已经形成了一种异常强大的技术来对付复杂性,我们对它进行抽象。不能够全面掌握一个复杂的对象,我们就选择忽略它的非本质的细节,转而处理这个对象的一般化的,理想化的模型 

    1.5.3 层次结构的作用

另一种增加单块信息的语义内容的方法,是在复杂的软件系统中显式地组织类和对象层次结构。对象结构很重要,因为它展示了不同的对象之间如何通过一些交互模式进行协作,我们把这些交互模式称为“机制”。类结构也同样重要,因为它强调了系统中的公共结构行为。 

  1.6 复杂系统的设计

每一种工程方法的实践,无论它是土木工程,机械工程,化学工程,电气工程还是软件工程,都涉及科学和艺术两方面的元素。正如Petroski雄辩地指出:“一个新结构的设计概念既包括想象力的跳跃,也包括经验和知识的融合,就像艺术家在他的画布或纸上创作作品一样。当作为艺术家的工程师想出这个设计之后,必须由科学家的工程师采用科学方法进行分析,像其他科学家所使用的方法一样”。类似地,Dijkstra说:“程序设计任务是应用抽象的大规模练习,因此需要正规数学家的能力和有能力的工程师的态度" 

    1.6.1 作为科学和艺术的工程 

    1.6.2 设计的含义

在软件工程中, Mostow指出设计的目的是要构建如下的一个系统:

  • 满足给定的(可能是非正式的)功能规格说明;
  • 符合目标介质的限制;
  • 满足隐含的和明确的性能及资源使用需求;
  • 满足隐含的和明确的关于产品形式方面的设计限制条件
  • 满足对设计过程本身的限制条件,如时间,费用或进行设计可用的工具

Stroustrup指出,“设计的目的是创建一个干净的,相对简单的内部结构,有时候也被称为架构......一份设计是设计过程的最终产物”。设计包括在一组竞争的需求之间进行平衡。设计的产品是一些模型,让我们能够阐明我们的结构,当需求冲突时进行折中,总之,为实现提供了一份蓝图 

      1. 建模的重要性

建模在所有工程实践中都已得到广泛地接受,这主要是因为建模引证了分解,抽象和层次结构的原则。设计中的每个模型都描述了被考虑的系统的某个方面。我们尽可能地在老模型的基础上构建新模型,因为我们对那些老模型已经建立起了信心。模型让我们有机会在受控制的条件下失败。我们在预期的情况和特殊的情况下评估每个模型,当它们没能按照我们的期望工作时,我们就修改它们

我们发现,为了表达一个复杂系统的所有精妙之处,必须使用多种模型。例如,当设计一台个人计算机时,电子工程师必须考虑系统的组件级视图以及线路板的物理布局。这个组件视图构成了系统设计的逻辑视图,它帮助工程师思考组件间的协作关系。线路板的布局代表了这些组件的物理封装,它受到线路板尺寸,可用电源和组件种类等条件的限制。通过这个视图,工程师可以独立地思考散热和制造等方面的问题。线路板的设计者还必须考虑到在建系统的动态方面和静态方面。因此,电子工程师利用一些图示来展示单个组件之间的静态连接,也用一些时间图来展示这些组件随时间变化的行为。然后,工程师可以利用示波器和数字分析设备来验证静态模型和动态模型的正确性 

      2. 软件设计方法学的要素

显然,不存在魔法,没有“银弹”可以万无一失地让软件工程师从需求得到一个复杂系统的实现。实际上,关于复杂系统的设计,不存在所谓的指南手册。正如前面提到的复杂系统的5个属性,设计这样的系统需要增量和迭代的过程

但是,好的设计方法仍然为开发过程带来了非常必要的实践方法。软件工程界已经发展出几十种不同的设计方法学,可以将这些方法学大致分成三类。除了它们的不同之处,所有这些方法都有一些共同的要素。具体来说,包括下列要素:

  • 表示法 表达每个模型的语言
  • 过程 导致有序构建系统模型的过程
  • 工具 消除建模中的枯燥工作并强制实现模型本身的规则工件,可以揭示错误和不一致性

一个好的设计方法基于牢固的理论基础,同时又提供艺术创新的自由 

      3. 面向对象开发的模型     

是否存在“最好的”设计方法?对这个问题没有绝对的答案。它实际上只是前面问题的另一种提法:什么是分解一个复杂系统的最佳方法?通过反复实践,我们发现,构建关注问题域中的“事物”的模型具有很大的价值,这形成了我们所谓的面向对象分解

面向对象分析和设计的方法实现了面向对象分解。通过应用面向对象设计,我们创建出能够灵活适应变化的软件,并体现出表达的经济性。通过明智地分离它的状态空间,我们对软件正确性的信心被提升到了一个新高度,最终,降低了开发复杂软件系统所固有的风险

  1.7 小结

  • 软件本质上是复杂的,软件系统的复杂性常常超出了人类智能的范围
  • 软件开发团队的任务就是制造出简单的假象
  • 复杂性常常以层次结构的形式表现出来,建立复杂系统的“是一种”和“组成部分”层次结构模型是有意义的
  • 复杂系统通常是从一些稳定的中间状态演进而来的
  • 人类的认知有一些基本的限制因素,我们可以通过分解,抽象和层次结构来克服这些限制
  • 复杂系统可以从事物或处理过程的角度来分析,采用面向对象的分解有一些令人感兴趣的理由。在这种方法中,将世界看作是一组有意义的对象进行协作,实现某种高级的行为
  • 面向对象分析和设计的方法实现了面向对象分解,面向对象的设计采用了一套表示法和过程来构造复杂软件系统,提供了丰富的模型,可以通过这些模型来阐明目标系统的不同方面

第2章 对象模型

面向对象技术建立在很好的工程基础之上,它的要素称为“开发对象模型”,或简称为“对象模型”。对象模型包括抽象,封装,模块化,层次结构,类型,并发和持久等原则。就它们本身来说,没有一项原则是新的。但重要的是,对象技术将这些要素以一种相互配合的方式结合起来了

面向对象分析和设计在本质上与传统的结构化设计方法是不同的:它要求以一种不同的方式来思考分解,它得到的软件架构基本上超出了结构化设计的领域

  2.1 对象模型的演进

面向对象开发是建立在以前技术的最佳思想之上的    

    2.1.1 程序设计语言的换代

 

 

第一代语言主要用于科学和工程应用,这个问题领域的词汇几乎全是数学,所以,第一代高级程序设计语言标志着向问题空间靠近了一步,向底层计算机远离了一步

第二代语言中,重点是算法抽象。同样,这一代的程序设计语言让我们向问题空间又靠近了一步,向底层计算机又远离了一步

第三代语言演进到支持数据抽象,这时,程序员可以描述相关数据的意义(它们的类型),并让程序设计语言强制确保这些设计决策。这一代高级程序设计语言再一次让我们向问题空间靠近了一步,向底层计算机远离了一步 

    2.1.2 第一代和第二代早期程序设计语言的拓扑结构

所谓”拓扑结构“(topology),指的是这种语言的基本物理构成单元,以及这些部分是如何连接的

 

像FORTRAN和COBOL这样的语言,所有应用的基本物理构成单元是子程序(对于使用COBOL的人来说,称之为段落) 

用这些语言编写的应用展现出相对较平的物理结构,只包含全局数据和子程序 

    2.1.3 第二代后期和第三代早期程序设计语言的结构

在20世纪60年代中期,程序被认为是问题和计算机之间重要的中间点。首次软件抽象,现在所谓的”过程化“抽象,直接来自于这种实际的软件观点,子程序在1950年之前就发明了,但是作为一种抽象,那时候并没有被完全接受,相反,最初它们被看作是一种节省劳力的机制,但是很快,子程序就被认为是抽象程序功能的一种方式

意识到子程序可以作为一种抽象机制,这产生了三个重要结果,首先,人们开始发明一些语言,支持各种参数传递机制。其次,奠定了结构化程序设计的基础,表明在语言上支持嵌套的子程序,并在控制结构和声明的可见性范围方面发展了一些理论。最后,出现了结构化设计方法,为试图构建大型系统的设计提供了指导,利用子程序作为基本构建块。因此,毫无悬念,如图2-2所示,第二代后期和第三代早期语言的结构基本上是前一代语言的主题变奏。这种结构关注早期语言的一些不足之处,具体来说就是需要对算法抽象有更强的控制。但是,它仍然未能解决大规模程序设计和数据设计的问题    

    2.1.4 第三代后期程序设计语言的结构

从FORTRAN II开始,在大部分第三代后期程序设计语言中,出现了另一种重要的结构机制,并发展为对不断增长的大规模编程问题的关注。大规模编程项目意味着大型的开发团队,因此需要独立地开发同一个程序的不同部分。这种需求的解决方案是能够独立编译的模块,这在早期的概念中只是一种随意的数据和子程序的容器,如图2-3所示。模块很少被看作是一种重要的抽象机制,在实践中,它们只是用于对最有可能同时改变的子程序分组 

 

在这一代语言中的大多数虽然支持某些模块化结构,但是很少有规则要求模块间接口的语义一致性。为一个模块编写子程序的开发者可能假定它会通过三个不同的参数调用:一个浮点数,一个包含10个元素的数组和一个代表布尔标记的整型。在另一个模块中,对这个子程序的调用可能使用了不正确的参数,违反了这一假定:一个整数,一个包含5个元素的数组和一个负数。类似地,一个可能使用一块公共数据,把它当作自己的一样,而另一个模块可能违反这些假定,直接操作这块数据。不幸的是,因为这些语言中的大部分对数据抽象和强类型支持得都不太好,这样得错误只有在执行程序时才能被检测出来 

    2.1.5 基于对象和面向对象的程序设计语言的结构

数据抽象对于掌握复杂性是很重要得。”通过过程可以实现的抽象在本质上很适合描述抽象操作,但并不太适合描述抽象的对象。这是一个严重的缺陷,在许多应用中,要操作的数据对象的复杂性在很大程度上决定了问题的复杂性。“这种认识有两个重要结果。首先,出现了数据驱动的方法,它为面向对象的语言提供了一种解决数据抽象问题的方法,其次,出现了关于类型概念的理论,这种理论最终在像Pascal这样的语言中得到了实现

 

这类语言的构建块是模块,它表现为逻辑上的一组类或对象,而不像早期语言那样是子程序。换言之,”如果过程和函数是动词,数据是名词,那么面向过程语言的程序就是围绕动词组织的,面向对象的程序就是围绕名词组织的“。出于这个原因,小型或中型面向对象应用的物理结构表现为一个图,而不像面向算法的语言那样通常是一棵树。另外,基本上很少或没有全局数据。数据和操作被放在一个单元中,系统的基本逻辑构建块不再是算法,而是类或对象

到目前为止,我们已经超越了”大型编程“(programming-in-the-large),必须面对”巨型编程“(programming-in-the-colossal)。对于非常复杂的系统,我们发现类,对象和模块提供了基本的抽象手段,但这还不够,幸运的是,对象模型在规模上可以扩展,在大型系统中,一组抽象可以构建在另一组抽象层之上。在任何一个抽象层上,都可以找到某些有意义的对象,它们可以协作实现更高层的行为。如果我们仔细看一组对象的实现,会看到另一组协作的抽象

  2.2 对象模型基础

对象模型已被证实是计算机科学中的一个统一的概念,它不仅仅适用于程序设计语言,也适用于用户界面,数据库甚至计算机架构的设计。这种广泛适用的原因就是,面向对象帮助我们处理许多种不同系统中固有的复杂性

因此,面向对象分析和设计代表了一种演进式的开发,而不是一种革命性的开发。它没有抛弃过去的优点,而是建立在已经证明的好方法的基础之上 

 

    2.2.1 面向对象编程

面向对象编程是一种实现的方法,在这种方法中,程序被组织成许多组相互协作的对象,每个对象代表某个类的一个实例,而类则属于一个通过继承关系形成的层次结构

这个定义有三个要点:(1)利用对象作为面向对象编程的基本逻辑构建块,而不是利用算法;(2)每个对象都是某个类的一个实例;(3)类与类之间可以通过继承关系联系在一起

一个程序可能看起来像是面向对象的,但是如果不满足这三点之一,它就不是一个面向对象的程序。具体来说,没有继承的编程显然不是面向对象的,那只是利用抽象数据类型在编程

    2.2.2 面向对象设计

面向对象设计是一种设计方法,包括面向对象分解的过程和一种表示法,这种表示法用于展现被设计系统的逻辑模型和物理模型,静态模型和动态模型

这个定义中有两个要点:(1)面向对象设计导致了面向对象分解,(2)面向对象设计使用了不同的表示法来表达系统逻辑设计(类和对象结构)和物理设计(模块和处理架构)的不同模型,以及系统的静态和动态特征

对面向对象分解的支持是面向对象设计与结构化设计的不同之处:前者利用类和对象抽象来构建逻辑系统结构,后者则利用算法抽象。我们用术语”面向对象设计“来指所有导致面向对象分解的方法 

    2.2.3 面向对象分析

面向对象分析是一种分析方法,这种方法利用从问题域的词汇表中找到的类和对象来分析需求

OOA,OOD和OOP之间的关系如何?基本上,面向对象分析的结果可以作为开始面向对象设计的模型,面向对象设计的结果可以作为蓝图,利用面向对象编程方法最终实现一个系统 

  2.3 对象模型要素

Bobrow和Stefik将编程风格定义为”一种组织程序的方式,基于某种编程概念模型和一种合适的语言,其目的是使得用这种风格编写的程序很清晰“。他们进一步指出,存在5种主要的编程风格,这5种风格以及它们使用的抽象如下:

  1. 面向过程 算法
  2. 面向对象 类和对象
  3. 面向逻辑 目标,通常以谓词演算的方式表示
  4. 面向规则 如果-那么规则
  5. 面向约束 不变的关系

没有一种编程风格是最适合所有类型的应用的。例如,面向规则的编程可能最适合设计知识库,而面向过程的编程可能最适合设计计算密集的操作。根据我们的经验,面向对象风格最适合的应用范围最广,实际上,这种编程风格通常作为架构框架,被其他编程风格所使用

每一种编程风格都是基于它自己的概念框架。对于所有面向对象的东西,概念框架就是对象模型。这个模型有4个主要要素:

  1. 抽象
  2. 封装
  3. 模块化
  4. 层次结构

所谓”主要“,指的是如果一个模型不具备这些元素之一,就不是面向对象的

对象模型有3个次要要素

  1. 类型
  2. 并发
  3. 持久

所谓”次要“,指的是这些要素是对象模型的有用组成部分,但不是本质的

    2.3.1 抽象的意义

抽象是我们人类处理复杂性的基本方式。Dahl,Dijkstra和Hoare指出,”抽象来自于对真实世界种特定对象,场景或处理的相似性的认知,并决定关注这些相似性而忽略不同之处“。Shaw将抽象定义为”对一个系统的一种简单的描述或指称,强调系统的某些细节或属性同时抑制另一些细节或属性。 好的抽象强调了对读者或用户重要的细节,抑制了那些至少是暂时的非本质细节或枝节“。Berzins,Gray和Naumann建议,”只有当一个概念可以独立于最终使用和实现它的机制来描述,理解和分析时,我们才说这个概念是抽象的“。结合这些不同的观点,我们将抽象定义如下:

”抽象描述了一个对象的基本特征,可以将这个对象于所有其他类型的对象区分开来,因此提供了清晰定义的概念边界,它与观察者的视角有关“

对于给定的问题域决定一组正确的抽象,就是面向对象设计的核心问题。因为这个问题如此重要,所以我们将用第4章一整章的篇幅来讨论它

从那些准确地为问题域实体建模的对象到那些实际上没有什么理由存在的对象,存在着一系列的抽象。按最有用到最没用的次序,这些抽象是:

  • 实体抽象 一个对象,代表了问题域或解决方案域实体的一个有用的模型
  • 动作抽象 一个对象,提供了一组通用的操作,所有这些操作都执行同类的功能
  • 虚拟机抽象 一个对象,集中了某种高层控制要用到的所有操作,或者这些操作将利用某种更低层的操作集
  • 偶然抽象 一个对象,封装了一组相互间没有关系的操作

我们追求构建实体抽象,因为它们直接对应着给定问题域的词汇

客户对象是使用其他对象(称为服务器对象)的资源的对象。可以通过考虑对象提供给其他对象的服务来总结一个对象的行为,以及它可能施加在其他对象上的操作。这种视角迫使我们集中关注对象的外部视图,导致了Meyer所谓的“编程契约模型”:每个对象的外部视图定义了一份契约,其他对象可以依赖这份契约,而该对象则需要通过它的内部视图来实现这份契约(常常需要与其他对象协作)。这份契约因此建立了客户对象可以对服务器对象做出的所有假定。换言之,这份契约包含了对象的责任,即它的可靠的行为

单独来看,构成这份契约的每个操作都有一个唯一的签名,包含它所有的正式参数和返回值。我们把客户对象可以调用的整个操作集,以及这些操作合法的调用顺序,称为它的“协议”。协议表明了对象的动作和反应的方式,从而构成了抽象的完整静态和动态外部视图

抽象思想的核心是不变性的概念。“不变量“(invariant)是某种布尔(真或假)条件,它的值必须保持不变。对于对象的每个操作,我们可以定义”前置条件“(precondition)(操作假定的不变量)和”后置条件“(postcondition)(操作满足的部变量)。违反一个不变量将破坏一个抽象相关的契约。如果违反了前置条件,这意味着客户没有完成它那部分的责任,因此服务器不可能可靠地执行。类似地,如果违反了后置条件,这意味着服务器没有完成它那部分的责任,所以客户不能再信任服务器的行为。出现异常表明某个不变量没有满足或不能满足。某些语言允许对象抛出异常,这样就可以中止处理,向其他对象报告问题,然后这些对象就可以捕捉异常并处理问题

顺便提一下,术语“操作”,“方法”和“成员函数”是从三种不同的编程文化种发展而来的(分别是Ada,Smalltalk和C++)。它们基本上指的是同样的东西,所以我们互换使用这些术语

 

 

    2.3.2 封装的意义

抽象和封装是互补的概念:抽象关注对象的可以观察到的行为,而封装关注这种行为的实现。封装通常是通过信息隐藏来实现的(不只是数据隐藏)。信息隐藏是将那些不涉及对象本质特征的秘密都隐藏起来的过程。通常,对象的结构是隐藏的,其方法的实现也是隐藏的。“复杂系统的每一部分都不应该依赖于其他部分的内部细节”。抽象“帮助人们思考他们做什么”,而封装“让程序可以借助最少的工作进行可靠地修改"

封装在不同的抽象之间提供了明确的边界,因此导致了清晰的分离关注 

封装是一个过程,它分隔构成抽象的结构和行为的元素,封装的作用是分离抽象的概念接口及其实现

 

    2.3.3 模块化的意义

 

"将一个程序分割到一些不同的组件中,这可以在某种程度上减少它的复杂性......虽然从这一点上来说,分割程序是有帮助的,但是分割程序的更大理由是它在程序内部创造了一些定义良好的,有文档描述的边界。这些边界,或者叫接口,对于理解程序是非常有价值的” 。在某些语言,如Smalltalk中,没有模块的概念,所以类就组成了分解的唯一物理单元。Java有包的概念,包中包含类。在许多其他语言中,包括Object Pascal,C++和Ada,模块是一种独立的语言结构,确保了一组独立的设计决策。在这些语言中,类和对象构成了系统的逻辑结构,我们把这些抽象放入模块中,形成系统的物理架构。特别是对于较大型的应用来说,可能有成百上千个类,使用模块对管理复杂性有很大的帮助

“模块化将程序划分为一些模块,这些模块可以独立地编译,但又与其他模块有联系。我们将使用Parnas的定义:‘模块之间的联系是模块相互之间所做出的假定’”。大多数语言将模块作为一个独立的概念,它们也区分模块的接口和它的实现。因此,可以说模块化和封装是密不可分的

对于一个给定的问题决定一组正确的模块,这和决定一组正确的抽象的难度几乎差不多

“模块化是一个系统的属性,这个系统被分解成一组高内聚,低耦合的模块”

因此,抽象,封装和模块化的原则是相辅相成的。一个对象围绕单一的抽象提供了一个明确的边界,封装和模块化都围绕这种抽象提供了屏障 

    2.3.4 层次结构的意义

抽象是个好东西,但是除了那些太简单的应用之外,所有应用中都会包含许多不同的抽象,我们不能够一下子就理解它们。通过隐藏抽象的内部视图,封装有助于管理这种复杂性。但是,这还不够。一组抽象常常构成一个层次结构,通过在设计中确定这些层次结构,可以极大地简化对系统的理解

“层次结构是抽象的一种分级或排序”

在复杂系统中,最重要的两种层次结构是它的类结构(“是一种” 层次结构)和对象结构(“组成部分”层次结构)

      1. 层次结构的例子:单继承

继承是最重要的“是一种”层次结构,它是面向对象系统的基本要素。继承基本上定义了类之间的关系,在这种关系中,一个类共享了一个或多个类(分别对应于单继承或多继承)中定义的结构或行为。 

      2. 层次结构的例子:多继承

 

      3. 层次结构的例子:聚合

“是一种”层次结构说明了一般/特殊关系,而“组成部分”层次结构“则描述了聚合关系。例如,考虑种植园的抽象,可以认为种植园包含了一些植物和一份培育计划。换言之,植物是种植园的”组成部分“,培育计划也是种植园的”组成部分“,这种”组织部分“关系被称为聚合(aggregation)

聚合不是面向对象开发或面向对象编程语言所特有的概念。实际上,只要是支持类似记录结构的语言都支持聚合。但是,将继承和聚合结合在一起的功能是强大的:聚合允许对逻辑结构进行物理分组,而继承允许这些共同的部分在不同的抽象中被复用

在处理这样的层次结构时,常常提到抽象的层次,这个概念最早是由Dijkstra提出来的。对于”是一种“层次结构,高层的抽象是一般的,低层的抽象是具体的。因此,我们说花卉类的高层抽象是植物类。对于”组成部分“层次结构,一个类相对于组成它的实现的类来说,处于更高的层次。因此,种植园是植物类的更高层抽象 

聚合提出了所有权的问题。我们关于种植园的抽象允许不同的时间在种植园中培育不同的植物,但换一种植物不会改变种植园作为一个整体的特征,删除一个种植园也不会摧毁它的所有植物(它们可能被移植)。换言之,种植园和它的植物的生命周期是不相关的。与此不同的是,我们认为培育计划对象与种植园对象有着固有的关系,不能够独立存在。因此,当创建一个种植园的实例时,也创建了一个培育计划实例,当删除种植园对象时,也会删除培育计划实例

    2.3.5 类型的意义

类型的概念主要来自于抽象数据类型的理论。Deutsch指出,“一个类型是关于结构或行为属性的准确描述,一组实体共享这些属性”。为方便起见,我们互换地使用术语类型(type)和类(class)。虽然类型和类的概念相似,我们仍然把类型作为对象模型的一个独立要素,因为类型的概念对于抽象的含义有着特别不同的强调。具体来说,我们认为:“类型是关于一个对象的类的强制规定,这样一来,不同类型的对象不能够互换使用,或者至少它们的互换使用受到非常严格的限制

类型匹配的概念是类型概念的核心

 

多态(polymorphism)是动态类型和继承互相作用时出现的一种情况。多态代表了类型理论中的一个概念,即一个名字(如一个变量声明)可以代表许多不同类的对象,这些类具有某个共同的超类。这个名字所代表的对象因此可以响应一组共同的操作。与多态相对的是单态(monomorphism),这在强类型和静态类型的语言中都可以发现

多态可能是面向对象语言中除了对抽象的支持以外最强大的功能,也正是它,区分了面向对象编程和较传统的抽象数据类型编程。多态也是面向对象设计中的核心概念

    2.3.6 并发的意义

虽然面向对象编程关注数据抽象,但是封装,继承和并发关注了过程抽象和同步。对象是统一这两种不同观点的概念:每个对象(来自于真实世界的一个抽象)都可以代表一个独立的控制线程(一种过程抽象)。这样的对象被称为“主动的”。在基于面向对象设计的系统中,我们可以将世界概念化为一组协作的对象,其中某些是主动的,因此作为独立活动的中心,出于这个概念,我们将并发定义如下:

并发是一种属性,它区分了主动对象和非主动对象” 

    2.3.7 持久的意义

持久是对象的一种属性,利用这种属性,对象跨越时间(例如,当对象的创建不存在了的时候,对象仍然存在)和空间(例如,对象的位置从它被创建的地址空间移开)而存在

  2.4 应用对象模型 

    2.4.1 对象模型的好处

  • 使用对象模型帮助我们探索基于对象和面向对象编程语言的表达能力
  • 利用对象模式不仅鼓励软件的复用,而且鼓励整个设计的复用,这导致了可复用应用框架的产生
  • 使用对象模型将得到构建在稳定的中间状态之上的系统
  • 对象模型引起了人类认知的工作的兴趣。Robson说:“许多不知道计算如何工作的人会发现,面向对象系统的思想非常自然”

    2.4.2 开放式问题

为了有效应用对象模型的诸要素,接下来必须关注如下一些开放式问题:

  • 究竟什么是类和对象?
  • 如何正确地确定与特定应用有关的类和对象?
  • 怎样的表示法适合表示面向对象系统的设计?
  • 怎样的过程可以导致结构良好的面向对象系统?
  • 使用面向对象设计管理层意味着什么?

这些问题是后面5章的主题 

  2.5 小结

  • 软件工程的成熟导致了面向对象分析,设计和编程方法的形成,所有这些技术都是为了解决大规模编程的问题
  • 有一些不同的编程模式:面向过程的,面向对象的,面向逻辑的,面向规则的,面向约束的
  • 抽象描述了一个对象的基本特征,可以将这个对象与所有其他类型的对象区分开来,因此提供了清晰定义的概念边界,它与观察者的视角有关
  • 封装是一个过程,它被分隔构成抽象的结构和行为的元素,封装的作用是分离抽象的概念接口及其实现
  • 层次结构是抽象的一种分级或排序
  • 类型是关于一个对象的类的强制规定,这样,不同类型的对象不能够互换使用,或者至少它们的互换使用受到非常严格的限制
  • 并发是一种属性,它区分了主动对象和非主动对象
  • 持久是对象一种属性,利用这种属性,对象跨越时间和空间而存在

第3章 类与对象

  3.1 对象的本质 

    3.1.1 什么是对象,什么不是对象

在软件中,“对象”这个术语首先正式出现在Simula语言中

 

某些对象可能有明确的概念边界,但其代表的是不可触摸的事件或过程。

一个对象是一个具有状态,行为和标识符的实体。结构和行为类似的对象定义在它们共同的类中。“实例”和“对象”这两个术语可以互换使用 

    3.1.2 状态

对象的状态包括这个对象的所有属性(通常是静态的)以及每个属性当前的值(通常是动态的)

一个属性是一种内在或独特的特征,特点,品质或者特性,使一个对象区别于别的对象

 

    3.1.3 行为

行为是对象在状态改变和消息传递方面的动作和反应的方式

一个操作是某种动作,一个对象对另一个对象执行这个操作,目的是获得反应。例如,客户可能调用append和pop操作,分别使一个队列对象增长或缩减。客户也可能调用length操作,它返回一个值,表示队列对象的大小,但不会改变队列本身的状态

一般来说,一个消息就是一个对象执行了另一个对象的操作,虽然底层的分发机制是不一样的。方便起见,我们互换使用”操作“和”消息“这两个术语

对象的行为会受到其状态的影响

改进的状态的定义

一个对象的状态代表了它的行为的累积效果

      1. 操作

一个操作代表了一个类提供给它的对象的一种服务。一个客户通常执行一个对象的5种操作

  • 修改操作 更改一个对象的状态的操作
  • 选择操作 访问一个对象的状态但并不更改这个状态的操作
  • 遍历操作 以一种定义良好的方式访问一个对象的所有部分的操作
  • 构造操作 创建一个对象并初始化它的状态的操作
  • 析构操作 释放一个对象的状态并销毁对象本身的操作

      2. 角色和责任

一个对象的所有方法共同构成了它的协议

 

      3. 对象像自动机

对象中存在状态,这意味着操作调用的次序非常重要。从而,导致了这样一种思想,即每个对象就像一个微小的,独立的自动机

    3.1.4 标识符

标识符是一个对象的属性,它区分了这个对象与其他所有对象

  3.2 对象之间的关系 

    3.2.1 链接

链接(link)这个术语来自Rumbaugh等人,他们将它定义为“两个对象之间物理上或概念上的联系

 

作为链接大参与者,一个对象可能扮演以下三种角色之一:

  • 控制器 这个对象可以操作其他对象,但不会被其他对象操作。在某些地方,“主动对象”和“控制器”这两个术语是互换使用的
  • 服务器 这个对象不操作其他对象,它只被其他对象操作
  • 代理 这个对象既可以操作其他对象,也可以被其他对象操作。创建代理通常是为了表示问题领域中的一个真实对象

FlowController是一个控制器对象,DisplayPanel是一个服务器对象,Valve是一个代理

FlowController调用 valve.adjust(单向流动), value 调用 DisplayPanel

FlowController调用 isClosed(双向流动),如果Valve关闭了,FlowControlller 调用DisplayPanel

当一个对象通过链接向另一个对象发送一条消息时,这两个对象就称为同步

    3.2.2 聚合

链接表明了一种端到端的关系或客户/服务提供者的关系,而聚合则表明了一种整体/部分层次结构,提供了从整体(也被称为聚合体)导航到它的部分的能力

 

如果一个对象是另一个对象的一部分,就意味着它到它的聚合体有一个链接。通过这个链接,聚合体可以向它的部分发送消息。对于TemperatureController对象,可以找到它对应的Heater。对于Heater这样的对象,当且仅当Heater的状态包括它的包装对象(也被称为它的容器)的知识时,才有可能导航到它的包装对象

聚合可以代表物理上的包含,也可以不代表。例如,一架飞机由机翼,引擎,起落架等组成,这是物理上包含的例子。于此不同,股票持有人及其持有股票之间的关系则是不需要物理上包含的聚合关系。股票持有人拥有股票,但这些股票不是股票持有人的物理组成部分。这种整体/部分的关系更多的是概念上的,因此不太直接, 不像构成飞机的各部分那样的物理聚合

显然,在链接和聚合之间需要折中,有时候聚合更好,因为它将各个部分封装为整体的秘密:有时候链接更好,因为它们允许对象之间较松的耦合。 

  3.3 类的本质    

    3.3.1 什么是类,什么不是类

在面向对象分析和设计的上下文中,我们将类定义为:

类是一组对象,它们拥有共同的结构,共同的行为和共同的语义

一个对象就是类的一个实例    

 

什么不是一个类?一个对象不是一个类。没有共同结构和行为的对象不能够被划分为一类,因为根据定义,它们除了都是对象之外,没有别的共同点 

值得一提的是,类(正如大多数编程语言中所定义的)对于分解是必要的,但不是充分的。某些抽象非常复杂,所以不能够利用单个类定义的方式很方便地表达。例如,一种相当高层的抽象,一个GUI框架,一个数据库和整个库存系统在概念上都是独立的对象,但它们都不能被表示为一个单独的类。相反,最好是将这些抽象表示为一组类,这些类的实例互相协作,提供我们所期望的结构和功能。Stropstrup将这样的一组类称为一个组件

    3.3.2 接口和实现

Meyer和Synder都指出,编程在很大程度上是一种”制定契约“:一个较大问题的不同功能通过子契约被分配给不同的设计元素,被分解成较小的问题。没有别的情况比在设计类时更能体现这种思想了

这种编程即契约的观点区分了一个类的外部视图和内部视图。一个类的接口提供了它的外部视图,因此强调了抽象,隐藏了它的结构和行为的秘密。 

    3.3.3 类的生命周期

  3.4 类之间的关系

总的来说,存在三种基本类型的类关系。

  • 第一种关系是一般/特殊关系,表示”是一种“关系。例如,玫瑰是一种花
  • 第二种关系是整体/部分,表示”组成部分“关系。因此,花瓣不是一种花,它是花的一个部分
  • 第三种关系是关联,表示某种语义上的依赖关系,如果没有这层关系,这些类就毫无关系了。如瓢虫和花之间的关系。再入,玫瑰和蜡烛基本上是独立的类,但它们都是可以用来装饰餐桌的东西

    3.4.1 关联

在这些不同类型的类关系中,关联是最常见的,也是语义上最弱的。确定类之间的关联通常是分析和早期设计的活动。随着继续设计和实现,我们常常会细化这些较弱的关联,将它们变成某种更具体的类关系

      1. 语义上的依赖关系

如示例3-4所示,关联只代表一种语义上的依赖关系,它不表示这种依赖关系的方向(如果没有特别说明,关联意味着双向导航,如我们的例子所示),也不表示一个类与另一个类相关的具体形式(我们只能通过命名每个类在关系中扮演的角色来暗示这些语义)。

 

       2. 关联有以下三种常见的多重性

  • 一对一
  • 一对多
  • 多对多

    3.4.2 继承

 

 

      1. 单继承

子类通常扩展或限制了超类中原有的结构和行为。扩展超类的子类被称为扩展继承。例如,子类GuardedQueue可能提供额外的操作,使这个类在多控制线程的情况下变得安全,从而扩展了超类Queue的行为。与此相对的是,限制超类的子类被称为限制继承。例如,子类UnselectableDisplayItem可能限制了超类DisplayItem的行为,禁止客户从视图中选择它的实例。在实践中,子类扩展了超类还是限制了超类并不是总是很清楚。实际上,子类常常同时做这两件事

      2. 多态

多态是类型理论中的一个概念,即一个名字可能代表许多不同类的实例,只要它们都有共同的超类。于是,由这个名字所代表的对象就能够以不同的方式对同一组操作做出反应。利用多态,一个操作可以被层次结构中的类以不同的方式实现。

多态的概念首先是由Strachey提出的,他谈到了一种初级的多态,指出像+这样的操作符可以被定义成含义不同的东西。我们将这个概念称为“重载”。

没有多态,开发者写的代码就必须包含大量的case或switch语句。没有多态,我们就不能为各种遥测数据创建一个类层次结构,而不得不定义一个一体化的可变记录来包含所有与这类数据相关的属性。为了区分每一种变种,必须检查与该记录关联的标记

有了继承,就不需要一体化的记录,因为可以区分不同类型的抽象。Kanplan和Johnson指出:“如果许多类使用相同的协议,多态就最有用”。利用多态,我们就不需要大型的case语句,因为每个对象都知道自己的类型 

      3. 多继承

    3.4.3 聚合

我们也需要聚合关系,它提供了类实例中的整体/部分关系。类之间的聚合关系与这些类的对象之间的聚合关系是并存的

 

虽然股票持有人拥有股票,但股票持有人并没有在物理上包容拥有的股票。相反,这些对象的生存期完全可以是独立的,虽然概念上的整体/部分关系仍然存在(每股股票都是股票持有者的一部分资产)。这种聚合的表示形式可以是非常间接的

这仍是聚合,虽然它不是物理上的包容。总之,聚合的判别测试是:当且仅当两个对象之间存在整体/部分关系时,它们对应的类之间必然存在一种聚合关系 

    3.4.4 依赖关系

依赖关系表明,出于这种关系一端的元素以某种方式依赖于出于另一端的元素。这警告了设计者,如果其中一个元素发生了改变,可能会影响到另一个元素。 

  3.5 类与对象的互动

    3.5.1 类与对象的关系 

    3.5.2 类与对象在分析和设计中的角色

在分析阶段和设计的早期阶段,开发者有两项主要任务:

  • 从问题域的词汇表中确定处类;
  • 创建一些结构,让多组对象一起工作,提供满足问题需求的行为

我们将这样的类和对象统称为问题的“关键抽象”,把这些协作结构称为实现的“机制” 

在开发这些的阶段中,开发者必须关注这些关键抽象和机制的外部视图。外部视图代表了系统的逻辑框架,因此包含了系统的类结构和对象结构。在设计阶段的后期以及随后的实现阶段,开发者的任务发生了变化,其关注的焦点放在了这些抽象和机制的内部视图上,包括它们的物理实现

  3.6 创建高品质的类与对象

Ingalls建议:“系统应该利用一组最少的不会变化的部分进行构建,这些部分应该尽可能地通用,系统的所有部分应该被放入一个统一的框架”。对于面向对象来说,这些部分就是构成系统的关键抽象的类和对象,这个框架由系统的机制来提供

根据我们的经验,类和对象的设计是一个增量,迭代的过程。坦白地说,除了那些最不重要的抽象,我们从来没有第一次就完全正确地定义一个类。对于最初的抽象,需要花一些时间来琢磨它粗糙的概念边界。当然,精化这些抽象是有代价的,包括系统的重新设计,系统设计的可理解性和系统设计结构的完整性等方面。因此,我们希望在一开始就尽量正确 

    3.6.1 评判一种抽象的品质

人们怎样才能知道某个类或对象的设计是良好的?我们建议使用下面5个测量指标:

  • 耦合
  • 内聚
  • 充分性
  • 完整性
  • 基础性

耦合是来自于结构化设计的一个概念,但从字面上的解释来看,它也适用于面向对象设计。Stevens,Myers和Constantines将耦合定义为“一个模块与另一个模块之间建立起的关联强度的测量。强耦合使系统变得复杂,因为如果模块与其他模块高度相关,它就难以独立地被理解,变化或修正。通过设计系统,使模块间的耦合降至最低,可以降低复杂性” 。Page-Jones给出了一个好的耦合的反例,它描述了一个模块化的立体声系统,其供电部分位于一个音箱之中

模块之间的耦合也适用于面向对象的分析和设计,但类和对象之间的耦合同样重要。然后,在耦合和继承的概念之间存在着矛盾关系,因为继承引入了严重的耦合。一方面,我们希望类之间的弱耦合,另一方面,继承(超类和子类之间的强耦合)又能帮助我们处理抽象之间的共性

 

内聚的思想也来自结构化设计。简单地说,内聚测量了单个模块(对于面向对象来说,就是单个类或单个对象)内各个元素的联系程度。最不希望出现的内聚就是偶然性内聚,即将完全无关的抽象塞进同一个类或模块中。例如,考虑由狗和航天飞机的抽象组成的一个类,这两种抽象的行为基本上是无关的。最希望出现的内聚就是功能性内聚,即一个类或模式的各元素一同工作,提供某种清晰界定的行为。因此,如果Dog类的语义包含了一只狗的行为----完全是狗,只有狗而没有其他,那么它就是功能性内聚

 

与耦合和内聚的概念相关,一个类或模块应该是充分的,完整的,简单的。所谓充分,指的是类或模块应该记录某各抽象足够多的特征,从而允许有意义的,有效的交互。否则,将使该组件变得无用。例如,如果我们设计Set(集合)类,应该包含从集合中删除元素的操作,但如果忘记了加入元素的操作,我们的努力就徒劳了。在实践中,这种问题很早就会被发现。只要我们构建一个必须使用这种抽象的客户时,就会发现这样的缺陷

 

所谓完整,指的是类或模块的接口记录了某个抽象全部有意义的特征。充分性意味着最小的接口,但一个完整的接口就意味着该接口包含了某个抽象的所有方向。因此完整的类或模块是足够通用的,可以被任何客户使用。完整性是一种主观判断,有可能做过头。为某个抽象提供全部有意义的操作会让用户不知所措,通常这也是不必要的,因为许多高级操作可以由低级操作组合得到。出于这个原因,我们也建议类和模块应该具有基础性

 

基础性操作就是只有访问该抽象的底层表现形式才能够要有效地实现的那些操作。因此,对一个集合添加一个元素就是一项基础性操作,因为要实现这个Add操作,必须知道底层表现形式。另一方面,向集合添加4项元素的操作不是基础性操作,因为它可以通过更为基础性的Add操作来有效地实现,不必访问底层的表现形式。当然,有效与否也是一种主观的判断。如果一个接口操作只能通过访问底层表现形式来实现,那它无疑是基础性的。如果一个操作可以在已有的基础性操作之上实现,但这样做会消耗大量的计算资源,那么它也可以作为基础性操作的候选者 

    3.6.2 选择操作

打造类或模块的接口是艰苦的工作。通常,我们先尝试设计类,然后,当我们和其他人创建客户时,我们发现需要扩展,修改并进一步精化这个接口。最后,我们可能发现一些操作的模式或抽象的模式,导致我们创建一些新的类或重新组织已有类之间的关系

      1. 功能语义

对于一个给定的类,我们的风格是让所有的操作保持基础性,这样每个操作都展示出小的,定义良好的行为。我们把这样的方法称为“细致的(fine-grained)”。我们也倾向于分离方法,让它们相互之间不通信。通过这种方式,我们更容易构造一些子类,它们可以有意义地重新定义超类的行为。将一个行为提取为一个方法还是多个方法的决定有两个互相竞争的原因:将一个行为塞进一个方法中将导致更简单的接口,但方法会更大,更复杂;将一个行为分散到多个方法中将导致更复杂的接口,但方法会更简单。Meyer指出:“好的设计知道如何在太多契约和太少契约之间平衡折中,太多契约导致片段化,太少契约导致无法管理的大模块” 

在面向对象开发中,通常会整体设计一个类的所有方法,因为所有这些方法共同构成了这个抽象的完整协议。因此,对于某个期望的行为,我们必须决定将它放到哪个类中。Halbert和O'Brien提出了下面的判断条件,需在做这样的决定时思考

  • 可复用性 这个行为可以在多种上下文中使用吗?
  • 复杂性 实现这个行为的难度有多大?
  • 适用性 这个行为与打算放入的类型之间相关程度如何?
  • 实现知识 这个行为的实现依赖于一个类型的内部细节吗?

      2. 时间和空间语义

当确定存在某个操作,并定义了它的功能语言之后,必须决定它的时间语义和空间语义。这意味着我们必须决定它完成操作需要的时间以及存储空间。这样的决定通常是用最佳,平均和最差等术语来表达的,最差的情况规定了能够接受的上限 

    3.6.3 选择关系

在类之间和对象之间选择关系与选择操作是有联系的。如果决定对象X向对象Y发送消息M,那么X必须能够直接或间接地访问Y;否则,就不能够在X的实现中命名操作M。所谓能够访问,指的是一种抽象能够看到另一种抽象,并引用它的外部视图中的资源。只有当它们的范围重叠,并且访问得到授权时(例如,类的私有部分只能被该类本身和它的友元访问),一种抽象才可以访问另一种抽象。因此,耦合是度量可访问程度的指标 

    3.6.4 选择实现

只有当我们使某个类或对象的外部视图稳定下来之后,才会转向它的内部视图。这个视图涉及两个不同的决定:为类或对象选择表示形式,以及将类或对象放入一个模块

      1. 表示形式

类或对象的表示形式几乎总是该抽象封装起来的秘密。这使得我们可以改变表示形式(例如,改变时间语义和空间语义),同时又不会违反客户对功能所做的任何假定。

      2. 打包

类似的问题也适用于在模块中声明类和对象。可见性和信息隐藏这一对矛盾的需求通常引导我们决定在哪里声明类和对象。

  3.7 小结

  • 对象具有状态,行为和标识符
  • 类似对象的结构和行为定义在它们共同的类中
  • 对象的状态包括对象所有的属性(通常是静态的)加上这些属性当前的值(通常是动态的)
  • 行为是对象在状态改变和消息传递方面的动作和反应
  • 标识符是对象的属性,它区分这个对象和其他对象
  • 类是具有共同的结构和行为的一组对象
  • 三种关系包括关联,继承和聚合
  • 关键抽象是来自问题域词汇表的类和对象
  • 机制是一种结构,一组对象通过它相互协作,提供满足问题域的某种需求的行为
  • 抽象的品质可以通过它的耦合,内聚,充分性,完整性和基础性来度量 

第4章 分类

分类是组织知识的手段。在面向对象设计中,认识到事物间的相似性让我们能够将共性放在关键抽象和机制中,最终导致更小的应用和更简单的架构。不幸的是,没有实现分类的金光大道。对于那些习惯寻找菜谱答案的读者,我们明确地指出,确定类和对象没有简单的诀窍。没有所有的“完美”类结构,也没有一组“正确”的对象。像所有工程学科一样,我们的设计选择是对许多竞争因素的折中

幸运的是,在其他学科中存在这大量有关分类的历史经验。从一些更为经典的方法中,面向对象分析技术出现了,它提出了一些有用的,值得推荐的实践和经验法则,用于确定某个特定领域相关的类和对象

  4.1 正确分类的重要性

 

    4.1.1 分类的困难

 

    4.1.2 分类的增量和迭代本质

 

  4.2 确定类和对象

 

    4.2.1 经典方法和现代方法

 

    4.2.2 面向对象分析

 

  4.3 关键抽象与机制

 

    4.3.1 确定关键抽象

 

    4.3.2 识别机制

 

  4.4 小结

 

第5章 表示法

画图的动作并不是分析或设计的要点。一张图示只是记录了系统行为的一种说明(对于分析来说)或一种架构的愿景与细节(对于设计来说)。如果遵循任何一种工程师的工作方式(软件,土木,机械,化工,建筑或其他),你很快会意识到系统构想形成的唯一场所就是设计者的头脑

  5.1 统一建模语言

拥有一种定义良好的,富有表现力的表示法对于软件开发过程是很重要的。

  • 首先,标准的表示法让分析师或开发者能够描述一个场景,阐明一种架构,然后无二义地将这些决定告诉别人
  • 其次,正如Whitehead在他的数学名著中所说的,“好的表示法消除了大脑的不必要工作,让大脑能够集中考虑更高级的问题”
  • 最后, 一种富有表现力的表示法能够利用自动化的工具,消除关于这些决定的许多繁琐的一致性和正确性检查

美国国防科学委员会的一份报告指出:“软件开发是并且永远将是一种劳动力密集型的技术,虽然机器能够做一些辛苦而乏味的工作并让我们能够记录下我们心中的构想,但概念开发仍是典型的人类活动,软件开发中永远不会消失的部分就是打造概念结构,可以消失的部分是表达这些概念结构所需的工作量

    5.1.1 简单历史回顾

统一建模语言(UML)是分析,说明和设计软件系统的主要建模语言。 

    5.1.2 模型与多重视图

UML有几种不同类型的图,每一种都提供了系统的某一视图。Kleyn和Gringrich指出:”开发者必须理解对象所涉及的结构和功能,开发者必须理解类对象的分类结构,使用的继承机制,对象独立的行为以及整体系统的动态行为。这个问题有点类似观看网球或足球这样的体育赛事----每个摄像机都揭示了动作的一个方面,这不能由一个摄像机独立完成“

例如,考虑由数百个类组成的应用,不可能制作一张图来展示所有这些类和它们之间的关系,实际上也不需要这样做。我们会使用几张类图,每张图都展示模型的一个视图,有一张图可能展示了某些关键类的继承关系,另一张图可能展示了某个类用到的所有类的传递闭环

    5.1.3 图分类

UML图可以分成两大类:结构图和行为图

    5.1.4 在实践中使用图

UML是一份详细的规范,但这并不意味着每次都要用到它的所有方面。实际上,这种表示法的一个子集足以表达大多数分析和设计问题中的语义。

Winberg曾指出:在其他设计领域,例如建筑,草图是最常用到的图形手段,在创建性的设计工作完成之前,很少画出精确而详细的图纸。要记住,表示法只是记录系统行为和架构分析的工具,表示法本身并不是目的。因此,应该只使用那些表达意思所必需的表示法元素,而不是其他的东西 

    5.1.5 概念模型,逻辑模型和物理模型

随着系统开发的推进和成熟,你的系统模式可能代表了不同的细节层次。概念模型记录了系统中存在(或将存在)的领域实体以及它们与系统中其他领域实体的关系。概念层的建模是利用业务领域的术语来完成的,应该是技术无关的。系统的逻辑视图利用了概念模型中创造的概念,建立起关键抽象和机制的意义,并确定系统的架构和整体设计。系统的物理模型描述了系统实现的具体软件和硬件构成。显然,物理模型是技术相关的 

    5.1.6 工具的角色 

    5.1.7 面向对象开发的产品

通常,系统分析将得到一组用例图和活动图(通过场景来表示系统的行为),类图(表示代理的角色和责任,这些代理提供了系统的行为)及交互图和状态机图(展示这些代理的事件次序行为)。类似地,系统的架构可能包括几组包图,类图,对象图,组件图和部署图,以及它们对应的动态视图 

    5.1.8 规模上的伸缩

我们发现,UML既适用于只包含几十个类的小系统,也适合包含几千个类的大系统 

    5.1.9 UML的语法和语义

  • 包图
  • 组件图
  • 部署图
  • 用例图
  • 活动图
  • 类图
  • 序列图
  • 交互概述图
  • 组合结构图
  • 状态机图
  • 时间图
  • 对象图
  • 通信图

    5.1.10 UML2.0信息资源

  5.2 包图

在进行面向对象分析和设计时,需要组织开发过程的工件,从而清晰地展现出问题域的分析和相关的设计。具体的原因会有不同,但主要集中在可视模式本身的物理结构上,或者是希望通过多重视图来清晰地展现模型元素。组织OOAD工件的好处有以下几点:

  • 在复杂系统开发中提供清晰性和可理解性
  • 支持多用户使用的并发模型
  • 支持版本控制
  • 在多个层次提供抽象----从系统到组件中的类
  • 提供封装和包容,支持模块化

实现这种组织的方式是利用UML包图,它提供了表示UML元素分组的能力

包图的主要元素是包,它们的可见性和它们的依赖关系 

    5.2.1 基本概念:包表示法

 

它提供了HydroponicsGardeningSystem包的黑盒视图,没有显示其中包含的元素

 

 

包包含包 

    5.2.2 基本概念:元素的可见性

    5.2.3 基本概念:依赖关系

UML元素(包括包)之间的依赖关系是用一个虚线的开放箭头来表示的。箭头的尾部位于具有依赖性的元素(客户),箭头位于支持这种依赖的元素(提供者)。依赖关系可以标上标签,通过在书名号(《》)中包含依赖关系的类型(由一个关键字来表示),强调元素间依赖关系的类型。包特有的依赖关系包括导入,访问和合并,由于包容的元素之间的关系而导致的包间依赖关系包括跟踪,派生,精化,允许和使用

 

如果两个包之间存在多个包容元素依赖关系,这些依赖关系会聚合为包层面的依赖关系。包层面的依赖关系可以用一个关键词标签标出,放在书名号(《》)中,表示类型,但是,如果包含的依赖关系是不同类型的,包层面的依赖关系就不提供标签

    5.2.4 基本概念:包图

用包分组的元素通常应该在某种意义上是相关的。例如,它们是系统中的一个子系统,是系统某个方面的相关用例,或者是一些协作类,提供系统功能的一个子集

    5.2.5 高级概念:导入和访问

  5.3 组件图

组件代表了一块可复用的软件,它提供了某种有意义的功能集。从最低的层面上来说,组件是一组类,它们本身是内聚的,与其他类的耦合相对比较松。系统中的每个类要么处于一个组件中,要么处于系统的最顶层。组件也可以包含其他组件

组件是一种结构化的分类器(classifier),组件间的协作和内部结构可以利用组件图来表示。组件与组件之间通过定义良好的接口进行协作,从而提供系统的功能。组件本身也可以由一些协作的组件组成,提供它自己的功能。因此,我们可以用组件分层地结构一个系统,并表示它的逻辑架构    

    5.3.1 基本概念:组件表示法

由于组件是一个有结构的分类器,所以它的详细装备可以通过一个组合结构来表示,这个结构包括部件,端口和连接器

 

对于图5-9中显示的方形端口来说,我们有与之相连的接口,这些接口定义了组件交互的细节。

提供的接口使用小球表示法,表明该组件向环境提供的功能,LightingControl就是提供的接口的一个例子。

要求的接口使用球窝表示法,表明该组件向环境要求的接口,AmbientTemp就是要求的接口的一个例子。 

端口和接口之间不一定是一对一的关系,端口可以用来对接口分组,如图5-10所示。这样做可能是为了在复杂的图中提供清晰性,也可能是为了反映拥有一个端口的意图,某些类型的交互将通过这个端口进行。

    5.3.2 基本概念:组件图

在开发中,我们利用组件图来表达架构的逻辑分层和划分方式。组件图中展现了组件间的相互依赖关系,也就是它们通过定义良好的接口进行协作,从而提供系统的功能

 

像图5-9一样,我们使用小球和球窝表示法来指定每个组件提供的接口和要求的接口。两个组件之间的接口被称为“组装连接器”,也被称为“接口连接器”。虽然组装连接器是通过小球和球窝表示法来表示的,但也可以使用直线来表示每个连接。然而,用直线连接给出的信息要少一些

    5.3.3 基本概念:组件接口

 

    5.3.4 基本概念:组件实现

 

    5.3.5 高级概念:组件的内部结构

 

子系统划分了系统的逻辑模型,子系统包含其他的子系统和其他组件。系统中的每个组件要么处于某个子系统中,要么处于系统的顶层。在实践中,大的系统会有一个顶层组件图,包含处于最高抽象层的子系统。开发者通过这个图来理解系统的总体逻辑架构 

  5.4 部署图

部署图用于展示在系统的物理设计中,工件在节点上分布的情况。单张部署视图代表了一种系统工件结构的视图。在开发中,我们使用部署图来说明节点的物理集合,这些节点是系统执行的平台

部署图有三个基本元素:工件,节点和它们的连接 

    5.4.1 基本概念:工件表示法

工件是物理上存在的一件东西,它实现了一部分的软件设计。它通常是软件代码(可执行),但也可能是一个源文件,一份文档或与软件代码相关的其他文件。工件之间可以有关系,如依赖或组合

工件的表示法包括一个带有工件名称的类矩形,关键词标签《artifact》及一个可选的图标。其图标看起来像一页折了右上角的智。图5-17展示了HeatingController.exe工件,带有可选的图标

 

工件的名称中包含扩展名.exe,表明这是可执行的(即软件代码)。HeatingController.exe工件拥有一个到HeatingController组件的依赖关系,带有《manifest》标签。这意味着它是这个组件的物理实现,于是实现和设计就联系起来了。一个工件可以体现多个组件

    5.4.2 基本概念:节点表示法

节点是一种计算资源,通常包含存储和处理能力,工件部署在它上面执行。 节点可以包含其他节点,以表示复杂的执行能力,这种情况是通过嵌套或利用组合关系来体现的。有两种类型的节点:设备和执行环境

设备是一个提供了计算能力的硬件,如一台计算机,一个调制解调器或一个传感器。执行环境是软件,它用于部署特定类型的执行工件,如《database》和《J2EE server》。执行环境通常以一个设备作为宿主

节点之间会通过消息和信号进行通信,我们用一条实线来表示通信路径。

通信路径通常表示某种直接的硬件耦合,如USB电缆,以太网连接甚至是共享内存。但是,路径也可以表示不太直接的耦合,如卫星和地面的通信或移动电话通信

    5.4.3 基本概念:部署图

  5.5 用例图

多年的证据表明,软件项目失败最常见的原因都集中在关键涉众之间沟通不佳或缺少沟通

我们非常需要有一种系统开发方法,让开发组织能够理解业务的目标,同时又不会麻烦业务人员(毕竟,他们的主要工作是进行日常的操作和业务)。用例图提供了这种能力。我们通过用例图展示待建系统的上下文范围以及他提供的功能。它们描述了谁(或什么)与系统交互,外部世界希望系统做些什么

    5.5.1 基本概念:执行者

执行者是与系统交互的实体,他们可以是人或其他系统。执行者位于他们使用的系统之外,用棍状的小人来表示

 

 

在考虑执行者时, 一种方法是思考执行者所扮演的角色。在真实世界中,人们(系统)可能承担许多不同的角色。例如,一个人可以是一名销售人员,一名经理,一个父亲,一个艺术家等

    5.5.2 基本概念:用例

Jacobson等人对用例的定义如下:

用例是通过某部分功能来使用系统的一种具体的方式,因此,用例是相关事务的一个具体序列,执行者和系统以对话的方式执行这些事务,从用户的观点来看,每个用例都是系统中一个完整序列的事件

    5.5.3 基本概念:用例图

      1. 确定用例细节

在UML文献中,用例规格说明有许多不同的形式。大多数都包括以下信息:用例的名称,关于他的目的一段简短描述,乐观流程(即如果一切正常,用例中发生的事件流),一个或多个更实际的流程(即事情没有按照愿望发生时的流程)

      2. 用例规格说明示例

 

 

 

 

总之,用例规格说明不应该太长----它应该只有几页。如果规格说明非常长,就应该考虑一下这个用例做的事情是否太多了,有可能他实际上是多个用例,另外,处于实际的原因,也不能包含所有会触发可选流程的事情,而只要包含最重要,最关键的可选流程。不要包含所有可能的出错条件,诸如操作者输入的数据格式错误的情况(让用户界面去处理这类异常) 

    5.5.4 高级概念:<<include>>和<<extend>>关系

      1. 《include》关系

View Report用例是其他两个用例需要的共同功能。这可以利用《include》关系在用例模型中描述

      2.《extend》关系

在执行Manager Garden时,View Reports并不是必需的,Manage Gradern本身就是完整的。

 

      3.《include》和《extend》关系的危险

 

    5.5.5 高级泛化:泛化

泛化关系也可以被用于组织用例。和类一样,用例也可以有一些共同的行为,其他用例(即子用例)可以通过添加步骤或精华其他方面来修改这些共同行为

 

Purchase Ticket包含了购买任何门票所需的基本步骤,而子用例针对具体门票的种类对Purchase Ticket进行了特化 

  5.6 活动图

活动图提供了活动流程的可视化描述,可以是在系统,业务,工作流或其他过程中。这些图关注被执行的活动以及谁(或什么)负责执行这些活动

活动图的元素包括动作节点,控制节点和对象节点。有三种类型的控制节点:初始和终止(终止节点有两个变例:活动终止和流程终止),判断和合并,分叉和结合 

    5.6.1 基本概念:动作

在活动图中,动作是行为的基本单元。活动可以包含许多动作,这就是活动图所展现的东西

    5.6.2 基本概念:开始和停止

既然活动图展示了一个处理流程,那么流程就必须有开始和结束的地方

    5.6.3 基本概念:判断节点和合并节点

判断和合并节点控制了活动图中的流程。

    5.6.4 基本概念:分区

活动图中的元素可以利用分区来分组。分区的目的是说明执行具体活动的责任。在业务模型中,分区可以是一个业务单位,部门或组织机构;对于系统来说,分区可以是其他系统或子系统;在应用建模中,分区可以是应用中的对象。每个分区都可以被命名,表示负责者 

    5.6.5 高级概念:分叉,结合和并发

    5.6.6 高级概念:对象流

    5.6.7 高级概念:其他元素

  5.7 类图

在系统的逻辑视图中,类图用于表示类和它们之间的关系。单张类图表示了系统类结构的一个视图。在分析时,我们利用类图来说明实体共同的角色和责任,这些实体提供了系统的行为。在设计时,我们利用类图来记录类的结构,这些类构成了系统的架构

类图中的两个基本元素是类和它们的基本关系

    5.7.1 基本概念:类表示法

 

对于特定的类图,显示一个类的某些属性和操作是有用的。我们说“某些”是因为,对于凡是具有一点重要性的类,在一张类图中显示它的所有属性既不方便,也不必要。

    5.7.2 基本概念:类关系

类很少是独立的,相反,它们会通过不同的方式与其他类协作。类之间的基本联系包括关联,泛化,聚合和组合

关联图标连接了两个类,体现了一种语义联系。关联通常用名词词组来标注,以说明关系的实质。类可能与它自己有关联(称为自关联),如PlanAnalyst类的实例之间的协作

 

其他三个基本类关系是对一般关联图标的精化。实际上,在开发过程中,这正是关系演变的一种趋势。我们先断定两个类之间存在语义上的联系,然后随着具体地确定它们之间联系的实质,常常会将关联精化为泛化,聚合和组合关系 

泛化图标表示一种泛化/特化关系,表现为带有封闭箭头的关联。箭头指向超类,关联的另一端是子类

 

聚合图标表明一种整体/部分层次结构,也意味着能够从聚合体导航到它的部分。它表现为带有一个空心菱形的关联,菱形所在的一端是聚合体(整体)。另一端的类代表它的实例构成了聚合对象的部分

 

组合图标表示一种包容关系,表现为带有一个实心菱形的关联,菱形所在的一端是整体。在这一端的多重性是1,因为根据定义,部分在整体之外就么没有任何意义,整体拥有部分,部分的生命周期与整体是一样的

    5.7.3 高级概念:模板(参数化)类

某些面向对象编程语言,如C++,提供了模板(参数化)类。模板类代表了一个类家族,它们的结构和行为与形式化的类参数是分别定义的。我们必须将这些形式化的参数映射到某一个具体的类(绑定的过程),然后才能得到这个类家族中的一个具体的类。所谓具体的类,指的是它能够拥有实例

    5.7.4 高级概念:可见性

 

关联的方向性。在分析时,我们认为关联是分析类之间的双向逻辑连接。在设计时,我们将关注的焦点转到关联的导航性上。从GrainCrop类到GrainYieldPredictor类的单向关联通常意味着GrainCrop类的某些方法在实现时使用了GrainYieldPredictor类的服务 

    5.7.5 高级概念:关联端名称和限定符

    5.7.6 高级概念:约束

    5.7.7 高级概念:关联类和注解

  5.8 序列图

作为一种通信图,序列图用于跟踪在同一个上下文环境中一个场景的执行。实际上,序列图在很大程度上就是通信图的另一个表现形式 

    5.8.1 基本概念:对象与交互

 

    5.8.2 基本概念:生命线与消息

 

    5.8.3 高级概念:销毁事件

 

    5.8.4 高级概念:执行说明

 

    5.8.5 高级概念:交互使用

 

    5.8.6 高级概念:控制结构

 

  5.9 交互概念图

 

    5.9.1 基本概念:框

 

    5.9.2 基本概念:控制流元素

 

    5.9.3 基本概念:交互图元素

 

  5.10 组合结构图

 

    5.10.1 基本概念:组合结构的部分

 

    5.10.2 基本概念:组合结构的部分与接口

 

    5.10.3 基本概念:组合结构连接器

 

    5.10.4 高级概念:协作

 

  5.11 状态机图

    

    5.11.1 基本概念:初始状态,最终状态和简单状态

 

    5.11.2 基本概念:转换和事件

 

    5.11.3 高级概念:状态活动----入口活动,执行活动和出口活动

 

    5.11.4 高级概念:控制转换

 

    5.11.5 高级概念:复合状态和嵌套状态

 

    5.11.6 高级概念:并发与控制

 

    5.11.7 高级概念:子状态机状态

 

    5.11.8 高级概念:其他状态机图元素

 

  5.12 时间图

 

    5.12.1 基本概念:更多相同之处

 

    5.12.2 基本概念:布局

 

    5.12.3 基本概念:事件

 

    5.12.4 基本概念:约束

 

    5.12.5 高级概念:另一种表示形式

 

    5.12.6 高级概念:事件与消息

 

  5.13 对象图

 

    5.13.1 基本概念:对象

 

    5.13.2 基本概念:对象关系

 

    5.13.3 高级概念:端点名称和限定符

 

  5.14 通信图

 

    5.14.1 基本概念:对象,链接和消息

 

    5.14.2 基本概念:顺序表达式

 

    5.14.3 高级概念:消息与同步

 

    5.14.4 高级概念:迭代子句和警戒条件

 

  5.15 小结

 

第6章 过程

  6.1 首要原则

 

    6.1.1 成功项目的特征

 

    6.1.2 追求理性的开发过程

 

  6.2 宏观过程:软件开发生命周期

 

    6.2.1 概述

 

    6.2.2 宏观过程的内容维:科目

 

    6.2.3 宏观过程的时间维:里程碑和阶段

 

    6.2.4 宏观过程的时间维:迭代

 

    6.2.5 发行计划

 

  6.3 微观过程:分析与设计过程

 

    6.3.1 概述

 

    6.3.2 抽象层次

 

    6.3.3 活动

 

    6.3.4 产品

 

    6.3.5 微观过程与抽象层次

 

    6.3.6 识别元素

 

    6.3.7 确定元素间的协作

 

    6.3.8 确定元素间的关系

 

    6.3.9 详细确定元素的语义

 

  6.4 小结

 

第7章 实战

  7.1 管理和计划

 

    7.1.1 风险管理

 

    7.1.2 任务计划

 

    7.1.3 开发评审

 

  7.2 人员配置

 

    7.2.1 资源配置

 

    7.2.2 开发团队角色

 

  7.3 发布版本管理

 

    7.3.1 配置管理和版本控制

 

    7.3.2 集成

 

    7.3.3 测试

 

  7.4 复用

    

    7.4.1 复用的元素

 

    7.4.2 建立复用制度

 

  7.5 质量保证和度量

 

    7.5.1 软件质量

 

    7.5.2 面向对象度量

 

  7.6 文档化

 

    7.6.1 工具种类

 

    7.6.2 组织上的意义

 

  7.7 工具

 

    7.7.1 工具种类

 

    7.7.2 组织上的意义

 

  7.8 特殊主题

 

    7.8.1 领域特定问题

 

    7.8.2 采纳面向对象技术

 

  7.9 面向对象开发的好处和风险

 

    7.9.1 面向对象开发的好处

 

    7.9.2 面向对象开发的风险

 

  7.10 小结

 

第8章 系统架构----基于卫星的导航(聚焦于系统架构)

  8.1 先启 

开发系统架构的第一个步骤实际上是系统工程步骤,而不是软件工程步骤,即使对于纯软件系统或绝大部分是软件的系统来说也是这样系统工程被国际系统工程委员会(INCOSE)定义为“使得系统能够成功实现的跨学科方法和手段”。INCOSE进一步定义系统架构(这是我们在这里关注的焦点)为“布置元素和子系统,并给它们分配功能“,以满足系统需求

在这里关注的焦点是,通过定义问题的边界决定必须为顾客建造什么,决定使命用例,然后通过分析使命用例之一来决定一个系统用例子集。在这个过程中,我们从功能需求开发用例,并非对功能需求和约束归档。但在跳到需求分析之前,请先阅读一下补充材料”全球定位系统介绍“

    8.1.1 卫星导航系统的需求

建造系统来帮助解决顾客问题的过程,开始于决定我们必须建造什么。第一步是阅读顾客给我们的任何陈述问题或需要的文档。对于系统来说,我们已经得到了一份和高级别需求及约束相关联的愿景陈述

 

显然,这是一个高度简化的需求陈述,但它确实提供了基于卫星的导航系统非常基本的规格。实践上,像这么大一个系统的详细需求只有在展示解决方案的活力,经过涉及众多领域专家和系统最终用户及客户参与的几百个人月的分析之后,才能得到。最终,一个大系统的需求可能包含数千页的文档(希望是可视的模型),不只阐明系统的通用行为,而且还包含错综复杂的细节,如用于人/机交互的屏幕布局 

    8.1.2 定义问题的边界

虽然只是最小的需求和约束,但它们确实允许我们迈出设计卫星导航系统架构的重要的第一步----定义它的上下文,如图8-1所示。上下文图可以让我们清晰理解环境内SNS必须提供的功能。执行者代表和系统交互的外部实体,包括人,提供服务的其他系统和现实环境。依赖箭头展示了外部实体依赖于SNS,还是SNS依赖于它

 

显而易见,User(用户),Operator(操作员)和Maintainer(维护人员)执行者依赖于SNS,分别使用它的导航信息,操作它和维护它。虽然卫星导航系统有能力生成它自己的电源作为地面系统的备份,主电源服务还是由外部系统ExternalPower(外部电源)执行者提供。类似地,ExternalCommunication(外部通信)执行者为SNS提供购买的通信服务,在某些情况下充当首要服务,在另一些情况下,作为内部提供的系统通信的备份。我们给这两个执行者的名字加上前缀“External(外部)”,以清晰地把它们和内部的系统电源及通信服务分开

剩下的执行者Atmosphere/Space(大气/空间),可能看起来相当奇怪,直到我们把它看作卫星导航系统的地面和空基资产之间的通信的传输介质。因此,它是一个服务提供者。它的状态当然影响这些通信的质量。关于这个执行者,另一个方法是从约束“兼容国际标准”的视角来看。众多的国家和国际规范及条约管制着卫星传输,因此,我们有重要的理由来阐述这个执行者

上下文图的一个关键点是系统的确切边界,即什么在系统里面,什么不在里面。

细节不重要,重要的是开发团队选择一种风格,对它归档,遵循它,以保证其清晰和容易理解。但是,我们更喜欢展示一张上下文图,因为它简单和清楚地传达了高级别概念:系统是与它的外部环境实体交互的功能的容器。在这些交互中,系统给一些实体提供服务,从另一些实体接收服务,在开发的开始,理解这一点至关重要

除了功能需求之外,我们还有高级别的非功能需求,它们适用于部分功能能力或整个系统。这些非功能需求包括可靠性,精度,冗余,自动化,可维护性,可扩展性和服务生命期。我们也看到开发SNS的一些设计约束,它也被用于维护适用于多于一个用例的功能需求。另一个此时必须开始的关键文档是词汇表,开发团队在术语定义上取得一致并相应地使用它们,是非常重要的

即使从这些高度简略的系统需求中,也可以对开发卫星导航系统的过程给出两点意见:

  1. 架构必须允许随时间进化
  2. 实现必须依赖于已有的标准,以达到最大程度的可行性

显然,我们不能在一章里实现对卫星导航系统(或仅是架构)完整的分析或设计,即使是一本书也远远不够。既然我们的意图是探索表示法和过程如何放大到系统架构的开发,那么就聚焦于设计第一和第二个级别架构,分别定义组成片段和子系统。我们通过对Operator执行者使用的功能进行逻辑上的分区来开发这些架构级别

在审查愿景和需求之后,我们(架构团队)认识到,提供给我们的功能需求实际上是众多使命级别用例的容器(在UML中就是包),这些用例定义卫星导航系统必须提供的功能。这些使命用例包给我们提供SNS的高级别功能上下文,如图8-2所示。这些包包含展示SNS的用户,操作员和维护人员如何和要实现的系统交互以完成其使命的使命用例。既然我们在使用面向对象分析设计技能和UML2.0表示法来执行一个系统工程而不是一个软件工程任务,那么对于如何使用图8-2展示的表示法,我们可能有点陌生。但是,我们相信它清晰展示了所需信息,并确保了可理解性 

    8.1.3 决定使命用例

系统的愿景陈述相当开放:为我们的顾客提供高效和买得起的卫星导航系统服务。因此,架构师的任务是需要明智地修剪问题空间,使得问题可以解决。类似这样的问题容易遭受分析瘫痪,因此我们必须聚焦于提供最通用的导航服务,而不是视图使导航系统变成一个什么都有的大杂烩(结果是不能为任何人提供有用的功能)。我们将从开发SNS的使命用例开始

像这样的大项目通常围绕一些小的,位于中心的负责建立总体系统架构的团队来组织。把实际的开发工作外包给其他公司或同一公司内的不同团队。甚至在分析期间,系统架构师通常会考虑一些划分实现元素的概念模型。基于我们建造,操作和维护基于卫星的系统的经验,我们相信最高级别的逻辑架构包括4个片段:地面,发射,卫星和用户

有人可能争辩说,这是设计,不是分析,但我们反驳说一个人必须开始在某些点上约束设计空间。实际上,确定这个逻辑架构代表系统需求还是系统设计是困难的。抛开这个问题,这个开发阶段的系统架构主要是面向对象的。例如, 架构展示了复杂的对象,如地面片段和卫星片段,每一个执行系统的主要功能。这正如在第4章讨论的,大系统中,在最高级别的抽象的对象倾向于沿着主要的系统功能线聚集。如何识别和精化这些对象,在分析期间和在设计期间所做的稍有不同

甚至在有一个如图8-3所示的包图级别的概念架构之前,我们就可以开始分析,和领域专家一起工作,来清晰描述系统所需行为的首要使命用例。说”甚至在这之前“是因为,即使我们有SNS架构的一个表示法,也应该从黑盒视角开始我们的分析,这样就不会不必要地约束它的架构。即,首先分析所需功能来决定SNS的使命用例,而不是单个SNS片段的。然后,我们以卫星导航系统的白盒视角,分配这个用例功能到某个片段

 

“活动图有助于理解系统的高级执行行为,而不涉及协作图所需的消息传送内部细节”。因此,活动图是我们分析使命用例,阐述所期待的系统行为的首要工具。 一些开发团队使用序列图或通信图来达到此目标,但我们相信,那些图更适合低级别架构层次的设计工作。在分析时,我们只关注于使命用例的成功场景,而把众多的备选场景留到以后

术语成功场景(success scenario)读者可能不熟悉,众所周知的ATM例子帮我们解释了这一点。ATM的一个用例是Withdraw Cash(提取现金),这通常是我们在这些机器上所要做的。提取现金时,我们通过很多不同步骤和ATM交互:插卡,输入PIN,选择取款,选择金额,等等。但这些步骤都没有体现最终目标(提取现金),因此它们本身不是真正的用例。 Withdraw Cash 用例(和所有用例)包含很多不同的场景,每一个场景代表用例功能的一条路径。第一个是成功地提取现金,因此术语成功场景也叫作主要场景。备选或次要场景通常处理从主要场景分支的场合。例如,沿着主要场景Withdraw Cash往下走,到达选择所需金额这一点。ATM回应所要求的金额超过了每天允许的金额,并请求我们选择另一个金额,我们照做,得到现金(虽然比我们最初想要的少)。这就是对一个次要场景的阐述。它跟随首要(成功)场景路径的若干步骤,然后偏离轨道去处理金额问题,然后跳回主要场景

对于实时系统,如卫星导航系统,要明白它的很多功能是在次要场景中体现的。这些可能被认为是冰山在水面下的一部分,然而其对系统完整和安全的操作很关键。隐藏在幕后的次要场景通常得到关注较少,但它们可以损坏系统,就像冰山的水下部分能够把船撞沉。简言之,我们的分析必须包括次要场景。次要场景中包含的系统功能数量会有不同,但通常在这样的系统中是比较重要的。在这里我们不考虑它----但是,我们必须认识到真实的系统开发工作量

现在,回到手边的任务,开发OperateSNS使命用例包的使命用例。基于对SNS总体操作的分析,定义4个相应的使命用例

  • Initialize Operations(初始化操作)
  • Provide Normal Operations(提供正常操作)
  • Provide Special Operations(提供特别操作)
  • Terminate Operations(终止操作)

    8.1.4 决定系统用例

我们聚焦于SNS,把它当作一个黑盒,我们不能看到里面,只能看到它提供什么服务,而看不到它如何提供服务。在分析系统的高级别执行行为时,我们对跨越操作员和卫星导航系统之间边界的控制流感兴趣

既然我们关心由SNS执行的活动,而不是通信或序列图里表达的消息传送,活动图是相对简单的。如果要定义整个SNS的系统活动,我们将为它的每个使命用例执行基于活动图的分析,以发现卫星导航系统必须执行以满足需求的众多活动

 

 

  8.2 精化 

    8.2.1 开发一个好的架构

好的架构,不管是系统的还是软件的,通常有若干共同的属性。

  • 它们由定义良好的抽象层构成,每一层代表一个一致的抽象;它们通过定义良好的受控的接口提供,在同样定义良好和受控的低级别的抽象设施上建造
  • 每一层的接口和实现之间有清晰的分离关注,使得改变一层的实现不会侵犯它的客户所做的假设
  • 架构是简单的----通过共同的抽象和机制达到共同的行为 

    8.2.2 定义架构开发活动

第6章展示的分析和设计微观过程定义了一个开发活动的集合,这些开发活动在系统内每个抽象级别执行。活动一般被定义为卫星导航系统开发系统架构必需的系统工程任务,这里从我们所聚焦的角度重新描述如下:

  • 识别给定抽象级别的架构元素,进一步建立问题边界和开始面向对象分解
  • 识别元素的语义,即建立它们的行为和属性
  • 识别元素之间的关系以固化它们的边界和协作者
  • 指定元素接口,然后精化它们,准备下一个抽象级别的分析

这个活动集合使得开发SNS架构时的首要关注点十分清晰,就是定义SNS架构的元素(片段和子系统),它们的责任,它们的协作和它们的接口。这些提供给架构师一个框架,来演进架构和探索备选的设计 

    8.2.3 验证所建议的系统架构

 

    8.2.4 分配非功能需求和阐明接口

 

    8.2.5 规定系统架构及部署

 

    8.2.6 分解系统架构

 

  8.3 构造

 

  8.4 后移交

 

    8.4.1 添加新的功能

 

    8.4.2 改变目标硬件

 

第9章 控制系统----交通管理(聚焦于系统需求)

  9.1 先启

 

    9.1.1 列车交通管理系统的需求

 

    9.1.2 决定系统用例

 

  9.2 精化

 

    9.2.1 分析系统功能

 

    9.2.2 定义TTMS架构

 

    9.2.3 从系统工程到硬件和软件工程

 

    9.2.4 关键抽象和机制

 

  9.3 构造

 

    9.3.1 消息传送

 

    9.3.2 列车日程计划

 

    9.3.3 显示信息

 

    9.3.4 传感器数据采集

 

    9.3.5 发布版本管理

 

    9.3.6 系统架构

 

    9.3.7 子系统规格

 

  9.4 后移交

 

第10章 人工智能----密码分析(聚焦于分析)

  10.1 先启

 

    10.1.1 密码分析需求

 

    10.1.2 定义问题的边界

 

    10.1.3 黑板框架的架构

 

    10.1.4 知识源的分析

 

  10.2 精化

 

    10.2.1 黑板对象

    

    10.2.2 依赖和认定

 

  10.3 构造

 

    10.3.1 设计黑板对象

 

    10.3.2 设计知识源

 

    10.3.3 设计控制器

 

    10.3.4 集成黑板框架

 

    10.3.5 添加新的知识源

 

  10.4 后移交

 

    10.4.1 系统增强

 

    10.4.2 改变需求

 

第11章 数据采集----气象监测站(聚焦于分析到初步的设计)

  11.1 先启

 

    11.1.1 气象监测站需求

 

    11.1.2 定义问题的边界

 

    11.1.3 场景

 

  11.2 精化

 

    11.2.1 气象监测系统用例

 

    11.2.2 架构框架

 

  11.3 构造

 

    11.3.1 帧机制

 

    11.3.2 发布计划

 

    11.3.3 传感器机制

 

    11.3.4 显示机制

 

    11.3.5 用户界面机制

 

  11.4 移交

 

第12章 Web应用----休假跟踪系统(聚焦于详细设计和实现)

  12.1 先启

 

    12.1.1 需求

 

    12.1.2 用例模型

 

  12.2 精化

 

    12.2.1 部署视图

    

    12.2.2 逻辑视图

 

    12.2.3 进程视图

 

    12.2.4 实现视图

 

    12.2.5 用例视图

 

  12.3 构造

 

    12.3.1 用户体验模型

 

    12.3.2 分析和设计模型

 

    12.3.3 实体

 

    12.3.4 控制器

 

    12.3.5 Web页面和用户界面

 

  12.4 移交和后移交

 

附录A 面向对象编程语言

  A.1 语言进化

 

  A.2 Smalltalk

 

    A.2.1 概述

 

    A.2.2 例子

 

    A.2.3 参考文献

 

  A.3 C++

    

    A.3.1 概述

 

    A.3.2 例子

 

    A.3.3 参考文献

 

  A.4 Java

 

    A.4.1 概述

 

    A.4.2 例子

 

    A.4.3 参考文献

    

附录B 进一步阅读

   

注解

 

术语表

  A

abstract class(抽象类)没有实例的类。编写一个抽象类,是希望它的具体子类给它添加结构和行为,通常通过实现它的抽象操作来达到该目的

abstract operatin(抽象操作)由一个抽象类声明但不实现的操作

abstraction(抽象)对象的本质特征,把它从所有其他种类的对象区分出来,这样,相对于查看者的视角,可以提供清晰定义的概念边界,聚焦于对象本质特征的过程。抽象是对象模型的基础元素之一

 

分类书目

 

posted on 2019-07-10 10:59  void87  阅读(771)  评论(0编辑  收藏  举报

导航