Software Construction学习——可复用的构建
一.设计可复用的类
在OOP之中设计可复用的类
· 封装和信息隐藏
· 继承和重写
· 多态、子类和重载
· 泛型编程
· LSP原则
· 委派和组合(Composition)
1.LSP
Behavioral subtyping——行为子类型
-子类型多态( Subtype polymorphism) :客户端可用统一的方式处理不同类型的对象
e.g.在任何使用a的场合都可以用c1和c2来代替而不会有任何问题
在java的静态类型检查之中,编译器强调了几条规则:
· 子类型可以增加方法但不可删除
· 子类型需要实现抽象类型之中未实现的方法
· 子类型中重写的方法必须具有相同或子类型的返回值
· 子类型中重写的方法必须使用同样类型的参数
· 子类型中重写的方法不能抛出额外的异常
可以将其总结为:(联想规约强弱的比较)
- 更强的不变量
- 更弱的前置条件
- 更强的后置条件
e.g.
LSP是一种子类型关系的特殊的定义,称之为强行为子类型化((strong) behavioral subtyping)
在编程语言之中,LSP具有如下限制:
· 前置条件不能强化
· 后置条件不能呢弱化
· 不变量要保持(Representation invariant)
· 子类型方法参数:逆变(Contravariance)——往上边
· 子类型方法返回值:协变(Covariance)——往下变
· 异常类型:协变
协变(Covariance):父类型 -> 子类型
越来越具体的规约。在LSP之中,返回值和异常——不变或变得更加具体
e.g.
逆变(反协变 Contravariance):子类型 -> 父类型
规约变得更加抽象。在LSP原则之中体现为——参数类型要变得越来越抽象
e.g.
然而这种操作在java之中是不被允许的
LSP的总结
java之中的例子:
数组是协变的:一个数组T[],可能包含了T类型的实例或者T的任何子类型的实例
e.g.
泛型中的LSP:
在代码的编译完成之后,泛型的类型信息就会被编译器擦除。因此,这些类型信息并不能在运行阶段时被获得。这一过程称之为类型擦除(type erasure)。
类型擦除的详细定义:如果类型参数没有限制,则用它们的边界或Object来替换泛型类型中的所有类型参数。因此,产生的字节码只包含普通的类、接口和方法。
泛型并不是协变的
e.g.
java之中的Collection
e.g.我们并不能认为List<Number>是List<Integer>的父类,尽管Number是Integer的父类。
对于两个具体的类型来说也是如此。MyClass<A>和MyClass<B>之间并没有任何关系,不论A和B之间是什么联系。MyClass<A>和MyClass<B>之间的共同的父类是Object
泛型中的通配符:无边界通配符?
List<?>称之为未知类型
低边界通配符<? super A> e.g. List<? super Integer> List<Number>
上边界通配符<? extends A> e.g. List<? extends Number> List<Integer>
而在无边界通配符的帮助下,泛型之间就会存在子类、父类的关系
2. 委派和组合
委派——Delegation:一个对象请求另一个对象的功能。
委派是复用的一种常见的形式。
e.g.
委派设计模式:是一种用来实现委派的软件设计模式。
委派依赖于动态绑定,因为它要求给定的方法调用可以在运行时调用不同的代码段。
用一个例子来说明:
如果我们有一个LoggingList类,List是其中的一个field,并且将其功能委派给List
所以,委派满足这样一个步骤:
委派与继承的区别:
继承:将基础类通过增加、重写操作来进行扩展
委派:将类中的一个特定操作交给其它的类来执行
很多设计模式都是继承和委派的混合
显然,当子类只需要复用父类之中的一小部分方法,那么就可以不需要使用继承,而是通过委派的机制来实现
用委派来代替继承:
组合代替继承原则(组合复用原则——Composite Reuse Principle (CRP))
类应当通过它们之间的组合(通过包含其它类的实例来实现期望的功能)达到多态表现和代码复用,而不仅仅是从基础类或父类继承。
我们可以将组合(Composition)理解为(has a)而继承理解为(is a)
委派可以看做对象层面的复用机制,继承可以看做是类的层面。
组合来代替继承的实现:
- 用接口来实现系统的最基础的行为
- 接口之间用extends来实现系统功能的扩展
- 最后用具体的类来实现接口
委派的类型:
· 临时性的委派(Dependency):最简单的方法,调用类里的方法(use a),其它的类仅仅在方法内出现,作为一个局部变量。
· 永久性的委派(Association):类之中有其它类的具体实例来作为一个变量(has a )
· 组合(Composition):更强的委派。将一些简单的对象组合成一个更为复杂的对象。(is part of)
· 聚合(Aggregation):对象是在类的外部生成的,然后作为一个参数传入到类的内部构造器。
注意区分该种委派和Association的区别
Composition和Aggregation的区别:
在Composition之中,当主对象(Owning object)被摧毁时,内部的所有类的实例也都不存在了
e.g. 一个大学有多个部门,每个部门下有多个教授,当大学关闭时,那些部门都不再存在,但是部门里的教授仍然存在。
在Aggregation之中,则并不一定是这样
e.g. 一个大学可以看做是多个部门的组合,然后部门是教授们的聚合(Aggregation)。一个教授可以在多个部门工作,但是一个部门只能是一个大学的一个部分
二. 设计系统层面的复用——库和框架
之所以library 和framework 被称为系统层面的复用,是因为它们不仅定义了1 个可复用的接口/ 类,而是将某个完整系统中
的所有可复用的接口/ 类都实现出来,并且定义了这些类之间的交互关系、调用关系,从而形成了系统整体的“架构”。
相应术语:
API(Application Programming Interface):库或框架的接口
Client(客户端):使用API的代码
Plugin(插件):客户端定制框架的代码
Extension Point:框架内预留的“空白”,开发者开发出符合接口要求的代码( 即plugin) , 框架可调用,从而相当于开发者扩展了框架的功能
Protocol(协议):API与客户端之间预期的交互序列。
Callback(反馈):框架将调用的插件方法来访问定制的功能。
Lifecycle method:根据协议和插件的状态,按顺序调用的回调方法。
1. API设计:
API 是程序员最重要的资产和“荣耀”,吸引外部用户,提高声誉。一个好的代码是模块化的,每一个模块都具有API。因此建议始终以开发API的标准面对任何开发任务,面向“复用”编程,而不是面向“应用”编程。
但是如此编程也可能成为一个负担,因为这需要有足够良好的设计,而且API一旦发布就无法自由改变。
好的API的特征:容易学习、容易使用即使是没有文档、很难去误用、很容易去阅读和维护、具有足够的能力去满足要求、很容易进化
· API应当完成一件事情并且完成的很好
· API应当尽量简洁,但是不能太过于简单。因此可以在API之中添加,但是不能移除原有的功能。
· API的实现应当不影响到API
· API的文档:每一个类、接口、方法、构建器、参数、异常都需要文档;前置和后置条件以及副作用都需要对应文档;保证线程安全。
· 考虑性能的结果。不要通过不正确使用API来获得性能
· API必须能在平台上共存。遵守传统标准
· 类的设计——减小可变性(mutablity),(子类应当需要有意义)LSP原则
· 方法的设计——不要让客户端做任何模块能做的事情;API的报错要越快越好(编译时就报错是最好的——静态检查)
2. 框架设计
e.g. 网络浏览器插件
白盒和黑盒框架:
白盒框架:通过子类和重写方法来实现扩展;普遍的设计模式——模板(Template)模式;子类具有main方法,但是由框架来控制。
e.g.
黑盒框架:通过实现插件的接口来实现扩展;普遍的设计模式——策略(Strategy)、观察(Observer)模式;插件加载机制加载插件,并且由框架控制
e.g.
白盒 vs 黑盒框架
白盒框架使用子类:
· 允许对所有非private方法进行扩展
· 需要理解父类的实现
· 一次只能进行一次扩展
· 同时编译
· 也称为开发者框架
黑盒框架使用组合(Composition):
· 允许在接口之中公开功能的扩展
· 只需要理解接口
· 很多插件
· 经常提供模块
· 独立开发
· 通常称为终端用户框架、平台
框架设计需要注意的:
· 一旦设计之后,就难以发生变化
· 关键决策——将通用部件与可变部件分开
· 存在一些问题——局限于一小部分用户;难以学习;很小的复用价值
因此设计一种特殊的框架:
· 定义自己的域(Domain)——识别潜在的通用部件和可变部件;设计并写出插件/应用的样本
· 将通用部件形成框架
· 为可变部件提供插件接口和反馈机制
· 获取足够的反馈( Get lots of feedback, and iterate)
这就是所谓的域工程(Domain Engineering)
革命性设计——提取普遍性:
· 在革命性设计之中,提炼接口是一个新的台阶——抽象类是从具体类之中派生出来的;接口则是从抽象类中抽取出来的
· 当架构一旦稳定的时候就要开始——将类中所有非public的方法移除;将默认的实现转移到实现接口的抽象类之中
运行一个框架:
一些框架自身是可运行的 e.g. Eclipse
一些框架需要被扩展之后才能运行 e.g. Swing、Junit、MapReduce
加载插件的方法:
· 客户端写一个main函数,创建一个插件并将它传递给框架
· 框架写一个main函数,客户端将插件名字作为一个命令行参数或者一个环境变量传递给框架
· 框架查找特定位置,然后自动加载和处理配置文件或jar包
3. Java之中的Collections框架
Collection:一个能聚集多个元素的对象
主要用途:数据存储和检索、数据传输
Collection 框架:一个统一的框架——独立实现的接口;可复用的数据结构的实现;算法,可复用的功能
选择合适的Collection
Set:
- HashSet --O1时间复杂度,无需顺序
- TreeSet --Ologn时间复杂度,需排序
Map:
- HashMap --同HashSet - TreeMap --同TreeSet
List:
- ArrayList --O1时间复杂度,随机获取;On的插入或移除
- LinkedList --On时间复杂度,随机获取;O1的插入或删除
不可修改的包装(Unmodifiable Wrappers)
· 匿名的实现方法
· 静态工厂方法
· 一个用于每个核心的接口
· 只能读(不可变类型)
可复用的算法: