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

前几次课介绍了软件构造的核心理论(ADT)与技术(OOP),其核心是保证代码质量、提高代码安全性。本次课面向一个重要的外部质量指标:可复用性——如何构造出可在不同应用中重复使用的软件模块/API?
首先探讨可复用的软件应该“长什么样”,然后学习“如何构造“

1.什么是软件复用

软件复用是使用现有软件组件实现或更新软件系统的过程。
软件复用的两个视角
创建:以系统的方式创建可重用资源(面向复用编程:开发出可复用的软件)
使用:将资源重用为构建新系统的构建块(基于复用编程:利用已有的可复用软件搭建应用系统)

在这里插入图片描述

2.如何测量复用性

复用的机会有多频繁?复用的场合有多少?

复用的代价有多大?
搜索、获取的花费
适配、扩展的花费
实例化的花费
与软件其他的部分的互连的难度的花费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqWO7CYm-1655126968180)(img/image-20220606194429593.png)]

3 可重用组件的级别和形态

最主要的复用是在代码层面, 但软件构造过程中的任何实体都可能被复用,如需求,规约,数据,测试用例。我们这里关注的是

源代码级别:方法、语句等

模块级:类和接口
库级别:API Java库,.jar
架构级别:框架

0.1 代码复用类型

A. 白盒复用:源代码可见,可修改和扩展

复制已有代码到正在开发的系统,进行修改。

好处:可定制化程度高

不足:对其修改增加了软件的复杂度,且需要对其内部充分的了解

B.黑盒复用:源代码不可见,不能修改
只能通过API接口来使用,无法修改代码
好处:简单,清晰
不足:适应性差些

0.2 可重用组件分发格式

形式:源代码;package如.jar
可重用软件组件的来源:
组织的内部代码库(Guava);第三方提供的库(Apache);语言库语言自身提供的库(JDK);代码示例;来自同事;已有系统内的代码;开源软件的代码

1.源代码复用:将部分/全部复制/粘贴到程序中
维护问题:需要在多个位置更正代码、代码太多,无法使用(版本太多)、过程中出错风险高、可能需要了解所用软件的工作原理、需要访问源代码

2.模块级复用:类/接口

A.重用类的方法:继承
Java提供了一种名为继承的代码重用方法,类扩展了现有类的属性/行为

注意:

它们可能会覆盖现有行为,通常需要在实现之前设计继承层次结构,无法取消属性或方法,因此必须小心不要过度

B.重用类的方法:委托委托
委派仅仅是指一个对象依赖另一个对象来实现其功能的某个子集(一个实体将某物传递给另一个实体),例如,sorter正在将功能委托给某个comparator

3.库级别复用:API/包

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

框架:可定制为应用程序的可重用框架代码
框架调用回客户端代码

在这里插入图片描述

4.系统级复用:框架

框架:一组具体类、抽象类、及其之间的连接关系。开发者根据framework的规约,填充自己的代码进去,形成完整系统

(库的话是程序员写、运行程序,调用库方法,框架的话是运行框架程序,其中调用程序员写的方法)

在这里插入图片描述

4 设计复用类

如何设计OOP中的复用类

封装和信息隐藏
继承和重写
多态性、子类型和重载
泛型编程

行为子类型与Liskov替代原则(LSP)
授权和组成

1.行为子类型与Liskov替代原则(LSP)

1.行为子类型

子类型多态:客户端可用统一的方式处理不同类型的对象

如果Cat型是Animal的一个子类型,则在使用Animal型表达式的任何地方都可以使用Cat型表达式。
如果对于类型T的对象x、 q(x)成立,那么对于类型T的子类型S的对象y、 q(y)也成立。

Animal a = new Animal();
Animal c1 = new Cat();
Cat c2 = new Cat();

在可以使用a的场景,都可以用c1和c2代
替而不会有任何问题
a = c1;
a = c2;

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

1.子类型可以增加方法,但不可删
2.子类型需要实现抽象类型 (接口、抽象类)中所有未实现的方法
3.子类型中重写的方法必须有相同或子类型的返回值或者符合co-variant(协变)的参数
4.子类型中重写的方法必须使用同样类型的参数或者符合contra-variant(逆变)的参数(此种情况Java目前按照重载overload处理)

5.子类型中重写的方法不能抛出额外的异常

总结即为:

子类型规约更强
更强的不变量
更弱的前置条件
更强的后置条件

2.LSP

1.LSP是子类型关系的特定定义,称为(强)行为子类型

在编程语言中,LSP依赖于以下内容限制:

前置条件不能强化

后置条件不能弱化

不变量要保持

子类型方法参数:逆变

子类型方法的返回值:协变

异常类型:协变

2.协变与逆变

协变:

父类型→子类型:越来越具体specific
返回值类型:不变或变得更具体
异常的类型:也是如此。

—>更具体的类可能有更具体的返回类型,这称为子类型中返回类型的协变。

—>为子类型的方法声明的每个异常都应该是为超类型的方法声明的某个异常的子类型

逆变:

父类型→子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来越抽象

从逻辑上讲,它被称为子类型中方法参数的逆变。

目前Java中遇到这种情况,当作overload看待

在这里插入图片描述

Java中数组是协变的: 对T[]数组,可以保存类型T及其子类型的数据

在运行时,Java知道这个数组被实例化为Integer数组,只是通过一个Number[]引用进行访问

在这里插入图片描述

3.泛型里的LSP

泛型是类型不变的
–ArrayList是List的子类型
–List不是List的子类型
▪ 代码编译完成后,编译器丢弃类型参数的类型信息;因此,此类型信息在运行时不可用。
▪ 此过程称为类型擦除
▪ 泛型不是协变的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N18ZlVnz-1655126968183)(img/image-20220606210526410.png)]

类型擦除

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

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

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

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

(1)类型无限定时:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvjzBUT1-1655126968184)(img/image-20220606210653632.png)]

(2)类型有限定时:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhibGU6i-1655126968184)(img/image-20220606213027516.png)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4GLTrCzX-1655126968185)(img/image-20220606213212588.png)]

Box不是Box的子类型,即使Integer是Number的子类型。
▪ 给定两种具体类型A和B(例如,number和integer),MyClass与MyClass没有关系,无论A和B是否相关。MyClass和MyClass的共同父级是Object
▪ 有关在类型参数相关时如何在两个泛型类之间创建类似子类型的关系的信息,参见通配符。

泛型中的通配符

使用通配符指定无界通配符类型字符(?),例如,List<?>,这称为未知类型的列表。
▪ 有两种情况下,无界通配符是一种有用的方法:

情况1:方法的实现不依赖于类型参数(不调用其中的方法),如List中的方法;

情况2:只依赖于Object 类中的功能

事实上,Class< ? >之所以经常使用,是因为Class<?>中的大多数方法都不依赖于T。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O1tXpSAH-1655126968185)(img/image-20220606214044007.png)]

printList的目标是打印任何类型的列表,但由于泛型不协变,它无法实现该目标
它只打印对象实例的列表;它无法打印List、List、List等,因为它们不是List的子类型。

利用通配符修改:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yJzRmcFH-1655126968186)(img/image-20220606214314898.png)]

1.<? super A> 下限通配符
List List<? super Integer>

前者只匹配integer类型的列表,而后者匹配integer超类型的任何类型的列表,如integer、number和object。(也就是说可以匹配比integer级别高的类型,integer作为可匹配类型的下限)

  1. <? extends A> 上限通配符

这里的extends既可以代表类的extends,也可以代表接口的implements
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RzqX8N3m-1655126968186)(img/image-20220606214756585.png)]

和下限通配符相反,可以匹配比integer级别低的类型,integer作为可匹配类型的上限(从图里可以看出来,integer和double都是number的子类)

一个类型变量如果有多个限定(类或接口),则它是所有限定类型的子类型如果多个限定中有类(至多只允许一个类),要写到声明的最前面。

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }

除了限制可用于实例化泛型类型的类型外,有界类型参数还允许调用在边界中定义的方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HhFzvPZZ-1655126968187)(img/image-20220606215231062.png)]

△2 委托和组成

1.Interface Comparator

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

如果ADT需要比较大小,或者要放入Collections或Arrays中进行排序,可实现Comparator接口并override compare()函数,在里面定义自己所需的排序方法,然后把这个比较器对象传递给sort。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gwmj0NiG-1655126968187)(img/image-20220606220245768.png)]

2.Interface Comparable 这种不是委托

此接口对实现它的每个类的对象施加总顺序。这种排序称为类的自然排序,而类的compareTo方法称为其自然比较方法。

另一种方法:让你的ADT实现Comparable接口,然后override compareTo() 方法

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

(comparator的做法是用一个类实现comparator接口,里面重写compare方法,然后在sort里定义这个子类对象,把它传给Collections.sort里;comparable的做法是直接Edge实现comparable接口,在Edge类里重写compareTo,里面定义如何排序,然后在sort里直接调用Collections.sort即可)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vgpjshMp-1655126968188)(img/image-20220606221059457.png)]

3.委派/委托:一个对象请求另一个对象的功能

委派是复用的一种常见形式
sorter可与任意分拣订单一起重复使用
comparator可以与需要比较整数的任意客户端代码一起重用

▪ 显式委托:将发送对象传递给接收对象
▪ 隐式委派:根据语言的成员查找规则

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ntTPWfYw-1655126968189)(img/image-20220606221548381.png)]

假设我们需要一个将其操作记录到控制台的列表
LoggingList由一个列表组成,并将(非日志记录)功能委托给该列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mdaopYGZ-1655126968189)(img/image-20220606221828557.png)]

继承:通过新操作扩展基类或覆盖操作。

委派:捕获操作并将其发送到另一个对象。
许多设计模式结合使用继承和委派。

如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现。 一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VjACr0KX-1655126968190)(img/image-20220607155417357.png)]

4.复合重继承原则–>或称为复合重用原则(CRP)

类应该通过组合(通过包含实现所需功能的其他类的实例)而不是从基类或父类继承来实现多态行为和代码重用。
与其扩展对象(is_ a),不如编写对象所能做的(has_a或use_a)。

“委托”发生在object层面,而“继承”发生在class层面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NH1fPBP5-1655126968191)(img/image-20220607155936611.png)]

组合而非继承的实现通常从创建各种接口开始,这些接口表示系统必须展示的行为。
▪ 实现所标识接口的类将根据需要构建并添加到业务域类中。
▪ 因此,系统行为是在没有继承的情况下实现的。

使用接口定义系统必须对外展示的不同侧面的行为
接口之间通过extends实现行为的扩展(接口组合)
类implements 组合接口
从而规避了复杂的继承关系

在这里插入图片描述

委托指的是在A类中以各种方式利用B类,完成类的功能。

委托的类型:

1.A use B dependency

通过方法的参数或者在方法的局部中使用发生联系。

B类对象在A类中出现,但是是以局部变量或是方法参数的形式出现的。A类中并没有B类的对象作为域。

一般称这种delegation为临时性的delegation。

(这个例子里,duck是A,flyable是b)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jXYlro0N-1655126968192)(img/image-20220607160819716.png)
2.A has B

以下,B指的是flyable,A指的是duck

B类对象在A类中出现,B类的对象是A类的域之一。B类对象通过A类对象的constructor方法或其它方法从外部输入A类对象。
A has B有两种情况。

1)Association 永久性委托。A类对象和B类对象之间并没有从属关系。

对象类之间的持久关系,允许一个对象实例使另一个对象实例代表其执行操作。
–has_ a:一个类(duck)将另一个类(flyable)作为属性/实例变量
–这种关系是结构化的,因为它指定一种类型的对象与另一种类型的对象相连接,并且不代表行为
在这里插入图片描述

2)Aggregation。更弱的association,可动态变化 A类对象(duck)由B类(flyable)聚合而成,但是B类可以脱离A类单独存在。
在这里插入图片描述

3.composition 更强的association,难以变化

组合是一种将简单对象或数据类型组合成更复杂对象或数据类型的方法。
–is_ part_of:一个类(duck)将另一个类(flyable)作为属性/实例变量
–实现为一个对象包含另一个对象(也就是duck包含flyable)

B类对象(flyable)在A类(duck)中出现,B类的对象是A类的域之一。B类对象在A类对象内创建。

B类对象不能脱离A类对象独立存在。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZBMt7elN-1655126968194)(img/image-20220607163432268.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMfl0LvP-1655126968195)(img/image-20220607164341484.png)]

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

框架不同于应用程序
抽象级别不同,因为框架为一系列相关问题提供了解决方案,而不是单个问题
为了适应一系列问题,该框架是不完整的,包含了hot spots和hooks,以允许定制

框架可以根据用于扩展它们的技术进行分类:白盒框架/黑盒框架

白盒框架,通过代码层面的继承进行框架扩展

黑盒框架,通过实现特定接口/delegation进行框架扩展

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uza2PZvA-1655126968195)(img/image-20220607165255727.png)]

(1)白盒框架 把可替换的内容形成方法放在类里,类本身变为抽象类,子类继承重写这些可变方法。application部分即为框架部分

在这里插入图片描述
在这里插入图片描述

例2:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fz4u1gld-1655126968197)(img/image-20220607170827999.png)]

(2)黑盒框架

在这里插入图片描述
在这里插入图片描述

通过实现插件接口扩展插件加载机制加载插件并控制框架

例2:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dXYICUss-1655126968199)(img/image-20220607171445373.png)]

对比

1.白盒框架使用子类化/子类型化—继承
–允许扩展每个非私有方法
–需要了解超类的实现
–一次只有一个分机
–一起编译
–通常是所谓的开发人员框架

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

.(img-Yp6OZwet-1655126968196)]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值