大纲
设计可复用的类
- 继承和重写
- 重载(Overloading)
- 参数多态和泛型编程
- 行为子类型与Liskov替换原则
- 组合与委托
设计可复用库与框架
- API和库 - 框架
- Java集合框架(一个例子)
设计可复用的类
在OOP中设计可复用的类
封装和信息隐藏
继承和重写
多态性,子类型和重载
泛型编程
行为子类型和Liskov替代原则(LSP)
组合与委托
(1)行为子类型和Liskov替换原则(LSP)
行为子类型
子类型多态性:客户端代码可以统一处理不同种类的对象。 子类型多态:客户端可用统一的方式处理不同类型的对象
- 如果Cat的类型是Animal的一个子类型,那么只要使用Animal类型的表达式,就可以使用Cat类型的表达式。
假设q(x)是T类型对象x可证明的性质,那么对于S类型的对象y,q(y)应该是可证明的,其中S是T的一个子类型。 - Barbara Liskov
Java编译器执行的规则(静态类型检查)
- 子类可以添加,但不能删除方法
- 具体类必须实现所有未定义的方法
- 重写方法必须返回相同的类型或子类型
- 重写方法必须接受相同的参数类型
重写方法不会抛出额外的异常
也适用于指定的行为(方法):
- 相同或更强的不变量
- 相同或较弱的先决条件
- 相同或更强的后置条件
Liskov替代原则(LSP)
LSP是一种特定的子类型关系定义,称为强行为子类型化
在编程语言中,LSP依赖于以下限制:
- 先决条件不能在子类型中加强。前置条件不能强化
- 后置条件在子类型中不能被削弱。后置条件不能弱化
- 超类型的不变式必须保存在一个子类型中。不变量要保持
- 子类型中方法参数的变换。子类型方法参数:逆变
- 子类型中返回类型的协边。子类型方法的返回值:协变
- 子类型的方法不应引发新的异常,除非这些异常本身是超类型方法抛出的异常的子类型。 异常类型:协变(这将在第7-2节讨论)
Covariance (协变)
父类型到子类型:
越来越具体specific
返回值类型:不变或变得更具体
异常的类型:也是如此
Contravariance (反协变、逆变)
父类型到子类型:
越来越具体specific
参数类型:要相反的变化,要不变或越来越抽象
从逻辑上讲,它被称为子类型中方法参数的逆变。
这在Java中实际上是不允许的,因为它会使重载规则复杂化。
协变和反协变
数组是协变的:根据Java的子类型规则,T []类型的数组可能包含T类型的元素或T的任何子类型。
在运行时,Java知道这个数组实际上是作为一个整数数组实例化的,它只是简单地通过Number []类型的引用来访问。
- 区分:对象的类型与引用的类型
考虑泛型中的LSP
泛型是类型不变的
- ArrayList <String>是List <String>的子类型
- List <String>不是List <Object>的子类型
编译完成后,编译器会丢弃类型参数的类型信息; 因此这种类型的信息在运行时不可用。
这个过程被称为类型擦除
泛型不是协变的。
什么是类型擦除?
类型擦除:如果类型参数是无界的,则将泛型类型中的所有类型参数替换为它们的边界或对象。 因此,生成的字节码只包含普通的类,接口和方法。
泛型中的通配符
无界通配符类型使用通配符(?)指定,例如List <?>。
- 这被称为未知类型的列表。
有两种情况,无界通配符是一种有用的方法:
- 如果您正在编写可以使用Object类中提供的功能实现的方法。
- 代码使用泛型类中不依赖于类型参数的方法。 例如,List.size或List.clear。 事实上,Class <?>经常被使用,因为Class <T>中的大多数方法不依赖于T.
下限通配符:<? super A>
上限通配符:<? extends A>
考虑具有通配符的泛型的LSP
List<Number>是List<?>的一个子类
List<Number> 是List<? extends Object>的一个子类
List<Object>是List<? super String>的一个子类
(2)委托和组合
Interface Comparator<T>
int compare(T o1,T o2):比较它的两个参数的顺序。
- 一个比较函数,它对某些对象集合进行总排序。
- 可以将比较器传递给排序方法(如Collections.sort或Arrays.sort),以便精确控制排序顺序。 比较器也可以用来控制某些数据结构(例如排序集合或排序映射)的顺序,或者为没有自然排序的对象集合提供排序。
如果你的ADT需要比较大小,或者要放入Collections或Arrays进行排序,可实现Comparator接口并重写compare()函数。
该接口对每个实现它的类的对象进行总排序。
这种顺序被称为类的自然顺序,类的compareTo方法被称为其自然比较方法。
另一种方法:让你的ADT实现Comparable接口,然后重写compareTo()方法
与使用Comparator的区别:不需要构建新的Comparator类,比较代码放在ADT内部。
委托
委托只是当一个对象依赖另一个对象来实现其功能的某个子集时(一个实体将某个事物传递给另一个实体)
委派/委托:一个对象请求另一个对象的功能
- 例如分拣机正在委托比较器的功能
委派是复用的一种常见形式
- 分拣机可以重复使用任意的排序顺序
- 比较器可以重复使用需要比较整数的任意客户端代码
委托可以被描述为在实体之间共享代码和数据的低级机制。
- 显式委托:将发送对象传递给接收对象
- 隐式委托:由语言的成员查找规则
委托模式是实施委托的一种软件设计模式,虽然这个术语也用于松散地进行咨询或转发。
委托依赖于动态绑定,因为它要求给定的方法调用可以在运行时调用不同的代码段。
处理
- 接收者对象将操作委托给Delegate对象
- 接收者对象确保客户端不会滥用委托对象。
委托与继承
继承:通过新操作扩展基类或重写操作。
委托:捕获操作并将其发送给另一个对象。
许多设计模式使用继承和委派的组合。
将继承替换为委派
问题:你有一个只使用其超类的一部分方法的子类(或者它不可能继承超类数据)。
解决方案:创建一个字段并在其中放入一个超类对象,将方法委托给超类对象,并消除继承。
实质上,这种重构拆分了两个类,并使超类成为子类的帮助者,而不是其父类。
- 代替继承所有的超类方法,子类将只有必要的方法来委派给超类对象的方法。
- 一个类不包含从超类继承的任何不需要的方法。
合成继承原则
或称为合成复用原则(CRP)
- 类应该通过它们的组合(通过包含实现所需功能的其他类的实例)实现多态行为和代码复用,而不是从基类或父类继承。
- 最好组合一个对象可以做的事(has_a)而不是扩展它(is_a)。
委托可以被看作是在对象层次上的复用机制,而继承是类层次上的复用机制。
“委托”发生在objet层面,而“继承”发生在类层面
合成继承原则
组合继承的实现通常始于创建代表系统必须展现的行为的各种接口。
实现已识别的接口的类将根据需要构建并添加到业务域类中。
这样,系统行为就没有继承地实现了。
使用接口定义不同侧面的行为
接口之间通过扩展实现行为的扩展(接口组合)
类实现组合接口
委托的类型
使用(A使用B)
组合/聚合(A拥有B)
关联(A有B)
这种分类是根据被委托者和委托者之间的“耦合程度”。
(1)依赖:临时性的委托
使用类的最简单形式是调用它的方法;
这两种类别之间的关系形式被称为“uses-a”关系,其中一个类使用另一个类而不实际地将其作为属性。 例如,它可能是一个参数或在方法中本地使用。
依赖关系:对象需要其他对象(供应商)实施的临时关系。
(2)关联:永久性的委托
关联:对象类之间的一种持久关系,它允许一个对象实例使另一个对象代表它执行一个动作。
- has_a:一个类有另一个作为属性/实例变量
- 这种关系是结构性的,因为它指定一种对象与另一种对象相连,并不代表行为。
(3)组成:更强的委托
组合是一种将简单对象或数据类型组合成更复杂的对象的方法。
- is_part_of:一个类有另一个作为属性/实例变量
- 实现了一个对象包含另一个对象。
(4)聚合
聚合:对象存在于另一个之外,在外部创建,所以它作为参数传递给构造者。
- has_a
组合(Composition)与聚合(Aggregation)
在组合中,当拥有的对象被破坏时,被包含的对象也被破坏。
- 一所大学拥有多个部门,每个部门都有一批教授。 如果大学关闭,部门将不复存在,但这些部门的教授将继续存在。
在聚合中,这不一定是正确的。
- 大学可以被看作是一个部门的组合,而部门则拥有一批教授。 一位教授可以在一个以上的部门工作,但一个部门不能成为多个大学的一部分。
设计系统级可复用的库和框架
实际中的库和框架
定义关键抽象及其接口
定义对象交互和不变量
定义控制流程
提供体系结构指导
提供默认值
之所以库和框架被称为系统层面的复用,是因为它们不仅定义了1个可复用的接口/类,而是将某个完整系统中的所有可复用的接口/类都实现出来,并且定义了这些类之间的交互关系,调用关系,从而形成了系统整体的“架构”。
更多条款
API:应用程序编程接口,库或框架的接口
客户端:使用API的代码
插件:定制框架的客户端代码
扩展点:框架内预留的“空白”,开发者开发出符合接口要求的代码(即插件),框架可调用,从而相当于开发者扩展了框架的功能
协议:API和客户端之间预期的交互顺序
回调:框架调用来访问定制功能的插件方法
生命周期方法:根据协议和插件状态按顺序调用的回调方法
(1)API设计
为什么API设计很重要?
如果你编程,你是一个API设计师,并且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决策的性能影响可能是真实且永久的
- Component.getSize()返回Dimension,但Dimension是可变的,因此每个getSize调用都必须分配Dimension,导致数百万无用的对象分配
(6)API必须与平台和平共存
习惯做什么
- 遵守标准的命名约定
- 避免过时的参数和返回类型
- 模仿核心API和语言中的模式
利用API友好功能
- 泛型,可变参数,枚举,函数接口
了解并避免API陷阱和陷阱
- 终结器,公共静态最终数组等。
不要音译API
(7)类设计
最小化可变性:除非有充分的理由否则类应该是不可变的
- 优点:简单,线程安全,可重复使用
- 缺点:为每个值分开对象
- 如果可变,保持状态空间小,定义明确。
只有子类才有意义:子类化会影响替代性(LSP)
- 除非存在某种关系,否则不要继承。 否则,请使用委托或组合。
- 不要为了复用实现而继承子类。
- 继承违反封装,子类对超类的实现细节很敏感
(8)方法设计
不要让客户做任何模块可以做的事情
- 客户通常通过剪切和粘贴,这是丑陋的,烦人的,错误的。
API应该快速失败:尽快报告错误。 编译时间最好 - 静态类型,泛型。
- 在运行时,第一个错误的方法调用是最好的
- 方法应该是失败原子的
以字符串形式提供对所有可用数据的编程访问。 否则,客户端会解析字符串,这对客户来说很痛苦
过度谨慎。 通常最好使用不同的名称。
使用适当的参数和返回类型。
- 欢迎界面类型的类输入灵活性,性能
- 使用最具体的可能输入参数类型,从而将错误从运行时移到编译时间。
避免长参数列表。 三个或更少的参数是理想的。
- 如果你必须使用很多参数呢?
避免需要特殊处理的返回值。 返回零长度数组或空集合,不为null。
(2)框架设计
白盒和黑盒框架
白盒框架
- 通过继承和覆盖方法进行扩展
- 通用设计模式:模板方法
- 子类具有主要方法,但对框架进行控制
黑盒框架
- 通过实现插件接口进行扩展
- 通用设计模式:策略,观察者
- 插件加载机制加载插件并对框架进行控制
白盒与黑盒框架
白盒框架使用子类/子类型---继承
- 允许扩展每个非私有方法
- 需要了解超类的实现
- 一次只能有一个分机
- 汇编在一起
- 通常所谓的开发者框架
黑盒框架使用组合 - 委派/组合
- 允许扩展在界面中显示的功能
- 只需要了解接口
- 多个插件
- 通常提供更多的模块化
- 可以单独部署(.jar,.dll,...)
- 通常称为最终用户框架,平台
框架设计考虑
一旦设计好,改变的机会就很小
关键决策:将通用部件与可变部件分开
- 你想解决什么问题?
可能的问题:
- 扩展点太少:限于狭窄的用户类别
- 延伸点过多:难以学习,速度缓慢
- 太通用:很少复用价值
“最大限度地利用重复使用最小化”
典型的框架设计和实现
定义你的域名
- 识别潜在的公共部分和可变部分
- 设计和编写示例插件/应用程序
分解和实施通用部件为框架
为可变部分提供插件接口和回调机制
- 在适当的地方使用众所周知的设计原则和模式...
获得大量的反馈,并迭代
这通常被称为“域工程”。
进化设计:提取共同点
提取界面是进化设计中的一个新步骤:
- 抽象类是从具体类中发现的
- 接口是从抽象类中提取的
一旦架构稳定就开始
- 从课堂上删除非公开的方法
- 将默认实现移动到实现接口的抽象类中
运行一个框架
一些框架可以自行运行
- 例如 Eclipse
其他框架必须扩展才能运行
- Swing,JUnit,MapReduce,Servlets
加载插件的方法:
- 客户端写入main(),创建一个插件并将其传递给框架
- Framework写入main(),客户端将plugin的名称作为命令行参数或环境变量传递
- Framework在一个神奇的位置查找,然后配置文件或.jar文件被自动加载和处理。
(3)Java集合框架
什么是收集和收集框架?
集合:对元素进行分组的对象
主要用途:数据存储和检索,以及数据传输
- 熟悉的例子:java.util.Vector,java.util.Hashtable,Array
集合框架:一个统一的架构
- 接口 - 实现独立
- 实现 - 可复用的数据结构
- 算法 - 可复用的功能
最着名的例子
- C++标准模板库(STL)
- Java集合框架(JCF)
同步包装(不是线程安全的!)
同步包装:线程安全的新方法
- 匿名实现,每个核心接口一个
- 静态工厂需要收集适当的类型
- 如果通过包装进行全部访问,线程安全保证
- 必须手动同步迭代
那时是新的; 现在已经老了!
- 同步包装很大程度上已经过时
- 由同时收集而过时
总结
设计可复用的类
- 继承和重写
- 超载
- 参数多态和泛型编程
- 行为分类和Liskov替代原则(LSP)
- 组成和委派
设计系统级可复用的库和框架
- API和库
- 框架
- Java集合框架(一个例子)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。