3.1 面向对象基本知识
对问题域的正确认识是软件开发工作的首要任务。
面向对象的分析(OOA)、面向对象的设计(OOD)和面向对象的程序设计(OOP)的关系如下:
- OOA和OOD采用一致的概念、原则和表示法,两者之间不存在鸿沟,不需要从分析文档到设计文档的转换。两者之间也不强调严格的阶段划分;
- OOA与OOD可适应多种软件生命周期模型;
- OOP又称面向对象的实现(00I),是建立在OOA与OOD的前提下。
3.2 面向对象的基本原则
面向对象方法是一种运用对象、类、继承、封装、聚合、消息传递、多态性等概念来构造系统的软件开发方法。
3.2.1 抽象
抽象的意义:
- 尽管问题域中的事物很复杂,但分析员并不需要了解和描述它们的全部特征;
- 通过舍弃个体事物在细节上的差异,抽取其共同特征而得到一批事物的抽象概念。
3.2.2 封装
将数据成员和成员函数结合为一个不可分的系统单位,并尽可能地隐蔽对象的内部细节。封装的信息隐蔽作用反映了事物的相对独立性。 它实际上也是抽象原则的一种具体应用 它既体现了过程抽象,也体现了数据抽象
3.2.3 继承
继承是一种连接类与类之间的层次模型。继承是指特殊类的对象拥有其一般类的属性和行为。通过继承可以达到以下目的:
- 派生类比不使用继承直接进行描述的类更加简洁;
- 能够重用和扩展现有类库资源;
- 使软件易于维护和修改。
面向对象的一个非常重要的基本特征是引入了泛化( Generalization)的概念 ,泛化与继承的概念非常相似,但在分析和设计阶段,更多地使用泛化这个术语。 都可以用来描述两个对象或类之间的关系,但它们考虑的出发点有一定的区别,泛化也称为概括。 使用泛化主要有如下两个目的:
- 后代的实例可以用于任何祖先被声明使用的地方。
共享对祖先的描述,并允许对元素进行增量描述,这被称之为继承。
3.2.4 分类
分类(Classification)就是把具有相同数据成员和成员函数的对象划分为一类,分类原则实际上是抽象原则运用于对象描述时的一种表现形式; 运用分类原则也意味着通过不同程度的抽象而形成基类一派生类结构(又称分类结构); 运用分类原则可以集中描述对象的共性,清晰地表示对象与类的关系(即“is a”关系)以及派生类与基类的关系(即“is-a-kind-of”关系)。
3.2.5 多态
多态是面向对象设计中的一种机制,可分为编译时的多态性和运行时的多态性。通过多态,一个通用接口就可以实现不同的行为特征。
继承性和多态性的结合,可以生成一系列虽类似但独一无二的对象。由于继承性,对象可共享许多相似的特征;由于多态性,针对相同的消息,不同对象可以拥有独特的表现方式,实现个性化的设计。3.2.6 聚合
OOA的聚合原则是把一个复杂的事物看成若干比较简单的事物的组装体,从而简化对复杂物的描述。 “聚合”有时又称为“包含”,在OOA中运用聚合原则就是要区分事物的整体和它的组成部分,分别用整体对象和部分对象来进行描述,形成一个整体—部分结构,以清晰地表示它们之间的组成关系(称为“has-a”关系,或反过来称为“is-a-part-of”关系)。
3.2.7 关联
在OOA中运用关联原则就是在系统模型中明确地表示对象之间的静态联系。
3.2.8 消息通信
消息通信这一原则要求对象之间只能通过消息进行通信,而不允许在对象之外直接地存取对象内部的数据。通过消息进行通信是由于封装原则而引起的。在OOA中要求用消息连接表示出对象之间的态联系。
3.2.9 粒度控制
人在面对一个复杂的问题域时,需要控制自己的视野,即考虑全局时,注重其大的组成部分,暂时不详查每一部分的具体细节;考虑某部分的细节时,则暂时撇开其余的部分。这就是粒度控制(Scale Controlling)原则。
3.2.10 行为分析
由大量事物所构成的问题域中各种行为往往相互依赖、相互交织。 控制行为复杂性的原则有以下几点:
确定行为的归属和作用范围;
- 认识事物之间行为的依赖关系;
- 认识行为的起因,区分主动行为和被动行为;
- 认识系统的并发行为;
-
3.3 面向对象设计原则
3.3.1 分治
软件系统分解为子系统
分布式系统可以分解为客户机和服务器;
- 系统可以分解为一系列子系统;
- 子系统可以分解为一个或多个包;
- 包可以分解为类;
-
3.3.2 增加内聚
不同内聚类型:优先级从高到低排序
功能内聚:模块只执行单一计算并返回结果,没有副作用。如函数过程。
- 层内聚:相关服务放在一起,并有严格的层次结构,高层服务可访问低层服务,反之不可。如分层结构。
- 通信内聚:访问或操作同一数据的过程放在一个类中,这些过程可以互相通信。如某个类设计。
- 顺序内聚:存在一系列过程,其中一个过程向另一个过程提供输入,这些过程放在一起,形成顺序内聚。如消息序列。
- 过程内聚:几个一次调用的过程放在一起,但其中一个过程的输出不一定是另一个过程的输入,形成过程内聚。如调用结构。
- 时间内聚:程序执行过程中同一阶段内完成的操作放在一起,达到时间内聚。
实用程序内聚:逻辑上不能纳入其他内聚类型的相关实用程序放在一起,形成实用程序内聚。如可复用的过程或类。
3.3.3 降低耦合
模块间存在相互依赖关系即为耦合。不同耦合类型从高向低排列有:
内容耦合:一个构件在不被察觉的情况下修改另一个构件内部的数据,应始终避免。
- 公共耦合:一组构件使用了全局数据,就产生公共耦合。应通过封装降低公共耦合。
- 控制耦合:一个过程通过标志、开关或命令显式地控制另一个过程的动作,就产生控制耦合。降低的方法是采用多态操作。
- 标记耦合:在一个操作的参数表中将类作为参数,就产生标记耦合。降低标记耦合的方法可以传递简单变量或使用接口做参数。
- 数据耦合:在一个操作的参数表中用简单变量或简单的类(如string)作为参数,就产生数据耦合。应通过减少参数个数降低耦合。
- 例程调用耦合:一个例程(或类操作)调用另一个例程,就产生例程调用耦合。如果出现例程调用序列,降低的方法是编写一个例程将这个调用序列封装起来。
- 类型使用耦合:类将实例变量或本地变量声明为另一个类时,就产生类型(嵌套)使用耦合。降低该耦合的方法是将变量的类型声明为包含所需操作的最通用的类或接口。
- 包含/引入耦合:当一个构件引入(import)一个包时就产生引入耦合,当一个构件包含(include)另一个构件时,就产生包含耦合。
外部耦合:模块对外部系统,如操作系统、共享库或硬件有依赖关系时就产生外部耦合。可通过信息隐蔽减少这种依赖关系。
3.3.4 提高抽象层次
设计应隐藏或推迟考虑细节以降低复杂性。 类是包含过程抽象的数据抽象。
父类和接口可进一步提高抽象层次。
- 类中公有操作越少,抽象程度越高。
- 类中所有变量都是私有,抽象程度达到最高。
抽象可确保在设计时不必关心不必要的细节,能把握问题的本质并做出重要的决策。
3.3.5 提高可复用性
可以在算法、类、过程、框架和完整应用程序的级别上创建可复用性。 复用构件的机制包括过程调用和继承父类。
3.3.6 复用已有的设计和代码
复用已有的设计是对可复用性设计的补充。通过复用可从以往对可复用构件的投资中获益。
3.3.7 灵活性设计
积极预测将来可能在实现和功能上的变化,并为此采取相应措施。在设计中引入灵活性的方法有:
- 降低耦合并提高内聚(易于提高替换能力);
- 建立抽象(创建有多态操作的接口和父类);
- 不要将代码写死(消除代码中的常数);
- 抛出异常(由操作的调用者处理异常);
-
3.3.8 预计过期
积极预测将来可能在技术和运行环境上的变化,并为此采取相应措施。在设计中应遵循的预计过期的规则有:
避免使用早期发布的技术;
- 避免使用针对特定环境的软件库;
- 避免使用软件库中未编档的或很少使用的功能;
- 避免使用小公司或可能不提供长期支持的公司提供的可复用构件或特殊硬件;
-
3.3.9 可移植性设计
可移植性设计的主要目标是让软件在尽可能多的平台上运行。实现可移植性的规则有:
避免使用特定环境的专有功能;
- 使用不依赖特定平台的程序设计语言;
- 小心使用可能依赖某一平台的类库;
-
3.3.10 可测试性设计
设计时采取措施使得测试易于进行。可测试性设计的最重要的方法是保证代码的所有功能都能脱离图形用户界面执行。
3.3.11 防御性设计
为提高可靠性,应确保不引入任何缺陷,能够处理其他代码不适当使用构件引起的问题。 按契约设计是防御性设计技术,其核心思想:
被调用操作为正常执行必须满足的前置条件(precondition):调用操作在调用一个操作时有责任确保该操作的前置条件成立。
- 被调用操作正常执行所得到的结果即为后置条件 (postcongdition):要求被调用操作在返回前有责任保证这些后置条件成立。、
- 被调用操作在执行时确保不会被修改的不变量(invariant)。