软件构造复习笔记7

复用

  1. 面向复用编程
    面向复用编程(programming for reuse):开发可以复用的软件

基于复用编程(programming with reuse):复用已有的软件开发

为了降低成本和开发时间,提出了面向复用的编程,所有面向复用的代码都应该经过充分的测试,以保证它的可靠性和稳定性(不能在未来使用的时候发现一堆bug,那就白干了),而因为它是面向复用的,所以在不同的应用里可以保持一致的表现,也就是说对此功能做了标准化。

可复用性的评估

评估的方面:复用的频繁性、复用的代价 (适配)

一个有高可复用性的代码应该有如下特点:小、简单;与标准兼容;灵活可变;可扩展;泛型、参数化;模块化;变化的局部性;稳定;丰富的文档和帮助。

  1. 复用的层面
    最主要的复用是在代码层面,这也是我们所关注的,但软件构造过程中的任何实体都可能被复用(需求、spec、数据、测试用例、文档等等)

源代码层面:方法、语句…

模块层面:ADT (类和接口)

库层面:API,如.jar文件

架构层面:框架

复用分为白盒复用和黑盒复用,白盒复用意味着源码是可见的,对我们来说意义不是很大,更多的是源码不可见的黑盒复用,只有这样才能隔离客户端和ADT的内部实现。

源代码层面的复用
可以在网络上寻找自己需要的代码,但要注意开发商用的软件不能直接复制开源的代码,避免引起法律纠纷。

模块层面的复用
通过继承 (Inheritance) 的方式复用父类的代码,同时也可override父类中已存在的方法。
另一个复用的方法是 委托(delegation),详见下一小节(4.2)。
库层面的复用
通过导入库来调用库中的API完成复用。

除了导入本地库,也可以通过导入部署在网络上的库来完成复用,如 Web Services / Restful APIs

架构层面的复用
框架:一组具体类、抽象类、及其之间的连接关系。开发者可以根据spec填充自己的代码从而形成完整的系统。开发者根据Framework预留的接口所写的程序,而Framework作为主程序加以执行,执行过程中调用开发者所写的程序。关于框架详见下一小节4.2.3。

黑盒框架:通过实现特定接口/delegation进行框架扩展
白盒框架:通过代码层面的继承进行框架扩展
Liskov替换原则(LSP)
子类型多态
子类型多态:客户端可用统一的方式处理不同类型的对象。例子类对象取代父类对象而不会产生任何问题。

LSP
Liskov Substitution Principle中子类重写父类的方法应该满足的条件:

编译器在静态类型检查时强制满足的条件

子类型可以增加方法,但不可删除
子类型需要实现抽象类型中的所有未实现方法
子类型中重写的方法返回值必须与父类相同或符合co-variance(协变)
子类型中重写的方法必须使用同样类型的参数或者符合contra-variance(逆变)的参数
子类型中重写的方法不能抛出额外的异常
还应该满足的条件

更强的不变量 (RI)
更弱的前置条件
更强的后置条件
协变
关于返回值的类型,应该保持不变或者变得更具体,也就是与派生的方向一致。

所抛出的异常的类型也是如此。

class T {
Object a() { … }
void b() throws Throwable {…}
}
class S extends T {
@Override //返回值从Object协变成了String,这是符合重写的语法的
String a() { … }
@Override //抛出的异常从Throwable协变成了IOException,这也是符合重写的语法的
void b() throws IOException {…}
}

逆变
关于参数的类型,应该保持不变或者变得更抽象,也就是与派生的方向相反。

class T {
void c(String s) { … }
}
class S extends T {
@Override //虽然按照LSP这是合法的,但是在java语法中,不当作override,而是overload
void c(Object s) { … }
}

类型擦除(泛型中的LSP)
泛型类型是不支持协变的,如ArrayList 是List的子类型,但List不是List的子类型。这是因为发生了类型擦除,运行时就不存在泛型了,所有的泛型都被替换为具体的类型。

但是在实际使用的过程中是存在能够处理不同的类型的泛型的需求的,如定义一个方法参数是List类型的,但是要适应不同的类型的E,于是可使用通配符?来解决这个需求。
组合与委托
委派/委托:一个对象请求另一个对象的功能。

一个使用Comparator接口实现delegation的例子:

public class Edge {
Vertex s, t;
double weight;

}
public class EdgeComparator implements Comparator{
@Override public int compare(Edge o1, Edge o2) {
if(… > …) return 1;
else if (… < …) return -1;
else return 0;
}
}
public void sort(List edges) {
Comparator comp = new EdgeComparator();
Collections.sort(edges, comp); //把比较的功能分离出来单独委派给了一个类
}

实现比较功能还有另一种方式,让ADT实现Comparable接口然后override该接口的comparaTo()方法,但是这种方法就不再是delegation了。

选择继承还是委派?

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

委托发生在object层面(朋友关系),而继承发生在class层面(父子关系)

四种委派方式
Dependency:依赖关系,临时性的delegation。把被delegation的对象以参数方式传入。只有在需要的时候才建立与被委派类的联系,而当方法结束的时候这种关系也就随之断开了。

//如果要让鸭子用其他方式叫(或飞)只需更换new的q(f)的类型即可
Flyable f = new FlyWithWings(); //使用翅膀飞行的飞行方式
Quackable q = new Quack(); //鸭叫声的叫声
Duck d = new Duck(); //一只鸭子
d.fly(f); //让鸭子飞
d.quack(q); //让鸭子叫

class Duck {
//no field to keep Flyable object
public void fly(Flyable f) { f.fly(); } //让这个鸭子以f的方式飞
public void quack(Quackable q) { q.quack() }; //让鸭子以q的方式叫
}

Association:关联关系,永久性的delegation。被delegation的对象保存在rep中,该对象的类型被永久的与此ADT绑定在了一起。

//法一:在构造方法中传入参数绑定
Flyable f = new FlyWithWings();
Duck d = new Duck(f);
d.fly();
class Duck {
Flyable f; //这个必须由构造方法传入参数绑定
public Duck(Flyable f) { this.f = f; }
public void fly(){ f.fly(); }
}
//法二:在rep或构造方法中直接写死
Duck d = new Duck();
d.fly();
class Duck {
//这两种实现方式的效果是相同的
Flyable f = new FlyWithWings(); //写死在rep中
public Duck() { f = new FlyWithWings(); } //写死在构造方法中
public void fly(){ f.fly(); }
}
Composition: 更强的association,但难以变化。也就是Association中的法二。

Aggregation: 更弱的association,可动态变化。也就是Association中的法一。

上面所说的都是一对一的delegation,也存在一对多的delegation,只需要在rep中保存所有被委派的对象即可。

组合 Composite Reuse Principle(CRP)
利用delegation的机制,将功能的具体实现与调用分离,在实现中又通过接口的继承树实现功能的不同实现方法,而在调用类中只需要创建具体的子类型然后调用即可。组合就是多个不同方面的delegation的结合。

接口的具体子类型又可以通过静态工厂方法隐藏。

抽象层是不会轻易发生变化的,会发生变化的只有底层的具体的子类型,而具体功能的变化(实现不同的功能)也是在最底层,所以抽象层是稳定的。而在具体层,两个子类之间的委派关系就有可能是稳定的也有可能是动态的,这取决于需求和设计者的设计决策。

上图中所存在的子类与父类的替换只有在满足LSP的前提下才能存在,不满足LSP就没有这种delegation机制了。
六种设计模式:Adapter、Decorator、Facade、Strategy、Template method、Iterator

  1. Adapter适配器模式
    目的是将某个类/接口转换为client期望的其他形式。通过增加一个接口,将已存在的子类封装起来,client面向接口编程,从而隐藏了具体子类。

因为Adaptee是不匹配客户端所需求的,可能是参数上的,所以此时就需要一个中间件来做客户端和Adaptee之间的适配工作,这就是Adapter,它接受客户端的功能请求但是不会做具体的功能实现,而是把客户端所提供的参数转换成Adaptee所接受的形式,然后将任务委派给Adaptee完成。

  1. Decorator装饰器模式
    你即将开始无限套娃之旅

每个子类实现不同的特性,因为这些特性都是多个维度上的个性化的特征,没办法做到在一个顶层的接口中完成所有特征的抽象,而且需要做到在各个维度上的特性的任意组合,此时光靠继承是没办法实现特性的组合的,如果要强行使用继承实现,那么面对的一个不可避免地问题是组合爆炸,因为每次继承只能扩展一个特性,多个特性就要多次继承实现,并且也不便于维护与扩展,而且会有大量的重复代码。

因此,提出了Decorator设计模式,为对象增加不同侧面的不同特性。

装饰器模式的原理是从接口派生出子类,然后在子类中定义一个父类接口然后将其作为delegation的对象,也就是说:自己到自己的委派。这里第一个自己指的是子类型本身,第二个自己是指该接口的其他子类,由于他们两个是同一个接口的子类,所以可以称为自己到自己的委派。

下图中,Component是一个接口,接口中是公共的特性,ConcreteComponent是这个接口的基本实现,没有任何个性。Decorator是接口的一个抽象实现,它解决了委派关系的建立问题,从它派生出的诸多子类可以实现各个单独的特性而不必考虑所需要的其他特性如何在本类中实现,这些问题都通过delegation机制交给了其他子类完成。

如果你没看懂,不要担心,看看下面的例子吧

一个使用装饰器模式设计的例子

//Stack接口,定义了所有的Stack共性的基础的功能
interface Stack {
void push(Item e);
Item pop();
}
//最基础的类,啥个性也没有的Stack,只有共性的实现
public class ArrayStack implements Stack {
… //rep
public ArrayStack() {…}
public void push(Item e) {…}
public Item pop() { … }
}
//装饰器类,可以是一个抽象类,用于扩展出有各个特性方面的各个子类
public abstract class StackDecorator implements Stack {
protected final Stack stack; //用来保存delegation关系的rep
public StackDecorator(Stack stack) {
this.stack = stack; //建立稳定的delegation关系
}
public void push(Item e) {
stack.push(e); //通过delegation完成任务
}
public Item pop() {
return stack.pop(); //通过delegation完成任务
}
}
//一个有撤销特性功能的子类
public class UndoStack extends StackDecorator implements Stack {
private final UndoLog log = new UndoLog();
public UndoStack(Stack stack) {
super(stack); //调用父类的构造方法建立delegation关系
}
public void push(Item e) {
log.append(UndoLog.PUSH, e); //实现个性化的功能
super.push(e); //共性的功能通过调用父类的实现来完成
}
public void undo() {
//implement decorator behaviors on stack
}

}

使用装饰类,通过一层一层的装饰,让得到的对象最终能够拥有任意不同特性的组合,这才是decorator模式最精妙的地方。而且装饰的顺序是不会影响到对象的最终结果拥有哪些特性的,影响到的唯一地方在于最终得到的是哪个类型的对象,也就是最后一次装饰的特性决定了最终得到哪个具体类型的对象。

简直神来之笔

// 先创建出一个基础类对象
Stack s = new ArrayStack();
// 利用UndoStack中继承到的自己到自己的委派建立起从UndoStack到ArrayStack的delegation关系
// 这样,UndoStack也就能够实现最基础的功能,并且自身也实现了个性化的功能
Stack us = new UndoStack(s);
// 通过一层层的装饰实现各个维度的不同功能
Stack ss = new SecureStack(new SynchronizedStack(us));

JDK中装饰器模式的应用:static List unmodifiableList(List list)、static Set synchronizedSet(Set set);

  1. Facade外观模式
    客户端在调用的API时候会以固定的方式调用一系列的方法,而为了简化客户端的使用,便于客户端的学习使用、解耦,所以需要提供一个统一的接口来取代一系列小接口调用,对复杂系统做了一个封装。

经过封装得到的一个方法通常是静态方法,因为客户端可以直接调用这个方法而没必要new一个对象。

  1. Strategy策略模式
    有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法,而不是写死在代码里。因此可以为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例。

ConcreatestrategyA和ConcreateStrategyB是Strategy接口的两种不同的实现,客户端在运行时可选择任意一种来完成功能。在方法中只需要留出一个Strategy接口类型的参数,客户端选择具体类型后传入即可。

  1. Template Method模板模式
    做事情的步骤一样,但具体方法不同,因此共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。使用继承和重写实现模板模式。

在模板中,实现了一个固定执行一系列操作的方法,这个方法使用final关键字做了限定,不能再被子类重写,因此,子类只能通过重写该方法调用的那些尚未实现的方法。

在上一节中提到的白盒框架就是用这种技术实现的

  1. Iterator迭代器模式
    客户端希望遍历被放入容器/集合类的一组ADT对象,而无需关心容器的具体类型,也就是说,不管对象被放进哪里,都应该提供同样的遍历方式

实现方式是在ADT类中实现Iterable接口,该接口内部只有一个返回一个迭代器的方法,然后创建一个迭代器类实现Iterator接口,实现hasnext()、next()、remove()这三个方法。

一个迭代器的实例:

public class Pair implements Iterable {
private final E first, second;
public Pair(E f, E s) { first = f; second = s; }
public Iterator iterator() {
return new PairIterator();
}
private class PairIterator implements Iterator {
private boolean seenFirst = false, seenSecond = false;
public boolean hasNext() { return !seenSecond; }
public E next() {
if (!seenFirst) { seenFirst = true; return first; }
if (!seenSecond) { seenSecond = true; return second; }
throw new NoSuchElementException();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
//使用隐式方法迭代
public static void main(String[] args){
Pair pair = new Pair(“foo”, “bar”);
for (String s : pair) { … }
}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值