9 Construction for Reuse面向复用的软件构造技术
时间:2023-04-10 01:07:01
9 Construction for Reuse
面向复用的软件结构技术
Outline
- 源代码级别复用;
- 复用模块级:类、抽象类、接口;
- 库级复用:API、包
- 系统级别复用:框架
设计可重用的类别
- 继承和重写
- 重载
- 多态和泛型参数
- 行为子类型与Liskov替换原则
- 组合和委托
可复用的仓库和框架设计
1.什么是软件复用?
它主要体现在两个方面:基于复用编程-使用现有可复用软件搭载应用系统开发可复用软件。前者需要分析各种应用场景之间的相似性和差异;后者需要基于现有,选择、适应、修改、扩展。代码越抽象,可重用性越高,越具体,可重用性越差。
为什么要重用软件?
- 降低开发成本和时间
- 经过充分测试,更加可靠稳定
- 标准化,在不同的应用中保持一致
一般来说,使用的产品规模越大,数量越多,成本越低
开发可复用软件:
- 开发成本高于一般软件,适应性足够高
- 性能差,缺乏针对更普通场景的针对性
使用现有软件进行开发常不能使用,需要适应
2. 如何衡量可复用性?
1. 复用的机会有多频繁?复用的场合有多少?
2. 复用的成本是多少?(搜索获取、适应扩展、实例化、与其他软件互联的难度)
3. 复用模块级(类、接口)
该类是代码重用的原子单元
- 不需要源代码,只需要类文件或jar/zip
- 只需包含在内classpath中即可
- 可以使用javap获取一类公共方法头文件的工具
? 文档很重要(Java API)
? 包装有助于重复使用
需要管理的代码较少
版本管理和向后兼容仍然是一个问题
相关类别需要一起包装 -- 静态链接
复用一个class有很多方法:
1. inheritance继承
此外,他们前的属性和行为,并重写现有的方法
没有必要设置一些假方法,如转发和委托,但在实现继承层结构之前,不能减少原始属性和方法,只能在原始基础上添加方法。
2. delegation委托
委托是指一个对象依靠另一个对象实现某些子集的功能(一个实体将某些东西传递给另一个实体)。
- 例如,排序器将功能委托给比较器。
谨慎的委托使代码重用
- 分拣器可以按任何顺序重复使用
- 比较器可以重用任何需要比较整数的客户端代码
? 显式委托:将发送对象传递给接收对象
? 隐藏委托:通过语言成员找到规则
委托可以描述为实体共享的低层次机制 委托是实体共享代码和数据的低级机制。
4. LIB等级复用:API、Package
Libaries:
库。一组提供可重复使用功能的类别和方法(API)。
Framework:
框架。可重复使用的骨架代码,可定制应用程序。
? 框架调用回客户端代码
- 好莱坞原则:"不要给我们打电话。我们会给你打电话。
一个好的API的特点:
? Easy to learn
▪ Easy to use, even without documentation
▪ Hard to misuse
▪ Easy to read and maintain code that uses it
▪ Sufficiently powerful to satisfy requirements
▪ Easy to evolve
▪ Appropriate to audience
5.系统级别的复用: Framework
框架是一个子系统设计,包含了一系列的抽象和具体的类,以及每个类之间的接口。
“只有骨架,没有血肉。”
开发者需要根据自己的规约,向其中填充代码形成完整的系统。
框架与应用程序不同
- 抽象水平不同,因为框架为一系列相关问题提供了解决方案,而不是单一问题。
- 为了适应系列问题,框架是不完整的,包含了hot spots和hooks,以允许定制
框架可按用于扩展的技术进行分类。
- 白盒框架
- 黑盒框架
白盒框架,通过代码层面的继承进行框架扩展
- 通过继承和动态绑定实现可扩展性。
- 通过子类化框架基类来扩展现有的功能
并覆盖预定义的钩子方法
- 通常情况下,设计模式,如模板方法模式,被用来覆盖钩子方法。
黑盒框架,通过实现特定接口/委托进行框架扩展
- 拓展性是通过定义组件的接口来实现的,这些接口可以插入到框架中。
- 通过定义符合特定接口的组件,现有的功能被重新使用特定接口
- 这些组件通过委派与框架集成。
6.设计可复用的class
技术归纳:
封装和信息隐藏
继承和重写
多态性、子类型化和重载
通用编程
行为子类型和利斯科夫替代原则(LSP)
委派和组合
6.1 Behavioral subtyping and Liskov Substitution Principle (LSP)
6.1.1行为子类型
子类型多态:用户可以用统一的方式处理不同类型的对象。
– If the type Cat is a subtype of Animal, then an expression of type Cat can be used wherever an expression of type Animal is used.
Animal a = new Animal();
Animal c1 = new Cat();
Cat c2 = new Cat();
在可以使用a的场景,都可以用c1和c2代替而不会有任何问题
a = c1;
a = c2;
Let q(x) be a property provable about objects x of type T, then q(y) should be provable for objects y of type S where S is a subtype of T. 如果对于类型T的对象x,q(x) 成立,那么对于类型T的 子类型S的对象y,q(y) 也成立。
——Barbara Liskov
1、Barbara Liskov简介
Barbara Liskov (1939- ),美国第一位计算机科学方向的女博士,2008年图灵奖获得者。她提出了第一个支持数据抽象的面向对象编程语言CLU,对现代主流语言如C++/Java/Python /Ruby/C#都有深远的影响。她所提炼出来的数据抽象思想,成为软件工程的重要精髓之一。她提出的“Liskov替换原则”,是面向对象最重要的几大原则(SOLID)之一。
2、Behavioral subtyping
Java中编译器依据强制的规则进行静态类型检查。
(1)子类型可以增加方法,但是不能删去方法;
(2)子类型需要实现抽象类型(接口、抽象类)所有未实现的方法;
(3)子类型中重写的方法必须拥有相同的或子类型的返回值,或者符合co-variant的参数;
(4)子类中重写的方法必须使用同样类型的参数或者符合contra-variant的参数(此种情况Java目前按照重载overload处理);
(5)子类型中重写的方法不能抛出额外的异常;
同样也适用于指定的行为(方法):
(1)相同或更强的不变量;
(2)相同或更弱的前置条件;
(3)相同或更强的后置条件;
6.1.2 Liskov Substitution Principle(LSP)
LSP是一种特殊的子类型关系的定义,称为(强)行为子类型。
在编程语言中,LSP依赖于以下限制:
(1)前置条件不能强化;
(2)后置条件不能弱化;
(3)不变量要保持;
(4)子类型方法参数:逆变(指从父类型到子类型越来越具体,参数类型要相反地变化,即不变或者越来越抽象);
(5)子类型方法的返回值:协变(指从父类型到子类型越来越具体,返回值类型要相同地变化,即不变或越变越具体);
(6)异常类型:协变(指从父类型到子类型越来越具体,异常的类型要相同地变化,即不变或越变越具体)。
Covariance(协变):子类型的返回值和异常,不变或者越来越具体
Contravariance(逆变):参数的类型相反的变化,要不变或者越来越抽象
目前Java中遇到这种情况,当作overload看待
错误提示:The method c(Object) of type S must override or implement a supertype method.
Java中的数组是协变的,可以保存定义的类型及其子类型的数据
在运行时,Java知道这个数组被实例化为Integer数组,只是通过一个Number[ ]引用进行访问。在编译的过程中检查不出来这样的错误,在运行阶段会报错。
泛型中的LSP
泛型是类型不变的(标签不变)
– ArrayList
– List
泛型的类型参数在编译的过程中被丢弃,在运行时不可用,即类型擦除。
虚拟机中没有泛型类型对象,所有的对象都属于普通类。在定义泛型类型时,会自动提供一个对应的原始类型,原始类型的名字就是去掉类型参数后的泛型类型名;擦出是类型变量会被擦除替换为限定的类型,如果没有限定类型则会默认的将其设置为Object。
泛型不是协变的。
在这里我们不能把一个Integer列表视为Number列表的子类型,编译器会认为这样是不安全的,将会立即拒绝它。
运行时的类型查询只适用于原始类型,否则编译器会报错
在运行的时候,泛型的变量参数被擦除,因此运行时两者的类型相同
关于当类型参数相关时如何在两个泛型类之间创建类似于子类型的关系的信息,请参见通配符。可采用通配符实现两个泛型类的协变。
泛型中的通配符
1.无限定通配符类型使用通配符(?)来指定,例如,List>。
- 这被称为未知类型的列表。
有两种情况下,无限制通配符是一种有用的方法
- 如果你正在编写一个可以使用Object类中提供的功能来实现的方法。
- 当代码使用通用类中不依赖类型参数的方法时。例如,List.size或List.clear。
- 事实上,Class>之所以被经常使用,是因为大多数的方法在类
(不依赖于类型参数,只依赖Object中的方法)
无限定通配符,一般用于定义一个引用变量,其可以指向多个不同类型的变量:
– SuperClass> sup0 = new SuperClass
– sup0 = new SuperClass
– sup0 = new SuperClass
2.下限通配符 Super A>
Super Integer>
匹配任何属于Integer的超类型的列表,如Integer、Number和Number。
3.上限通配符 extends A>
这里的extends既可以代表类的extends,也可以代表接口的implements。
List extends Number> list,意味着list可以匹配多种类型中的一种,但并不意味着同一个list可以存放所有的这些类型,无限定通配符和下限通配符同理。
一个类型变量如果有多个限定(类或接口),则它是所有限定类型的子类型;如果多个限定中有类(至多只允许一个类),要写到声明的最前面。限定的类型参数允许调用限定类型中的方法。
6.2 Delegation and Composition
来看一个简单的例子:
int compare(T o1, T o2): 对其两个参数进行顺序比较。
- 一个比较函数,它对一些对象的集合施加总的排序。
- 比较器可以被传递给一个排序方法(如Collections.sortor Arrays.sort),以允许精确控制排序顺序。比较器也可以用来控制某些数据结构的顺序(如 排序的集合或排序的地图),或者为没有自然的对象集合提供一个排序。对象的集合提供一个排序,这些对象没有一个自然的排序。
Interface Comparable
这个接口对实现它的每个类的对象施加了一个总排序。这个排序被称为该类的自然排序,该类的compareTo方法被称为其自然比较方法。与使用Comparator的区别:不需要构建新的Comparator类,比较代码放在ADT内部
6.2.1delegation委派
委派:一个对象请求另一个对象的功能
审慎的委派实现了代码重用
- 排序器Sorter可以用任意的排序顺序进行复用
- 比较器Comparators可以重用任意的需要整数比较的客户端代码
委派可以被描述为一种在实体之间共享代码和数据的低级机制。
- 显式委托:将发送对象传递给接收对象
- 隐式委托:通过语言的成员查找规则
来看一个简单的例子:
在B的定义中引入一个A,再次调用时,将foo方法委派给A来实现。
再来看一个动态绑定和功能委派的例子:
继承:通过一个新的操作来扩展一个基类或重写一个操作。
委托:捕捉一个操作并将其发送给另一个对象。
很多设计模式将继承和委托结合使用。
如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现,节约空间,避免继承大量无用的方法。
组合重用原则(CRP)
- 类应该通过它们的组合(通过包含实现所需功能的其他类的实例)而不是从基类或父类继承来实现多态行为和代码重用。
- 对一个对象所能做的事情进行组合(has_a或use_a)比对它是什么的扩展(is_a)更好。
组合优先于继承,委托发生在对象的层面上,而继承发生在class层面上。
下面我们着重来看一个实例:
An Employee class has a method for computing the employee's annual bonus:
class Employee {
Money computeBonus() {... // default computation}
...
}
Different subclasses of Employee: Manager, Programmer, Secretary, etc. may want to override this method to reflect the fact that some types of employees get more generous bonuses than others:由于雇员有很多种,因此一开始想到的是继承父类并重写这个Employee的方法。
class Manager extends Employee {
@Override
Money computeBonus() {... // special computation}
...
}
这样的solution引起了一系列的思考:
1.不同类型的Manager是否需要继续细化,引入子类?
2.将某人从Manager提升为SeniorManager时如何处理?
3.如果两个类型的计算方式一样时,采取复制?
这个问题的核心在于,每个Employee对象的奖金计算方式不同,在object方面而不是class层面。
Composite over inheritance principle 更普适的
▪ 一个假象的场景:
– 你要开发一套动物ADT,各种不同种类的动物,每类动物有自己的独特
“行为”,某些行为可能在不同类型的动物之间复用。
– 考虑到生物学和AI的进展,动物的“行为”可能会发生变化;
▪ 例如:
– 行为:飞、叫、…
– 动物:既会飞又会叫的鸭子、天鹅;不会飞但会叫的猫、狗;…
– 有10余种“飞”的方式、有10余种“叫”的方式;而且持续增加
class duck {
void fly() {…//飞法1}
void quack() {…//叫法1}
}
class swan {
void fly() {…//飞法2}
void quack() {…//叫法2}
}
直接面向具体类型动物的编程:类
缺陷:存在大量的重复;不易变化
Delegation的种类
Dependency
Association
Composition、aggregation
这种委托关系的分类基于委托者和被委托者的“耦合度”
(1)Dependency:临时性的delegation “use-a”
依赖性:一个对象需要其他对象来实现的临时关系。通过方法的参数或者在方法的局部中使用发生联系
2)Association: 永久性的delegation “has-a”
关联:对象类之间的持久关系,允许一个对象实例导致另一个对象代表它执行一个动作。
- has_a:一个类有另一个类作为属性/实例变量
- 这种关系是结构性的,因为它规定了一种对象与另一种对象的联系,并不代表行为。
(3) Composition: 更强的association,但难以变化
组成是将简单的对象或数据类型组合成更复杂的对象或数据类型的一种方式。
- is_part_of:一个类有另一个类作为属性/实例变量
- 实现的方式是一个对象包含另一个对象
(4) Aggregation: 更弱的association,可动态变化
聚合:该对象存在于其他对象之外,是在外部创建的,所以它被作为一个参数传递给构造者。
- has_a
组合VS聚合
Types of delegation
▪ Dependency (A use one or multiple B)
▪ Association (A has one or multiple B)
▪ Composition/aggregation (A owns one or multiple B)
▪ 都支持1对多的delegation——
7.设计系统级别的可复用的API libraries和Frameworks
Library: A set of classes and methods (APIs) that provide reusable functionality
API是程序员最重要的资产和“荣耀”,吸引外部用户,提高声誉
建议:始终以开发API的标准面对任何开发任务
面向“复用”编程,而不是面向“应用”编程
难度:要有足够良好的设计,一旦发布就无法再自由改变
白盒框架
- 通过子类和重写方法进行扩展
- 常见的设计模式:模板方法
- 子类有主方法,但将控制权交给框架
黑盒框架
- 通过实现一个插件接口进行扩展
- 常见的设计模式:策略,观察者
- 插件加载机制加载插件并将控制权交给框架
白盒框架使用子类/子类型化 ---继承
- 允许对每个非私有方法进行扩展
- 需要了解超类的实现
- 一次只能有一个扩展
- 编译在一起
- 通常是所谓的开发者框架
黑盒框架使用组合 -- 委派/组合
- 允许对接口中暴露的功能进行扩展
- 只需要理解接口
- 多个插件
- 通常提供更多的模块化
- 可以单独部署(.jar, .dll, ...)。
- 通常是所谓的最终用户框架、平台