哈工大软件构造第9章复习总结

面向复用的软件构造技术

9.1什么是软件复用

软件复用是使用现有软件组件实现或更新软件系统的过程

软件复用的两个视角:

  • 面向复用编程:开发出可复用的软件
  • 基于复用编程:利用已有的可复用软件搭建应用系统

复用的好处:

  • 降低成本和开发时间
  • 经过充分测试,可靠、稳定
  • 标准化,在不同应用中保持一致

劣势:

  • 开发成本高,需要超过临界点才能显现出作用
  • 性能差些,缺乏针对性

9.2 如何测量程序可复用性

一个有较高可复用性的软件应当有如下特点:

  1. Brief and Simple–小、简单
  2. Protable and Standard Compliance–与标准兼容
  3. Adaptable and Flexible–灵活可变
  4. Extensibility–可扩展
  5. Generic and Parameterization–泛型、参数化
  6. Modularity–模块化
  7. Localization of volatile design assumptions–变化的局部性
  8. Stability under changing requirements–稳定
  9. Rich documentation–丰富的文档和帮助

9.3 可复用组件的级别和形态

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

9.3.1 代码复用

代码复用分为白盒复用和黑盒复用两种。

白盒复用:

源代码可见,可修改和扩展。即复制已有代码到正在开发的系统,进行修改。

优点:可定制化程度高

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

黑盒复用:

源代码不可见,不能修改;只能通过API接口来使用,无法修改代码

优点:简单,清晰

缺点:适应性较差

9.3.2 模块复用:类/接口

类是代码重用的原子单元:不需要源代码、类文件或jar/zip;只需要包含在类路径中。另外,可以使用javap工具获取类的公共方法头

复用一个类的方法:继承或者委托

继承

不需要提出仅仅是提出或委派工作的虚拟方法

可以更好地捕捉真实世界

通常需要在实现之前设计继承层次结构

无法取消属性或方法,因此必须小心不要过度

委托

委派仅仅是指一个对象依赖另一个对象来实现其功能的某个子集(一个实体将某物传递给另一个实体)

显式委托:类A和B之间没有什么关联,当A中某个功能需要借助B实现时,将方法参数设置为B类型,通过传入B的对象,调用B的方法来完成。

public class A{
	……
	public void function(B b){
		b.function2();
	}
}

隐式委派:在类A中维护着一个类B的对象作为A的成员变量,从而可以调用B的方法,实现复用。

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

9.3.3 库级别的复用:API/package

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

框架:可定制为应用程序的可重用框架代码

库和框架的区别:

  • 库:开发者构造可运行软件实体,其中涉及到对可复用库的调用
  • 框架:Framework作为主程序加以执行,执行过程中调用开发者所写的程序

好的API的特点:

  • 易于学习
  • 易于使用,甚至无需文档
  • 难以滥用
  • 易于阅读和维护使用它的代码
  • 足够强大以满足要求
  • 易于进化
  • 适合用户

9.3.4 系统级复用:框架

框架:一组具体类、抽象类、及其之间的连接关系

开发者根据framework的规约,填充自己的代码进去,形成完整系统

可以将framework看作是更大规模的API复用,除了提供可复用的API,还将这
些模块之间的关系都确定下来,形成了整体应用的领域复用

框架与应用程序不同

  • 抽象级别不同,因为框架为一系列相关问题提供了解决方案,而不是单个问题
  • 为了适应这一系列问题,该框架是不完整的,包含了热点和挂钩,以允许定制

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

9.5 设计可复用类

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

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

  • 子类型可以增加方法,但不可删除父类中的方法
  • 子类型需要实现抽象类型(接口、抽象类)中所有未实现的方法
  • 子类型中重写的方法必须有相同或子类型的返回值或者符合co-variant的参数
  • 子类型中重写的方法必须使用同样类型的参数或者符合contra-variant的参数(此种情况Java目前按照重载overload处理)
  • 子类型中重写的方法不能抛出额外的异常

另外,行为子类型也适用于以下特殊方法:

  • 更强的不变量
  • 更弱的前置条件
  • 更强的后置条件

eg:

LSP是子类型关系的一个特殊定义,称为(强)行为子类型

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

  • 前置条件不能强化
  • 后置条件不能弱化
  • 保持不变量
  • 子类型方法参数:逆变
  • 子类型方法的返回值:协变
  • 异常类型:协变
协变

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

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

子类型参数的逆变在Java中被当做overload处理

总结:

协变与逆变

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

eg:

Number[] numbers = new Number[2];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; 
//run-time error!

在运行时,Java知道这个数组被实例化为Integer数组,只是通过一个Number[]引用进行访问,因此,有可能出错。解决方法是使用泛型,编译器会在编译时就发现问题。

泛型是类型不变的,例如:

  • ArrayList<String>是List<String>的子类型
  • List<String>不是List<Object>的子类型

类型参数在编译后被丢弃,运行时不可用,此过程为类型擦除。

正是因为虚拟机中没有泛型类型对象,所有对象都属于普通类,才会发生类型擦除。泛型信息只存在于编译阶段,在运行时会被”擦除”

无论何时定义泛型类型,都会自动提供相应的原始类型。原始类型的名称只是泛型类型的名称,类型参数已删除;擦除时类型变量会被擦除,替换为限定类型,如果没有限定类型则替换为Object类型。

eg:

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

if(a instanceof Pair<String>)//compiler error
if(a instanceof Pair<T>)//compiler error
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(StringPair.getClass()==employeePair.getClass()){...}//equal

实例分析:

List<Integer> myInts = new ArrayList<>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14);//如果上一行编译通过,这一行会带来不安全
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
List<Integer> myInts = Arrays.asList(1,2,3,4,5);
List<Long> myLongs = Arrays.asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
sum(myInts); //compiler error
sum(myLongs);//compiler error
sum(myDoubles);//compiler error

给定两种具体类型A和B(例如,number和integer),MyClass<A>与MyClass<B>没有关系,无论A和B是否相关。MyClass<A>和MyClass<B>的共同父级是Object类

通配符

可采用通配符实现两个泛型类的协变。

使用通配符(?)指定无界通配符类型,例如,List<?>,表示一个未知类型的list。

使用无限定通配符的两种情况:

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

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

无限定通配符,一般用于定义一个引用变量,其可以指向多个不同类型的变量;如果使用固定类型,则只能指向对应类型的对象

eg:

public static void printList(List<Object> list) {
	for (Object elem : list)
		System.out.println(elem + " ");
	System.out.println();
}
public static void printList(List<?> list) {
	for (Object elem: list)
		System.out.print(elem + " ");
	System.out.println();
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

本意是期望可打印任意类型的List,但由于泛型不协变,只能打印List<Object>;如果使用无限定通配符,则可以打印出任意类型

<? super A> 下限通配符,表示通配的下限是A <? extends A> 上限通配符,表示通配的上限是A 但是, List<? extends Number> list,意味着list可以匹配多种类型中的一种,但并不意味着同一个list可以存放所有的这些类型,无限定通配符和下限通配符同理。 一个类型变量如果有多个限定(类或接口),则它是所有限定类型的子类型;如果多个限定中有类(至多只允许一个类),要写到声明的最前面。 限定的类型参数允许调用限定类型中的方法。如\则可以调用int的方法。 使用通配符后可以进行子类型的比较,关系如下图: ![关系图](https://img-blog.csdnimg.cn/2fff93f270dd487e98b2600fd0e49ce9.png) 即: List is a subtype of List<?>

List is a subtype of List<? extends Object>
List is a subtype of List<? super String>

eg:只有小类型放到大类型中才不会报错

9.5.2 委托和组合

比较器:int compare(T a,T b):比较其两个参数的顺序。

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

另一种方法:让你的ADT实现Comparable接口,然后override compareTo() 方法。与使用Comparator的区别:不需要构建新的Comparator类,比较代码放在ADT内部。这不是委托。

委派/委托:一个对象请求另一个对象的功能,委派是复用的一种常见形式。

很多设计模式将继承和委托结合使用。

如果子类只需要复用父类的小部分方法,可以考虑委托机制实现,不需要继承父类所有方法,避免继承大量无用的方法。

eg:

复合重用原则(CRP)

类应该通过组合(通过包含实现所需功能的其他类的实例)而不是从基类或父类继承来实现多态行为和代码重用。组合优先于继承,注:组合是委派的一种形式。

“委托”发生在对象层面,而“继承”发生在类层面。因此,当核心问题出现在对象层面而不是类层面时,需要考虑CRP原则。在具体实现中,使用接口定义系统必须对外展示的不同侧面的行为,接口之间通过extends实现行为扩展,类实现组合接口,避免复杂的继承关系。

eg:

interface Flyable {//定义抽象行为的接口
	public void fly();
}
interface Quackable {//定义抽象行为的接口
	public void quack();
}
class FlyWithWings implements Flyable {//接口的具体实现
	@Override
	public void fly() {
		System.out.println("fly with wings");
	}
}
class Quack implements Quackable {//接口的具体实现
	@Override
	public void quack() {
	System.out.println("quack like duck");
	}
}

interface Ducklike extends Flyable, Quackable {}//接口的组合,定义了行为的组合
public class Duck implements Ducklike {//从组合接口中派生具体类
	Flyable flyBehavior;//委托
	Quackable quackBehavior;
	void setFlyBehavior(Flyable f) {//设置委托对象实例
		this.flyBehavior = f; 
	}
	void setQuackBehavior(Quackable q) {
		this.quackBehavior = q; 
	}
	@Override
	public void fly() {//通过委托实现具体行为
		this.flyBehavior.fly();
	}
	@Override
	public void quack() {
		this.quackBehavior.quack();
	}
}

委派的类型(都支持一对多):
  • Dependency,临时性的委派,用完即释放
  • Association,永久性的委派,一直占着
  • Composition/aggregation,Association的两种形态

这种分类是根据委托人和被委托人之间的“耦合度”进行的。

dependency

两个类之间的这种形式的关系被称为“uses-a”关系,在这种关系中,一个类使用另一个类,而实际上没有将其合并为属性。例如,它可以是参数,也可以在方法中局部使用。

eg:委派的对象作为一个参数传进方法中,其只在该方法内代码域有效,是临时的。

Association

对象类之间的持久关系,允许一个对象实例导致另一个对象实例代表其执行操作

has_a:一个类将另一个类作为属性/实例变量。此关系是结构化的,因为它指定一种类型的对象连接到另一种类型的对象,并且不表示行为

eg:委派对象为内部的一个属性,具有永久性。

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

一种将简单对象或数据类型组合成更复杂对象或数据类型的方法

is_part_of:一个类将另一个类作为属性/实例变量;实现为一个对象包含另一个对象

eg:相比于上者,使用者有一个专属的委派对象,委派对象是使用者的一部分。

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

聚合:对象存在于另一个外部,在外部创建,因此它作为参数传递给构造函数

eg:当上者中拥有对象被破坏,委派对象也会被破坏,而下面这个对象存在于另一个之外,如果拥有者被破坏,被包含者也不会被破坏。

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

API是程序员最重要的资产和荣耀,可以吸引外部用户,提高声誉。建议始终以开发API的标准面对任何开发任务,面向“复用”编程,而不是面向“应用”编程。
设计API存在一定难度,要有足够良好的设计,一旦发布就无法自由改变了。

白盒框架

  • 通过子类化和重写方法进行扩展(继承)
  • 常见的设计模式:模板方法
  • 子类拥有main方法,但可以控制框架

黑盒框架

  • 通过实现插件接口进行扩展(委托)
  • 通用设计模式:策略、观察者模式
  • 通过插件加载机制加载插件,并为框架提供控制

eg:

总结:

  • 白盒框架使用的是子类或子类型,是继承,允许扩展每个非私有方法,需要了解超类的实现,一次只能有一个子类,通常所谓的开发人员框架;
  • 黑盒框架使用的是组成要素,是委托/组合,允许扩展接口中公开的功能,只需要了解接口,多个插件,通常提供更多的模块化,可以单独部署(.jar、.dll、…),通常是所谓的最终用户框架、平台。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值