设计可复用的类java
设计可复用库与框架算法
在OOP中设计可复用的类
封装和信息隐藏
继承和重写
多态性,子类型和重载
泛型编程
行为子类型和Liskov替代原则(LSP)
组合与委托编程
行为子类型
子类型多态性:客户端代码能够统一处理不一样种类的对象。 子类型多态:客户端可用统一的方式处理不一样类型的对象设计模式
假设q(x)是T类型对象x可证实的性质,那么对于S类型的对象y,q(y)应该是可证实的,其中S是T的一个子类型。 - Barbara Liskov数组
Java编译器执行的规则(静态类型检查)安全
重写方法不会抛出额外的异常数据结构
也适用于指定的行为(方法):架构
Liskov替代原则(LSP)框架
LSP是一种特定的子类型关系定义,称为强行为子类型化
在编程语言中,LSP依赖于如下限制:编程语言
Covariance (协变)
父类型到子类型:
愈来愈具体specific
返回值类型:不变或变得更具体
异常的类型:也是如此
Contravariance (反协变、逆变)
父类型到子类型:
愈来愈具体specific
参数类型:要相反的变化,要不变或愈来愈抽象
从逻辑上讲,它被称为子类型中方法参数的逆变。
这在Java中其实是不容许的,由于它会使重载规则复杂化。
协变和反协变
数组是协变的:根据Java的子类型规则,T []类型的数组可能包含T类型的元素或T的任何子类型。
在运行时,Java知道这个数组其实是做为一个整数数组实例化的,它只是简单地经过Number []类型的引用来访问。
考虑泛型中的LSP
泛型是类型不变的
编译完成后,编译器会丢弃类型参数的类型信息; 所以这种类型的信息在运行时不可用。
这个过程被称为类型擦除
泛型不是协变的。
什么是类型擦除?
类型擦除:若是类型参数是无界的,则将泛型类型中的全部类型参数替换为它们的边界或对象。 所以,生成的字节码只包含普通的类,接口和方法。
泛型中的通配符
无界通配符类型使用通配符(?)指定,例如List <?>。
有两种状况,无界通配符是一种有用的方法:
下限通配符:<? super A>
上限通配符:<? extends A>
考虑具备通配符的泛型的LSP
List<Number>是List<?>的一个子类
List<Number> 是List<? extends Object>的一个子类
List<Object>是List<? super String>的一个子类
Interface Comparator<T>
int compare(T o1,T o2):比较它的两个参数的顺序。
若是你的ADT须要比较大小,或者要放入Collections或Arrays进行排序,可实现Comparator接口并重写compare()函数。
该接口对每一个实现它的类的对象进行总排序。
这种顺序被称为类的天然顺序,类的compareTo方法被称为其天然比较方法。
另外一种方法:让你的ADT实现Comparable接口,而后重写compareTo()方法
与使用Comparator的区别:不须要构建新的Comparator类,比较代码放在ADT内部。
委托
委托只是当一个对象依赖另外一个对象来实现其功能的某个子集时(一个实体将某个事物传递给另外一个实体)
委派/委托:一个对象请求另外一个对象的功能
委派是复用的一种常见形式
委托能够被描述为在实体之间共享代码和数据的低级机制。
委托模式是实施委托的一种软件设计模式,虽然这个术语也用于松散地进行咨询或转发。
委托依赖于动态绑定,由于它要求给定的方法调用能够在运行时调用不一样的代码段。
处理
委托与继承
继承:经过新操做扩展基类或重写操做。
委托:捕获操做并将其发送给另外一个对象。
许多设计模式使用继承和委派的组合。
将继承替换为委派
问题:你有一个只使用其超类的一部分方法的子类(或者它不可能继承超类数据)。
解决方案:建立一个字段并在其中放入一个超类对象,将方法委托给超类对象,并消除继承。
实质上,这种重构拆分了两个类,并使超类成为子类的帮助者,而不是其父类。
合成继承原则
或称为合成复用原则(CRP)
委托能够被看做是在对象层次上的复用机制,而继承是类层次上的复用机制。
“委托”发生在objet层面,而“继承”发生在类层面
合成继承原则
组合继承的实现一般始于建立表明系统必须展示的行为的各类接口。
实现已识别的接口的类将根据须要构建并添加到业务域类中。
这样,系统行为就没有继承地实现了。
使用接口定义不一样侧面的行为
接口之间经过扩展实现行为的扩展(接口组合)
类实现组合接口
委托的类型
使用(A使用B)
组合/聚合(A拥有B)
关联(A有B)
这种分类是根据被委托者和委托者之间的“耦合程度”。
(1)依赖:临时性的委托
使用类的最简单形式是调用它的方法;
这两种类别之间的关系形式被称为“uses-a”关系,其中一个类使用另外一个类而不实际地将其做为属性。 例如,它多是一个参数或在方法中本地使用。
依赖关系:对象须要其余对象(供应商)实施的临时关系。
(2)关联:永久性的委托
关联:对象类之间的一种持久关系,它容许一个对象实例使另外一个对象表明它执行一个动做。
(3)组成:更强的委托
组合是一种将简单对象或数据类型组合成更复杂的对象的方法。
(4)聚合
聚合:对象存在于另外一个以外,在外部建立,因此它做为参数传递给构造者。
组合(Composition)与聚合(Aggregation)
在组合中,当拥有的对象被破坏时,被包含的对象也被破坏。
在聚合中,这不必定是正确的。
实际中的库和框架
定义关键抽象及其接口
定义对象交互和不变量
定义控制流程
提供体系结构指导
提供默认值
之因此库和框架被称为系统层面的复用,是由于它们不只定义了1个可复用的接口/类,而是将某个完整系统中的全部可复用的接口/类都实现出来,而且定义了这些类之间的交互关系,调用关系,从而造成了系统总体的“架构”。
更多条款
API:应用程序编程接口,库或框架的接口
客户端:使用API的代码
插件:定制框架的客户端代码
扩展点:框架内预留的“空白”,开发者开发出符合接口要求的代码(即插件),框架可调用,从而至关于开发者扩展了框架的功能
协议:API和客户端之间预期的交互顺序
回调:框架调用来访问定制功能的插件方法
生命周期方法:根据协议和插件状态按顺序调用的回调方法
为何API设计很重要?
若是你编程,你是一个API设计师,而且API能够是你最大的资产之一
也能够是你最大的责任
公共API是永远的
(1)API应该作一件事,作得好
功能应该很容易解释
(2)API应该尽量小,但不能更小
API应该知足其要求
寻找一个很好的功率重量比
(3)实施不该该影响API
API中的实施细节是有害的
请注意什么是实施细节
例如:不要指定散列函数
不要让实现细节“泄露”到API中
尽可能减小一切的可达性(信息隐藏)
(4)文件事宜
记录每一个类,接口,方法,构造函数,参数和异常
先决条件,后置条件,反作用
文件线程安全
若是类是可变的,则记录状态空间
重复使用比说要容易得多。 这样作须要良好的设计和很是好的文档。 即便咱们看到良好的设计(这仍然不常见),若是没有良好的文档,咱们也不会看到组件被复用。 - D. L. Parnas软件老化,ICSE 1994
(5)考虑绩效后果
很差的决定会限制性能
不要扭曲API来得到性能
良好的设计一般与良好的性能相吻合
糟糕的API决策的性能影响多是真实且永久的
(6)API必须与平台和平共存
习惯作什么
利用API友好功能
了解并避免API陷阱和陷阱
不要音译API
(7)类设计
最小化可变性:除非有充分的理由不然类应该是不可变的
只有子类才有意义:子类化会影响替代性(LSP)
(8)方法设计
不要让客户作任何模块能够作的事情
API应该快速失败:尽快报告错误。 编译时间最好 - 静态类型,泛型。
以字符串形式提供对全部可用数据的编程访问。 不然,客户端会解析字符串,这对客户来讲很痛苦
过分谨慎。 一般最好使用不一样的名称。
使用适当的参数和返回类型。
避免长参数列表。 三个或更少的参数是理想的。
避免须要特殊处理的返回值。 返回零长度数组或空集合,不为null。
白盒和黑盒框架
白盒框架
黑盒框架
白盒与黑盒框架
白盒框架使用子类/子类型---继承
黑盒框架使用组合 - 委派/组合
框架设计考虑
一旦设计好,改变的机会就很小
关键决策:将通用部件与可变部件分开
可能的问题:
“最大限度地利用重复使用最小化”
典型的框架设计和实现
定义你的域名
分解和实施通用部件为框架
为可变部分提供插件接口和回调机制
得到大量的反馈,并迭代
这一般被称为“域工程”。
进化设计:提取共同点
提取界面是进化设计中的一个新步骤:
一旦架构稳定就开始
运行一个框架
一些框架能够自行运行
其余框架必须扩展才能运行
加载插件的方法:
什么是收集和收集框架?
集合:对元素进行分组的对象
主要用途:数据存储和检索,以及数据传输
集合框架:一个统一的架构
最着名的例子
同步包装(不是线程安全的!)
同步包装:线程安全的新方法
那时是新的; 如今已经老了!
设计可复用的类
设计系统级可复用的库和框架