软件构造--Chapter9总结

Construction for Reuse

Software Reuse

软件复用是用已经存在的软件要素去实现或更新软件系统的过程。
面向复用编程–开发出可复用的软件
基于复用编程–利用已有的可复用软件搭建应用系统

复用的原因:
1.复用可以降低成本、减少开发时间;
2.复用的产品经过充分测试,更加可靠、稳定;
3.标准化,在不同的应用中保持一致

被使用的产品规模越大、数量越多,相应的复用成本越低。
在这里插入图片描述
而开发可复用的软件,其开发成本高于一般软件的成本(考虑到适应性),其性能也相对较差(考虑到缺少针对性)

在使用可复用的软件时,常常无法不能直接使用,需要根据自己的场景进行相应的修改、适配。

下面是JDK中的可复用库和API的视图:
在这里插入图片描述

度量可复用性

一个有较高可复用性的软件应当有如下特点:
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–丰富的文档和帮助

Levels and morphology of reusable components

最主要的复用是在代码层面,但是软件构造过程中的任何实体都可能被复用,分为如下几个维度:
1.源代码层面:methods、statements等
2.模块层面:class、interface
3.库层面:API
4.体系结构层面:框架

除此之外,按照源代码是否可见、是否可以修改又分为白盒复用和黑盒复用。
白盒复用源代码可见,可修改,其可定制化程度较高,对其修改增加了软件的复杂度,需要对内部充分了解。
黑盒复用源代码不可见,不能修改,只可以API接口使用,简单、清晰,但是适应性较差

Source code reuse

代码层面复用,是最低层次的复用,常见的有copy/paste需要的部分到项目中。但是存在一定的维护问题,出错的风险也较高,需要对被使用的框架足够了解且能得到源代码。

Module-level reuse–class/interface

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

文档非常重要(Java API);封装有助于重用;要管理的代码更少;版本控制、向后兼容性仍然存在问题;需要将相关类打包在一起–静态链接

继承是一种复用类的方法,在继承中可能需要重写部分已有的方法。在继承体系中,通常需要先设计好继承体系,由于方法、属性不能在后续取消,因此小心避免冗余。

委托也是一种复用的方法,只是一个对象依赖另一个对象,其功能的某个子集(一个实体向其他实体),明智的委托支持代码重用。
委托又有显示、隐式之分。显式委托,将发送对象传递给接收对象;隐式委托,根据语言的成员查找规则。
委托可以描述为一种低级的共享机制,实体之间的代码和数据。

Library-level reuse-API/Package

库是提供可复用功能的类和方法(API)的集合。
开发者构造可运行软件实体,其中涉及到对可复用库的调用。
在这里插入图片描述
框架是可定制为应用程序的可重用框架代码,框架调用回客户端代码。
框架作为主程序加以执行,执行过程中调用开发者所写的程序。
在这里插入图片描述
一个好的API有如下的特点:
1.易于学习
2.易于使用,甚至无需文档
3.难以滥用
4.易于阅读和维护使用它的代码
5.足够强大以满足要求
6.易于进化
7.适合观众

System-level reuse-Framework

框架是一组具体类、抽象类及其之间的练习关系,开发者根据框架的规约,填充自己的代码,形成完整系统。
在这里插入图片描述
开发者根据框架预留的接口所写程序,填充到框架中。

框架可以黑盒框架和白盒框架。
黑盒框架,通过实现特定接口/委托进行框架扩展,通过为组件定义接口来实现可扩展性,通过定义符合特定接口的组件来重用现有功能,这些组件通过委托与框架集成。
白盒框架,通过继承和动态绑定实现可扩展性,通过将框架基类子类化和重写预定义的hook方法来扩展现有功能,通常使用模板方法模式等设计模式覆盖hook方法。

Designing reusable classes

Behavioral subtyping and Liskov Substitution Principle(LSP)

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

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

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

如果对于类型T的对象x,q(x)成立,那么对于类型T的子类型S的对象y,q(y)也成立。–LSP原则
概括起来为如下几点:
1.子类型可以增加方法,但不可以删;
2.子类型需要实现抽象类型中所有未实现的方法;
3.子类型重写的方法必须要有相同或子类型的返回值或者符合co-variant的参数;
4.子类型中重写的方法必须使用同样类型的参数或者符合contra-varaint的参数(目前Java按照重载处理);
5.子类型中重写的方法不能抛出额外的异常。
在规约方面则为如下几点:
1.更强的不变量;
2.更弱的前置条件;
3.更强的后置条件。
具体样例如下:
在这里插入图片描述
上述样例重写的方法有相同的前置条件、后置条件。
在这里插入图片描述
上述样例start方法有更弱的前置条件,而brake有更强的后置条件。
在这里插入图片描述
上述样例中父类Rectangle有setWidth方法,而子类Square未重写此方法,子类可以通过调用父类的方法来改变属性w,但是属性h不会随着调用setWidth改变w而对应的改变,不满足h=w,所以不符合LSP。
总结起来,LSP需要满足以下限制:
1.前置条件不能强化;
2.后置条件不能弱化;
3.不变量要保持,不能弱化;
4.子类型方法参数–逆变;
5.子类型方法的返回值–协变;
6.异常类型–协变。

Convariance-协变

协变是向更加具体方向变化。
父类型为T,a方法返回值为Object;子类型为S,a方法返回值为String,这就是返回值类型的协变。
在这里插入图片描述
同样地,有下述异常类型的协变样例:
在这里插入图片描述

Contravariance-逆变

逆变是向更抽象的方向变化。
父类型为T,方法c的参数s为String类型;子类型为S,方法c的参数s为Object类型,这就是参数类型的逆变。
在这里插入图片描述
但是,目前Java将此种情况视为重载。

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

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;//运行时错误

在运行时,Java知道此数组被实例化为Integer类型的数组,只是通过Number[]引用进行访问。

泛型是类型不变的,例如:ArrayList<String>List<String>的子类型,List<String>不是List<Object>的子类型。
类型参数在编译后被丢弃,运行时不可用,此过程为类型擦除。泛型不是不可协变的。

List<Integer> myInts = new ArrayList<>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //编译错误
myNums.add(3.14);//如果上一行编译通过,这一行就不安全

在虚拟机中没有泛型类型对象,所有对象都属于普通类。泛型信息只存在于编译阶段,在运行时会被“擦除”。
定义泛型时,会自动提供一个对应的原始类型(非泛型类型),原始类型的名字就是去掉类型参数后的泛型类型名。
擦除时类型变量会被擦除,替换为限定类型(用第一个限定类型),如果没有限定类型则替换为Object类型。
在这里插入图片描述
在这里插入图片描述
另外,运行时类型查询只适用于原始类型:

if(a instanceof Pair<String>)//编译错误
if(a instanceof Pair<T>)//编译错误
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(StringPair.getClass()==employeePair.getClass()){...}//他们是相等的

可以采用通配符实现两个泛型类的协变。
无限定通配符?List<?>,方法的实现不依赖于类型参数或者只依赖于Object类中的功能,则无限定通配符是有用的。
无限定通配符,一般用于定义一个引用变量,可以指向多个不同类型的变量,例如:

SpuerClass<?> sup0 = new SuperClass<String>();
sup0 = new SuperClass<People>();
sup0 = new SuperClass<Animal>();

不适用?,用固定类型,则需要使用下面的实现:

SuperClass<String> sup1 = new SuperClass<String>();
SuperClass<People> sup2 = new SuperClass<People>();
SuperClass<Animal> sup3 = new SuperClass<Animal>();
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();
}

上述的printList无法打印任意类型的List,只能打印参数类型为Object类型的List,而通过使用无限通配符,可以打印任意参数类型的List。

下限通配符,<? super A>,可以使用A及其父类作为参数类型。
上限通配符,<? extends A>,可以使用A及其子类作为参数类型。
注意List<? extends Number> list,虽然使用了通配符,但是不意味着同一个list可以存放不同类型的对象,无限定通配符、下限通配符同理。

限定的类型参数允许调用限定类型的方法。

public class NaturalNumber<T extends Integer> {
	private T n;
	public NaturalNumber(T n) { this.n = n; }
	public boolean isEven() {
		return n.intValue() % 2 == 0; 
	}// ...
}

同样是List类型,参数类型为Number的是参数类型为?的子类,是参数类型为? extends Object的子类;参数类型为Object的是参数类型为? super String的子类.
在这里插入图片描述

PECS部分暂时不做总结。

Delegation and Compostion

int compare(T o1,T o2)是比较两个参数顺序的接口,ADT需要比较大小,或者放入Collections或Arrays中排序,可以实现Comparator接口并重写compare()函数,例如下图的实现:
在这里插入图片描述
另一种方法是ADT实现Comparable接口,再重写compareTo()方法,但是这不再是委托机制。
在这里插入图片描述
委托,一个对象请求另一个对象的功能,是复用的一种常见形式。
显示委托,通过将发送对象传递给接收对象;隐式委托,根据语言的成员查找规则。
在这里插入图片描述
很多设计模式把继承和委托结合使用。
如果子类只需要复用父类的小部分方法,可以考虑委托机制实现,不需要继承父类所有方法,避免继承大量无用的方法。
同一个功能,继承版本和委托版本如下:

class RealPrinter {
	void print() {
		System.out.println("Printing Data");
	}
} 
class Printer extends RealPrinter { 
	void print(){
		super.print();
	} 
}
Printer printer = new Printer();
printer.print();
class RealPrinter {
	void print() {
		System.out.println("The Delegate");
	}
}
class Printer {
	RealPrinter p = new RealPrinter();
	void print() {
		p.print();
	} 
}
Printer printer = new Printer();
printer.print();

CRP,组合优先于继承(组合是委托的一种形式)。委托发生在对象层次,而继承则发生在类的层次。
在这里插入图片描述
在具体实现中,使用接口定义系统必须对外展示的不同侧面的行为,接口之间通过extends实现行为扩展,类实现组合接口,避免复杂的继承关系。
下面是一个比较详细的实例:

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();
	}
}

client对接口进行编程:

Flyable f = new FlyWithWings();
Quackable q = new Quack();
Duck d = new Duck();
d.setFlyBehavior(f);
d.setQuackBehavior(q);
d.fly();
d.quack();

Dependency,A使用B是临时性的委托,通过方法的参数或者在方法的局部使用发生联系,例如:

class Duck {
//no field to keep Flyable object
	void fly(Flyable f) {
		f.fly();
	}
}

Flyable f = new FlyWithWings();
Quackable q = new Quack();
Duck d = new Duck();
d.fly(f);
d.quack(q);

Association,A有B是永久性的委托,例如:

class Duck {
	Flyable f = new CannotFly();
	void Duck(Flyable f) { 
		this.f = f; 
	}
	void Duck() {
		f = new FlyWithWings();
	}
	void fly() { 
		f.fly(); 
	}
}

Flyable f = new FlyWithWings();
Duck d = new Duck(f);
Duck d2 = new Duck();
d.fly();

Compostion/aggregation,A拥有B,是Association的两种具体形态。
Compostion是更强的assocation,但是难以变化,例如:

class Duck{
	Fylable f = new FlyWithWings();
	void fly(){
		f.fly();
	}
}

Duck d = new Duck();
d.fly();

Aggregation是更弱的association,可以动态变化,例如:

class Duck{
	Flyable f;
	void Duck(Flyable f){
		this.f = f;
	}
	void setFlyBehavior(f){
		this.f = f;
	}
	void fly(){
		f.fly();
	}
}

Flyable f = new FlyWithWings();
Duck d = new Duck(f);
d.fly();
d.setFlyBehavior(new CannotFly());
d.fly();

上述几种不同种类的委托都是支持一对多的委托。

class Professor {
	List<Student> ls; //永久保存delegation关系
	void enroll(Student s) { 
		this.ls.add(s); //建立delegation关系
	}
	void evaluate() { 
		double score = 0;
		for(Student s : ls) 
			score += s.evaluate(this); //逐个delegate
	}
}

Designing system-level reusable API libraries and Frameworks

API是程序员最重要的资产和荣耀,可以吸引外部用户,提高声誉。
设计API存在一定难度,要有足够良好的设计,一旦发布就无法自由改变了。

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

一种使用白盒框架的方法:
在这里插入图片描述
在这里插入图片描述
另外一种:
在这里插入图片描述

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

一种使用黑盒框架的方法:
在这里插入图片描述
在这里插入图片描述
另外一种使用方法:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深海质粒ABCC9

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值