4.1 可复用性的度量、形态与外部表现
一. 设计模式与软件可复用性
软件复用是使用现有软件组件实现或更新软件系统的过程。软件可复用性具有两个方法:
- 面向复用编程:开发出可复用的软件
- 基于复用编程:利用已有的可复用软件搭建应用系统
越抽象的类复用度越高。
可复用性的特点:
- 降低成本和开发时间
- 经过充分测试,可靠、稳定
- 标准化,在不同应用中保持一致:
GUI
库的复用在应用程序中产生了共同的外观;与规则的一致性,连贯的设计。
可复用组件应该以一种定义清晰、开放的方式设计和构建,具有简洁的接口规范、可理解的文档和对未来使用的关注。做到这些需要代价。
复用是昂贵的:它涉及组织、技术和流程更改,以及支持这些更改的工具的成本,以及培训人员使用新工具和更改的成本。不仅面向复用编程代价高,并且基于复用编程代价也高。
由于开发可复用的软件的开发成本
高于一般软件的成本,故而要有足够高的适应性。
与特定的组件相比,通用组件的空间效率可能更低,执行时间可能更长。实质上是其性能更差些:针对更普适场景,缺少足够的针对性。
使用已有软件进行开发时,需要对可复用软件库进行有效的管理:必须开发和维护用于体系结构、设计、文档和代码的组件管理工具(如存储库)。
但其往往无法拿来就用,需要适配:
- 可能需要向组件添加额外的功能。添加了这个组件后,就可以重用新组件了。
- 不需要的功能可以从组件中删除,以提高其性能或减少其空间需求。
- 某些组件操作的实现可能需要修改。
二. 复用性的衡量
模块或代码或类被使用的机会越多,其可重用性就越高。
复用原则上是编写一次,复用多次。
复用的代价:
- 搜索、获取
- 适配、扩展
- 实例化
- 与软件其他部分的互连的难度
具有高可复用性的软件设备应该有:
- 体积小、复杂度低
- 与标准兼容
- 灵活可变
- 可扩展
- 泛型、参数化
- 模型化
- 变化的局部性
- 稳定
- 丰富的文档和帮助
三. 可复用组件的层级和形态
最主要的复用是在代码层面,但软件构造过程中的任何实体都可能被复用。例如:
- 需求
- 设计 / 规约
spec
- 数据
- 测试用例
- 文档
白盒复用:源代码可见,可修改和扩展:
- 复制已有代码当正在开发的系统,进行修改。
- 可以自定义模块以适应特定的情况,这允许在更多的情况下重用(可定制化程度高)。
- 对其修改增加了软件的复杂度,且需要对其内部充分的了解。
黑盒复用:源代码不可见,不能修改。
- 通过提供一些“
glue
”,以组合现有代码的形式进行重用,但通常不需要更改代码本身,因为您无法访问代码。然而只能通过API
接口来使用,无法修改代码。 - 简单、清晰然而适应性差些
1. 源代码复用
最低等级的复用:把部分/全部复制/粘贴到你的程序中。
然而:对于对其维护的问题
- 需要在多个地方更正代码
- 需要处理的代码太多(版本太多)
然而过程中出错的风险很高;可能需要有关所使用的软件如何工作的知识;需要访问源代码。
2. 模块级复用:类/接口
- 继承(白盒复用)
- 委托(黑盒复用)
类是代码复用的原子单位:
- 源代码不需要,类文件或
jar/zip
- 只需要包含类路径
- 可以使用
javap
工具来获取类的公共方法头。
其中需要注意的有:
- 非常重要的文档(
Java API
) - 封装可以复用
- 需要管理的代码更少
- 版本控制、向后兼容性仍然存在问题
- 需要将相关类打包在一起进行静态链接
Java
提供了一种代码复用的方式,叫做继承:类扩展了现有类的属性/行为,同时它们可能会重写现有的行为。特点是:
- 不需要放置只是转发或委托工作的哑方法
- 更好地符合现实世界
- 通常需要在实现之前设计继承的层次结构
- 不能取消属性或方法,因此必须小心不要做得太过
委托是指一个对象依赖另一个对象来实现其功能的某个子集(一个实体将某些内容传递给另一个实体)。例如,排序器把功能委托给某个比较器。
明智的委托支持代码复用:
Sorter
可以与任意排序顺序复用- 比较器可以与需要比较整数的任意客户端代码重用
显式委托:将发送对象传递给接收对象。明确指明调用对象使用的功能。
隐式委托:根据语言的成员查找规则。用户无法知道谁实现了功能。如向电脑或操作系统发送命令就是隐式委托。
可以将委托描述为在实体之间共享代码和数据的低级机制。
3. 库级别复用:API/Package
库是一组提供可复用功能的类和方法(APIs)。
框架可定制到应用程序中的可重用框架代码。框架会回调到客户端代码。
开发者构造可运行软件实体,其中涉及到对可复用库的调用。Framework
作为主程序加以执行,执行过程中调用开发者所写的程序。
一个好 API
的特点:
- 容易学习
- 易于使用,即使没有文档
- 很难被滥用
- 易于阅读和维护使用它的代码
- 足够强大来满足需求
- 容易进化
- 适合观众
- 不能暴露细节
4. 系统级复用:框架
框架是包含抽象类和具体类的集合,以及每个类之间的连接关系。开发者根据 framework
的规约,填充自己的代码进去,形成完整系统。
可复用性利用了应用程序领域的知识和有经验的开发人员先前的工作,将 framework
看作是更大规模的 API
复用,除了提供可复用的 API
,还将这些模块之间的关系都确定下来,形成了整体应用的领域复用。
白盒框架,通过代码层面的继承进行框架扩展。
- 可扩展性通过继承和动态绑定实现。
- 现有的功能是通过继承框架基类和重写预定义的钩子方法来扩展的。
- 通常使用模板方法模式之类的设计模式来重写钩子方法。
黑盒框架,通过实现特定接口 /delegation
进行框架扩展。
- 可扩展性是通过为可以插入到框架中的组件定义接口来实现的。
- 通过定义符合特定接口的组件,可以复用现有功能
- 这些组件通过委托与框架集成。
四. 可复用性的外部观察
- 类型可变
- 功能分组
- 实现可变
- 表示独立
- 共性抽取
1.类型可变
可复用组件应该是类型参数化的,以便它们能够适应不同的数据类型(输入、计算和输出):
- 可复用模块应该适用于许多不同类型的元素,而不需要开发人员对软件文本执行手动更改。即我们需要一种工具来描述类型参数化模块,也称为泛型模块。
- 类型可变 (泛型):适应不同的类型,且满足
LSP
2. 实现可变
实现可变:ADT
有多种不同的实现,提供不同的 representations
和 abstract function
,但具有同样的 specification (pre condition
, post condition
, invariants
),从而可以适应不同的应用场景。
3. 方法聚集
- 一个自给自足的可复用模块需要包含一组例程,每个例程对应一个操作。
- 完整性。
- 提供完备的细粒度操作,保证功能的完整性,不同场景下复用不同的操作(及其组合)。
4. 表示独立
- 可复用模块的一般形式应该允许客户端在不知道操作是如何实现的情况下指定操作。
- 表示独立性是信息隐藏规则的一种扩展,对于大型系统的顺利开发至关重要:实现决策经常会改变,客户端应该受到保护。内部实现可能会经常变化,但客户端不应受到影响。
- 表示独立性反映了客户对可复用性的看法,即忽略内部实现细节和变量的能力。
5.共性抽取
- 将共同的行为(共性)抽象出来,形成可复用实体:父类、抽象类。
- 原则上只要看到自己写了重复的或相似的代码,想办法抽取出来形成可复用方法 / 类。