摘要:代码使用泛型类中不依赖于类型参数的方法。委托依赖于动态绑定,因为它要求给定的方法调用可以在运行时调用不同的代码段。委托捕获操作并将其发送给另一个对象。委托可以被看作是在对象层次上的复用机制,而继承是类层次上的复用机制。
大纲
设计可复用的类
继承和重写
重载(Overloading)
参数多态和泛型编程
行为子类型与Liskov替换原则
组合与委托
设计可复用库与框架
API和库 - 框架
Java集合框架(一个例子)
设计可复用的类在OOP中设计可复用的类
封装和信息隐藏
继承和重写
多态性,子类型和重载
泛型编程
行为子类型和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
List
编译完成后,编译器会丢弃类型参数的类型信息; 因此这种类型的信息在运行时不可用。
这个过程被称为类型擦除
泛型不是协变的。
什么是类型擦除?
类型擦除:如果类型参数是无界的,则将泛型类型中的所有类型参数替换为它们的边界或对象。 因此,生成的字节码只包含普通的类,接口和方法。
泛型中的通配符
无界通配符类型使用通配符(?)指定,例如List <?>。
这被称为未知类型的列表。
有两种情况,无界通配符是一种有用的方法:
如果您正在编写可以使用Object类中提供的功能实现的方法。
代码使用泛型类中不依赖于类型参数的方法。 例如,List.size或List.clear。 事实上,Class <?>经常被使用,因为Class
下限通配符: super A>
上限通配符: extends A>
考虑具有通配符的泛型的LSP
List
List
List
Interface Comparator
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和客户端之间预期的交互顺序
回调:框架调用来访问定制功能的插件方法
生命周期方法:根据协议和插件状态按顺序调用的回调方法
为什么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集合框架(一个例子)
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/71349.html
摘要:大纲什么是软件复用如何衡量可复用性可复用组件的级别和形态源代码级别复用模块级别的复用类抽象类接口库级别的复用包系统级别的复用框架对可复用性的外部观察类型变化例行分组实施变更代表独立分解常见行为总结什么是软件复用软件复用软件复用是使用现有软件 大纲 什么是软件复用?如何衡量可复用性?可复用组件的级别和形态 源代码级别复用 模块级别的复用:类/抽象类/接口 库级别的复用:API /包 系...
摘要:共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现子类为每个步骤提供不同的实现。模板方法将算法的骨架定义为抽象类,允许其子类提供具体行为。迭代器依次访问对象的元素而不暴露其基础表示。 大纲 结构模式 Adapter允许具有不兼容接口的类通过将自己的接口包装到已有类的接口中来一起工作。 Decorator动态添加/覆盖对象的现有方法中的行为。 Facade为大量代码提供简化的界...
摘要:设计方案的容易改变这就是所谓的软件构建的可维护性,可扩展性和灵活性。这也可能表明类型或方法可能难以维护。基于源代码中不同运算符和操作数的数量的合成度量。对修改的封闭这种模块的源代码是不可侵犯的。 大纲 软件维护和演变可维护性度量模块化设计和模块化原则OO设计原则:SOLIDOO设计原则:GRASP总结 软件维护和演变 什么是软件维护? 软件工程中的软件维护是交付后修改软件产品以纠正故障...
摘要:抽象工厂模式将具有共同主题的对象工厂分组。对可重用性和可维护性设计模式的高层考虑创造性模式工厂方法模式也称为虚拟构造器意图定义一个用于创建对象的接口,但让子类决定实例化哪个类。 大纲 创造性模式 工厂方法模式创建对象而不指定要创建的确切类。 抽象工厂模式将具有共同主题的对象工厂分组。 Builder模式通过分离构造和表示来构造复杂的对象。 结构模式 Bridge将抽象从其实现中分...
摘要:什么事面向对象面向对象方法是一种运用对象,类,继承,封装,聚合,关联,消息,多态等概念和原则来构造软件系统的开发思想方法。面向对象方法以众多的类及交互模式类间的协同工作为中心。 1.什么事面向对象? 面向对象方法是一种运用对象,类,继承,封装,聚合,关联,消息,多态等概念和原则来构造软件系统的开发思想(方法)。 2.面向对象中的的基本概念: A.对象:把问题域中的事物抽象地表示为系统中...
阅读 993·2023-04-25 22:27
阅读 854·2021-11-22 14:56
阅读 964·2021-11-11 16:54
阅读 1655·2019-08-30 15:54
阅读 3483·2019-08-30 13:20
阅读 1197·2019-08-30 10:55
阅读 2060·2019-08-26 13:34
阅读 3269·2019-08-26 11:53