面向复用的软件构造技术
作者:互联网
1.什么是软件复用
软件重用是使用现有软件组件实现或更新软件系统的过程。
软件复用有两方面:
面向复用编程:开发出可复用的软件
基于复用编程:利用已有的可复用软件搭建应用系统
为什么复用:
降低成本和开发时间
经过充分测试,可靠、稳定
标准化,在不同应用中保持一致
2.如何衡量“可重用性”?
复用的代价:
搜索、获取库的代价
适配、扩展的代价
实例化的代价
与软件其他部分互联的代价
可重用性意味着对构建、打包、分发、安装、配置、部署、维护和升级问题进行明确的管理
一个软件库具有高度可重用性应具有以下特点:
规模小且简单
与标准兼容
灵活可变
可扩展
泛型化,参数化(不需要用户考虑实现)
模块化
变化的局部性
变化需求下的稳定性
丰富的文档和帮助
3.可重用组件的层次和形态
重用级别:
代码(最主要的)
需求
设计、规约
数据
测试用例
文档
几乎所有有关的一切都能拿来复用
3.1 源代码复用
源代码复用是最低级别
类型:
白盒复用
当代码本身可用时重用代码。通常需要某种修改或适应。于是,就把源码Ctrl+C并Ctrl+V,这是复用
好处: 可以自定义,以便于适应不同的情况
坏处: 增加了代码的复杂性,而且需要对其内部进行充分的了解
黑盒复用
通过API接口来使用,无法修改代码
好处: 简单,清晰
坏处: 适应性差
通常都是将源代码组织为库的形式,来进行复用的传播
可重用软件组件的来源:
内部(公司)代码库(Guava)
第三方库(Apache)
语言自身提供的库(JDK)
来自教程、示例、书籍等的代码示例。
代码专家或知识渊博的同事
现有系统代码
开源软件
源代码复用:
即为复制/粘贴部分/全部到您的程序中
维修问题:需要在多个地方更正代码;太多代码无法使用(很多版本)
需要考虑过程中出错的高风险
可能需要了解所用软件的工作原理
需要访问源代码
代码搜索网站:
grepcode.com
github.com/search
searchcode.com
3.2 模块级复用:类/接口
类是代码复用的组成单元,不需要源代码,需要一个压缩包jar/zip、一个使用文档
这样的封装是有利于复用的,优点在于管理更少的代码,但是版本控制,向后兼容性仍然存在问题
使用时之将相关类进行静态链接,将相关类打包在一起
复用类的方法:
继承(inheritance)
类扩展了现有类的属性/行为,同时还有可能覆盖(override)父类的内容
通常需要在实现之前设计继承层次结构
注意:不能删除一些属性或方法,因此必须小心不要对父类做出太多修改
委托(delegation)
将一个实体的任务委派给另一个实体
委托可以被描述为在实体之间共享代码和数据的一种低级机制
3.3 库级复用API/包
库(Library):包含可复用功能的一组类和方法(API)
框架(Framework):可复用的框架代码,可以自定义到应用程序中
两者的实现方式是不同的:
使用库,是你写出程序框架,然后用库中的方法和类去填充
使用框架,是你在框架的基础上,去设计填充的代码
一个好的API应该有以下特点:
易学
易于使用,即使没有文档
不易滥用
易于阅读和维护使用它的代码
足够强大以满足要求
易于进化
适合使用者
3.4 系统级重用:框架
框架(Framework):框架是包含抽象和具体类以及每个类之间的接口的集合的子系统设计。框架是一种抽象,其中提供通用功能的软件可以通过附加的用户编写的代码有选择地更改,从而提供特定于应用程序的软件。开发者根据框架的规约,填充自己的代码进去,形成完整系统。
将framework看作是更大规模的API复用,除了提供可复用的API,还将这些模块之间的关系都确定下来,形成了整体应用的领域复用
Framework作为主程序加以执行,执行过程中调用开发者所写的程序:开发者根据Framework预留的接口所写的程序
同样框架也可分为:
白盒框架
通过代码层面的继承和动态绑定实现可扩展性
通过子类化框架基类和重写预定义的钩子方法(当触发了某个条件执行的方法)来扩展现有功能
通常使用模板方法模式等设计模式来覆盖钩子方法
黑盒框架
通过为可插入框架的组件定义接口来实现可扩展性
通过定义符合特定接口的组件来重用现有功能
这些组件通过委托与框架集成
它与库的实现方式是不同的:
使用库,是你写出程序框架,然后用库中的方法和类去填充
使用框架,是你在框架的基础上,去设计填充的代码
4. 设计可复用类
OOP中的可复用类设计,要做到以下几点:
封装与信息隐藏
继承和重写
多态性、子类和重载
泛型程序设计
LSP里氏替换原则
委托和组合
4.1 LSP
子类型多态(Subtype polymorphism): 客户端可以统一处理不同类型的对象
举个例子,假设cat是animal的子类,那么cat可以用在所有出现animal的地方
Liskov替换原则: 如果对于类型T的对象x,q(x) 成立,那么对于类型T的子类型S的对象y,q(y) 也成立
于是在Java中为了满足这一原则我们有:
子类型可以增加方法,但不可删
子类型需要实现抽象类型 (接口、抽象类)中所有未实现的方法
子类型中重写的方法必须有相同或子类型的返回值或者符合co-variant的参数
子类型中重写的方法必须使用同样类型的参数或者符合contra-variant的参数(此种情况Java目前按照重载overload处理)
子类型中重写的方法不能抛出额外的异常
子类型也适用于指定的行为:
更强的不变量
更弱的前置条件
更强的后置条件
LSP是子类型关系的一个特殊定义,称为强化行为子类型
在编程语言中,LSP依赖于以下限制:
前置条件不能强化
后置条件不能弱化
不变量要保持
子类型方法参数:逆变
反协变、逆变(Contravariance)
父类型→子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来越抽象
子类型方法返回值:协变
协变(Covariance)
父类型→子类型:越来越具体
返回值类型:不变或变得更具体
异常的类型:不变或变得越来越具体
异常类型:协变
子类型的方法不应抛出新的异常,除非这些异常本身是子类型方法抛出的异常的子类型。
适用泛型时能否满足LSP呢?由于在使用泛型时,会发生类型擦除(type erasure)。因此, 泛型是类型不变的
ArrayList<String>是List<String>子类型;并且,泛型不是协变的,List<String>不是List<object>子类型。
类型擦除(type erasure):
泛型信息只存在于编译阶段,在运行时会被”擦除”
定义泛型类型时,会自动提供一个对应的原始类型(非泛型类型),原始类型的名字就是去掉类型参数后的泛型类型名。
擦除时类型变量会被擦除,替换为限定类型,如果没有限定类型则替换为Object类型。
泛型中的LSP:
Box<Integer>不是Box<Number>的子类型,即使Integer是Number的子类型。
给定两种具体类型A和B(例如,数字和整数), MyClass<A>与 MyClass<B>没有关系,无论A和B是否相关。MyClass<A>和MyClass<B>的共同父级是Object。
无限定通配符 是使用通配符(?)指定的,例如List<?>。
适用场景:
情况1:方法的实现不依赖于类型参数(不调用其中的方法),如List中的方法;
情况2:只依赖于Object 类中的功能
<? super A> 下限通配符 :匹配A及A的父类
<? extends A> 上限通配符 :匹配A及A的子类
使用上述三种通配符,可以使得原来不满足LSP的泛型,满足LSP:
List<Number> is a subtype of List<?>
List<Number> is a subtype of List<? extends Object>
List<Object> is a subtype of List<? super String>
类型类(The class Class): Java在运行时,为所有对象维护一个运行时类型标识,这个标识跟踪对象所属的类,用来确定选择哪个方法运行。保存这些信息的类叫做“Class类型类”。
4.2 委托和复合
先来介绍Java中一个实用的接口Interface Comparator<T>
。如果你的ADT需要比较大小,或者要放入Collections或Arrays中进行排序,可实现Comparator接口并override compare()函数
另一种方法:让你的ADT实现Comparable接口,然后重写compareTo() 方法
通过以上两种不同的使用Comparable接口的方法我们可以了解到,委托(Delegation) 与复合(Composition) 的区别
委派/委托(Delegation): 一个对象请求另一个对象的功能
就像上面第一种所展示的那样。
委托被描述为在实体之间共享代码和数据的一种低级机制
显式委托:将发送对象传递给接收对象
隐式委托:根据语言的成员查找规则
很多设计模式将继承和委托结合使用。如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现,从而避免继承大量无用的方法
组合优于继承原则(Composite over inheritance principle): 或称为CRP。一个类应该通过组合而不是从基类或父类继承来实现多态行为和代码重用。组合是委派的一种形式
委派可以分为三类:
依赖(Dependency)
关联(Association)
组合/聚合(Composition/aggregation)
可以认为复合/聚合是关联的两种具体形态
4.2.1 依赖
使用类的最简单形式是调用它的方法
依赖是将另一个类作为参数,或是在方法中局部使用,这种关系也被称为“uses-a”,这种关系是临时的,当方法调用结束,另一个类的生命也随之结束
4.2.2 关联
对象类之间的持久关系,一个类将另一个类作为属性或是实例变量,这种关系也被称为“has-a”
这种关系持续到调用类的结束
4.2.3 组合
是一种将简单对象或数据类型组合成更复杂对象或数据类型的方法,一个类将另一个类作为属性/实例变量,实现为一个对象包含另一个对象。也被称为“is_part_of”
对象x的生命期由唯一拥有者owner控制,例如Engine是Car的一部分。脱离Car的Engine是没有实在意义的
4.2.4 聚合
对象存在于另一个外部,在外部创建,因此它作为参数传递给构造函数也被称为,“has_a”关系
是一种特殊的联系,它表明了“部分”到“整体”的关系。例如Person有一个Address,但是Addess的存在是不依赖Person的
标签:框架,代码,List,复用,面向,类型,软件,方法 来源: https://www.cnblogs.com/492hyb/p/16366997.html