天天看点

4.2 面向复用的软件构造技术

大纲

▪ 设计可重用类

-继承和重写

-重载

-参数多态性和泛型编程

-行为子类型和Liskov替换原则(LSP)

-组合和委托

▪ 设计系统级可重用库和框架

——API和库

——框架

——Java集合框架(示例)

1设计可重用类

在OOP中设计可重用类

▪ 封装与信息隐藏

▪ 继承和重写

▪ 多态性、子类型化和重载

▪ 通用程序设计

▪ 行为亚型与Liskov替代原理(LSP)

▪ 授权和组成

行为亚型

▪ 子类型多态性:客户端代码可以统一处理不同类型的对象。

–如果类型Cat是动物的一个亚型,那么在使用类型Animal的表达式的任何地方都可以使用类型Cat的表达式。

▪ 设q(x)是关于T类型的对象x可证明的属性,那么q(y)应该是S类型的对象y可证明的,其中S是T的子类型。

行为亚型

▪ Java中编译器强制的规则(静态类型检查)

-子类型可以添加,但不能删除方法

-具体类必须实现所有未定义的方法-重写方法必须返回相同的类型或子类型

-重写方法必须接受相同的参数类型

-重写方法不能引发其他异常

▪ 也适用于指定的行为(方法):

-相同或更强的不变量

-相同或较弱的前提条件

-相同或更强的后置条件

行为子类型(LSP)的示例1

▪ 子类满足相同的不变量(以及附加的不变量)

▪ 重写的方法具有相同的前置和后置条件

4.2 面向复用的软件构造技术

行为子类型(LSP)的示例2

▪ 子类满足相同的不变量(以及附加的不变量)

▪ 重写的方法start具有较弱的前置条件

▪ 重写方法brake具有更强的后置条件

4.2 面向复用的软件构造技术

Liskov替代原理(LSP)

▪ LSP是子类型关系的一个特殊定义,称为(强)行为子类型▪ 在编程语言中,LSP依赖于以下限制:

–子类型中不能加强前置条件。

–后条件在子类型中不能减弱。

–子类型中必须保留父类型的不变量。

–子类型中方法参数的逆变

–子类型中返回类型协变。

–子类型的方法不应引发新异常,除非这些异常本身是父类型的方法引发的异常的子类型。

4.2 面向复用的软件构造技术

▪ 更具体的类可能有更具体的返回类型

▪ 这称为子类型中返回类型的协变

4.2 面向复用的软件构造技术

▪ 为子类型的方法声明的每个异常都应该是为父类型的方法声明的某个异常的子类型。

4.2 面向复用的软件构造技术

▪ 在逻辑上,它被称为子类型中方法参数的协变。▪ 这在Java中实际上是不允许的,因为这会使重载规则复杂化。

4.2 面向复用的软件构造技术

协方差与反方差

▪ 数组是协变的,给定Java的子类型规则,T[]类型的数组可以包含T类型的元素或T的任何子类型。

▪ 在运行时,Java知道这个数组实际上被实例化为一个整数数组,它只是碰巧通过一个类型为Number[]的引用来访问。

泛型的LSP

▪ 泛型是类型不变的–ArrayList是List的子类型

–List不是List的子类型

▪ 编译完代码后,编译器将丢弃类型参数的类型信息;因此在运行时此类型信息不可用。

▪ 此过程称为类型擦除

▪ 泛型不是协变的。

Java在运行时,为所有对象维护一 个运行时类型标识,这个标识跟踪对象所属的类,用来确定选择哪个方 法运行。保存这些信息的类叫做“Class类型类”。注意:Class是类的名 字,不是关键词class。每个”Class”的对象描述了一个类的信息

▪ 获取类类型的对象有三种方法

4.2 面向复用的软件构造技术

什么是类型擦除?

虚拟机中没有泛型类型对象-所有对 象都属于普通类!

泛 型信息只存在于编译阶段,在运行时会被”擦除”

定义泛型类型时,会自动提 供一个对应的原始类型(非泛型类型),原始类型的名字就是去掉类型参数后的 泛型类型名。

擦除时类型变量会被擦除,替换为限定类型, 如果没有限定类型则替换为Object类型。

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

运行时类型查询仅适用于原始类型

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

我们不能认为整数列表是数字列表的子类型。

对于类型系统,这将被认为是不安全的,编译器会立即拒绝它。

▪Box 不是Box 的子类型,即使Integer是Number的子类型。 ▪给定两个具体的类型A和B(例如,Number和Integer),无论A和B是否相关,MyClass 与MyClass

没有关系。 MyClass 和MyClass

的公共父对象是Object。

▪有关在类型参数相关时如何在两个通用类之间创建类似子类型的关系的信息,请参见通配符。

泛型中的通配符

▪使用通配符(?)指定无限制的通配符类型,例如List <?>。 无限定通配符?

–这称为未知类型列表。

▪在两种情况下,无界通配符是一种有用的方法:

–如果要编写一种可以使用Object类提供的功能实现的方法。 –当代码使用通用类中不依赖于类型参数的方法时。

例如,List.size或List.clear。

–实际上,经常使用Class <?>,因为Class 中的大多数方法都不依赖于T。

▪下限通配符:<? super A>下限通配符

– List List <? super Integer>

–前者仅匹配Integer类型的列表,而后者则匹配作为Integer的超类型的任何类型的列表,例如Integer,Number和Object。 ▪上界通配符:<? extendsA>上限通配符

–列表<? extends Number>

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

这是一个助记符(记忆术),即PECS(也称为Get and Put Principle),用于帮助记住要使用的通配符类型:producer-extends, consumer-super ▪为了获得最大的灵活性,请在代表生产者或生产者的输入参数上使用通配符类型。 消费者。 –如果参数化类型表示T生产者,请使用<? extendT>,例如get()–如果参数化类型表示T使用者,则使用<? superT>,例如add()

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

(2)委托与组合

4.2 面向复用的软件构造技术

接口比较器

▪int compare(T o1,T o2):比较其两个参数的顺序。 –比较功能,对某些对象集合施加总排序。 –比较器可以传递给排序方法(例如Collections.sort或Arrays.sort),以精确控制排序顺序。 比较器还可以用于控制某些数据结构(例如排序集或排序映射)的顺序,或为没有自然顺序的对象集合提供排序。

▪ 如果你的ADT需要比较大小,或者要放入Collections或Arrays中 进行排序,可实现Comparator接口并override compare()函数。

4.2 面向复用的软件构造技术

Interface Comparable

▪该接口对实现该接口的每个类的对象强加了总体排序。 ▪此排序称为类的自然排序,而该类的compareTo方法被称为其自然比较方法。

▪ 与使用Comparator的区别:不需要构建新的Comparator类,比较代 码放在ADT内部。

▪委派只是当一个对象依赖于另一个对象来实现其功能的某些子集时(一个实体将某物传递给另一个实体)

委派/委托:一个对象请求另一个对象的功能-例如 排序器将功能委派给某些比较器

▪明智的委派可实现代码重用委派是引用的一种常见形式–排序器可以按任意排序顺序重用–比较器可以与需要比较整数的任意客户端代码重用

▪委托可以 描述为在实体之间共享代码和数据的低级机制。

–显式委派:将发送对象传递给接收对象

–隐式委派:通过语言的成员查找规则

4.2 面向复用的软件构造技术

▪假设我们想要一个将其操作记录到控制台的列表…

– LoggingList由一个List组成,并将(非记录)功能委托给该List。

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

▪继承:通过新操作扩展基类或覆盖操作。 ▪委派:捕获操作并将其发送到另一个对象。 ▪许多设计模式结合使用继承和委托

4.2 面向复用的软件构造技术

用委派替换继承

▪问题:您有一个子类,该子类仅使用其超类的一部分方法(否则就无法继承超类数据)。 解决方案:创建一个字段并将一个超类对象放入其中,将方法委托给该超类对象,并摆脱继承。

▪本质上,此重构拆分了两个类,并使超类成为子类(而不是其父类)的助手。 –子类将仅继承将必需的方法委派给超类对象的方法,而不是继承所有超类的方法。

–一个类不包含任何从超类继承的不需要的方法。

4.2 面向复用的软件构造技术

复合超继承原理

▪或称为复合重用原理(CRP)–类应通过其组成(通过包含实现所需功能的其他类的实例)来实现多态行为和代码重用,而不是从基类或父类继承。

–组合对象可以执行的操作(has_a或use_a)比扩展其执行的操作(is_a)更好。 注:组合是委派的一种形式

▪委托可以看作是对象级别的重用机制,而继承是类级别的重用机制。”

▪Employee类具有一种计算员工年度奖金的方法:

4.2 面向复用的软件构造技术

▪雇员的不同子类:经理,程序员,秘书等可能希望覆盖此方法,以反映以下事实:某些类型的雇员比其他类型的雇员获得更多的奖金:

4.2 面向复用的软件构造技术

CRP示例

▪该解决方案存在多个问题。

–所有Manager对象获得相同的奖励。 如果我们想改变经理之间的奖金计算,该怎么办?

-要引入Manager的特殊子类?

4.2 面向复用的软件构造技术

–如果我们想更改特定员工的奖金计算,该怎么办? 例如,如果我们想将Smith从Manager提升为SeniorManager,该怎么办? 更改某人的经理的类型时如何处理?

–如果我们决定给予所有经理与程序员相同的奖金呢? 我们是否应该将计算算法从Programmer复制并粘贴到Manager?

▪问题是:每个雇主的奖金计算器可能会有所不同并且会经常更改。 –雇员和奖金计算器之间的关系应该在对象级别而不是类级别。

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

复合超继承原理

▪在继承之上实现组合的实现通常始于创建表示系统必须表现出的行为的各种接口。

▪构建实现已标识接口的类,并根据需要将其添加到业务域类中。

▪因此,无需继承即可实现系统行为

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

(1) Dependency: 临时性的delegation

▪使用类的最简单形式是调用其方法;

▪两个类之间的这种关系形式称为“使用关系”,其中一个类在不实际将其合并为属性的情况下利用另一类。 -它可以是例如参数,也可以在方法中本地使用。

▪依赖关系:一个对象之间需要其他对象(供应商)才能实现的临时关系。

4.2 面向复用的软件构造技术

(2) Association: 永久性的delegation

▪关联:对象类之间的持久关系,允许一个对象实例促使另一个对象实例代表其执行操作。 – has_a:一个类具有另一个作为属性/实例变量。通过成员变量–此关系具有结构性,因为它指定一种对象与另一种对象相连,并且不表示行为。

4.2 面向复用的软件构造技术

(3) Composition: 更强的association,但难以变化

▪组合是一种将简单对象或数据类型组合为更复杂对象的方法。 – is_part_of:一个类具有另一个作为属性/实例变量–实现为使一个对象包含另一个对象。

4.2 面向复用的软件构造技术

(4) Aggregation: 更弱的association,可动态变化

▪聚合:该对象存在于另一个对象之外,在外部创建,因此将其作为参数传递给构造函数。 - has_a

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

*2 设计系统级可重用的库和框架

▪库:提供可重用功能的一组类和方法(API)

构架

▪框架:可自定义到应用程序中的可重用框架代码。▪框架回调至客户代码–好莱坞原则:“不要打电话给我们。 我们会打电话给您。”

实践中的库和框架

▪定义关键抽象及其接口

▪定义对象交互和不变式

▪定义控制流

▪提供体系结构指导

▪提供默认值

▪ 之所以library和framework被称为系统 层面的复用,是因为它们不仅定义了1个 可复用的接口/类,而是将某个完整系统中 的所有可复用的接口/类都实现出来

▪ 并且定义了这些类之间的交互关系、调用关系,从而形成了系统整体 的“架构”

▪API:应用程序编程接口,库或框架的接口

▪客户端:使用API的代码

▪插件:用于定制框架的客户端代码

▪扩展点:框架通过插件支持扩展的地方 的“空白”,开发者开发出符合接口要求的代码(即插件),框架可调用,从而相当于开发者扩展了框架的功能。

▪协议:API和客户端之间预期的交互顺序。

▪回调: 框架将调用该插件方法以访问自定义功能

▪生命周期方法:根据插件的协议和状态按顺序调用的回调方法

(1)API设计

4.2重复使用的构造

为什么API设计很重要?

▪如果您进行编程,则您是API设计人员,并且API可能是您的最大资产之一,吸引外部门用户,提高声誉

–好的代码是模块化的

–每个模块都有一个API

–用户投入大量资源:获取,编写,学习

–关于API的思考提高了代码质量

–成功的公共API吸引了用户

▪也可能是您最大的责任

–不良的API可能会导致无休止的电话流

–可能抑制前进的能力

▪公开的API永远存在-一次正确的机会

-模块一旦拥有用户,就无法随意更改API

(1)API应该只做一件事并且做好

▪功能应该易于解释

–如果很难命名,那通常是一个不好的信号

–良好的名字会推动开发

–易于拆分和合并模块

▪ Good: Font, Set, PrivateKey, Lock, ThreadFactory, TimeUnit, Future

▪ Bad: – DynAnyFactoryOperations – _BindingIteratorImplBase – ENCODING_CDR_ENCAPS – OMGVMCID

(2)API应该尽可能小,但不能太小

▪API应该满足其要求

▪如有疑问,请忽略

–功能,类,方法,参数等

–您可以始终添加,但永远不能删除

▪概念权重比批量更重要

(3)实施不应影响API

▪API中的实现细节是有害的

–混淆用户

–禁止更改实现的自由

▪意识到什么是实现细节

–请勿过度指定方法的行为

▪例如:不要指定哈希函数–怀疑所有调整参数

▪不要让实现细节“泄漏”到API中

–序列化的形式,抛出的异常

▪最小化所有内容的可访问性(信息隐藏)

–使类,成员尽可能私有

–公共类不应具有公共字段

(4)文件事项

▪记录每个类,接口,方法,构造函数,参数和异常–类:实例代表什么

–方法:方法与其客户之间的契约

▪前提条件,后置条件,副作用

–参数:指示单元,形式,所有权

▪文件 线程安全

▪如果类是可变的,则记录状态空间

重用比说起来容易得多。 做到这一点既需要好的设计,也需要非常好的文档。 即使我们看到好的设计(这种情况仍然很少见),但如果没有好的文档,我们也不会看到组件的重用。 – D. L. Parnas Software Aging,有关ICSE 1994

(5)考虑绩效后果

▪错误的决定会限制性能

–使类型可变

–提供构造函数而不是静态工厂

–使用实现类型而不是接口

▪不要扭曲API以获取性能

–基本的性能问题将得到解决,但永远困扰着您

▪好的设计 通常会与良好的性能相吻合。

▪错误的API决策的性能影响可能是真实的和永久的

– Component.getSize()返回Dimension,但是Dimension是可变的,因此每个getSize调用都必须分配Dimension,从而导致数百万不必要的对象分配

(6)API必须与平台和平共处

▪一些习惯

–遵守标准命名约定

–避免过时的参数和返回类型

–核心API和语言中的模仿模式

▪利用API友好的功能

–泛型,可变参数,枚举,函数接口

▪知道并避免API陷阱和陷阱

–终结器,公共静态最终数组等。

▪不要音译API

(7)类设计

▪最小化可变性:除非有充分的理由,否则类应该是不变的

–优点:简单,线程安全,可重用

–缺点:每个值使用单独的对象

–如果可变,请保持状态空间小,定义明确。

▪仅在有意义的情况下子类化:子类化意味着可替换性(LSP)

–除非存在is-a关系,否则不要子类化。 否则,请使用委托或组合。

–不要只是为了重用实现而子类化。

–继承违反封装,并且子类对超类的实现细节敏感

(8)方法设计

▪不要让客户做模块可以做的任何事情–客户通常是通过剪切和粘贴来做事,这很丑陋,烦人且容易出错。

4.2 面向复用的软件构造技术

▪API应该尽快报告错误。 编译时是最好的-静态类型,泛型。

–在运行时,最好的方法调用是最好的

–方法应该是故障原子化的

(8)方法设计

▪以编程方式访问字符串形式的所有可用数据。 否则,客户将解析字符串,这对客户来说很痛苦

▪小心重载。 通常最好使用其他名称。

▪使用适当的参数和返回类型。

–优先于输入使用接口类型而不是类,以提高灵活性,

性能–使用最具体的可能的输入参数类型,从而将错误从运行时转移到编译时。

▪避免使用较长的参数列表。 三个或更少的参数是理想的。

–如果必须使用许多参数怎么办?

▪避免需要特殊处理的返回值。 返回零长度数组或空集合,不为null。

(2)框架设计

白盒和黑盒框架

▪白盒框架

–通过子类化和重写方法进行扩展

–通用设计模式:模板方法

–子类具有主要方法,但可以控制框架

▪黑盒框架

–通过实现插件接口进行扩展

–通用设计模式:策略,观察员

–插件加载机制加载插件并控制框架

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

白盒与黑盒框架

▪白盒框架使用子类/子类型—继承–允许扩展每个非私有方法–需要了解超类的实现–一次仅一个扩展–编译在一起–通常称为开发者框架

▪黑盒框架使用组合- -委派/组合–允许扩展接口中公开的功能–仅需要了解接口–多个插件–通常提供更多的模块化–可以单独部署(.jar,.dll等)–通常是所谓的最终用户框架, 平台

4.2 面向复用的软件构造技术

框架设计注意事项

▪一旦设计,几乎没有机会进行更改。▪关键决策:将通用零件与可变零件分开–您要解决什么问题? ▪可能的问题:–扩展点太少:仅限于一小部分用户–扩展点太多:难学,缓慢–太通用:重用价值很小

“最大限度地提高重复使用率,最大限度地减少使用量”

典型框架设计与实现

▪定义您的域–确定潜在的通用部分和可变部分–设计和编写示例插件/应用程序

▪分解并实现通用部分作为框架

▪为可变部分提供插件接口和回调机制–在适当的地方使用众所周知的设计原则和模式 …▪获得大量反馈并进行迭代

▪这通常称为“域工程”。

进化设计:提取共性

▪提取接口是进化设计的新步骤:–从具体类中发现抽象类–从抽象类中提取接口

▪一旦架构稳定就开始–从类中删除非公共方法–将默认实现移至抽象类中 实现接口

运行框架

▪有些框架可以自己运行-例如 Eclipse

▪必须扩展其他框架才能运行

– Swing,JUnit,MapReduce,Servle

t▪加载插件的方法:

–客户端编写main(),创建一个插件并将其传递给框架–框架编写main(),客户端传递名称 插件作为命令行参数或环境变量

–框架在魔术位置查找,然后自动加载和处理配置文件或.jar文件。

示例:一个Eclipse插件

▪Eclipse是流行的Java IDE。

▪更笼统地说,是一个工具框架,可促进“在整个生命周期中构建,部署和管理软件”。

▪基于OSGI标准的插件框架

▪起点:清单文件–插件名称–激活器类–元数据

学习框架

▪文档

▪教程,向导和示例

▪其他客户端应用程序和插件

▪社区,电子邮件列表和论坛

4.2 面向复用的软件构造技术

(3)Java Collections框架

什么是收集和收集框架?

▪集合:一个对元素进行分组的对象

▪主要用途:数据存储和检索以及数据传输

–熟悉的示例:java.util.Vector,java.util.Hashtable,Arra

y▪集合框架:针对

–接口-实现-的统一体系结构 独立性

–实现–可重用的数据结构–算法–可重用功能▪著名示例– C ++标准模板库(STL)– Java集合框架(JCF)

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

迭代器接口

▪枚举接口的替换

–添加删除方法

–改进方法名称

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

▪ Set – HashSet – O(1) access, no order guarantee – TreeSet – O(log n) access, sorted

▪ Map – HashMap – (See HashSet) – TreeMap – (See TreeSet)

▪ List – ArrayList – O(1) random access, O(n) insert/remove – LinkedList – O(n) random access, O(1) insert/remove;

• Use for queues and deques (no longer a good idea!)

同步包装器

▪不是线程安全的!

▪同步包装器:一种新的线程安全方法

–匿名实现,每个核心接口一个

–静态工厂收集适当类型的集合

–如果所有包装器都可以访问,则确保线程安全

–必须手动同步迭代

▪当时是新的; 现在已经老了! 同步包装器的方式现在已经过时

–同步包装器已过时

–并发收集已过时

4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术
4.2 面向复用的软件构造技术

▪向上兼容性

– Vector 实现List

– Hashtable <K,V>实现Map <K,V>

– Arrays.asList(myArray)

▪向后兼容性

– myCollection.toArray()

–new Vector <>(myCollection )

–new Hashtable <>(myMap)

新版本的功能,使其能够与更新或更强大的版本一起使用

允许与旧版本互操作的版本的属性

API设计准则

▪避免临时收集避免特定的容器类型–输入参数类型:

•任何收集接口(Collection,Map最佳)

•有时可能更喜欢使用数组–输出值类型:

•任何收集接口或类

•数组

摘要

▪设计可重用的类

–继承和重写

–重载

–参数多态性和泛型编程

–行为子类型化和Liskov替代原理(LSP)

–组成和委托

▪设计系统级可重用的库和框架

– API和库

–框架

– Java Collections框架(示例)

继续阅读