【软件构造】课件精译(十一)面向复用的软件构造技术

一、设计可复用的类

(1)行为子类型和里氏代换原则(LSP)

行为子类型
客户端可用统一的方式处理不同类型的对象
在这里插入图片描述

定义1:如果对一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1所定义的程序P中在o1全都替换成o2时,程序的行为不发生任何变化,那么T2为T1的子类。
定义2:所有引用父类的地方都必须能够透明地使用其子类对象。
来源:https://www.cnblogs.com/LeeGof/p/5705249.html

Java会确保以下规则:
子类型可以增加方法,但不可删
子类型需要实现抽象类型中的所有未实现方法
子类型中重写的方法必须有相同或子类型的返回值
子类型中重写的方法必须使用同样类型的参数
子类型中重写的方法不能抛出额外的异常

或者说:更强的不变量、更弱的前置条件和更强的后置条件

Example 1
在这里插入图片描述
这个例子中,重写函数的前置和后置条件不变,子类型实现了父类的变量并扩展了自己的变量。

Example 2
在这里插入图片描述
这个例子中,子类型重写start减弱了前置条件,重写brake增强了后置条件。

Example 3
在这里插入图片描述
这个例子中子类型增强了不变量

Example 4
在这里插入图片描述

Example 5
在这里插入图片描述
Example 6
在这里插入图片描述
里氏替换原则(LSP)
LSP是行为子类型的一个特殊定义,称作强行为子类型化
要求:
前置条件不能强化
后置条件不能弱化
不变量要保持
子类型方法参数:逆变
子类型方法返回值:协变
型变(Variance)
型变是指当子类型关系出现在更加 复杂类型中时,如何处理新类型中的子类型关系
包括:协变、逆变、不变
在这里插入图片描述

协变(Covariance)
父类型→子类型:越来越具体specific
返回值类型:不变或变得更具体
异常的类型:也是如此。
在这里插入图片描述在这里插入图片描述
逆变(Contravariance)
父类型→子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来 越抽象
在这里插入图片描述
目前Java中遇到这种情况,当作overload看待
协变和逆变
在这里插入图片描述
看这个例子,Integer和Double都是Number的子类型,所以允许如此赋值,数组本身是协变的。但是如果将子类型直接赋值给父类型变量引用,再次修改内容时会出现运行时错误,即后面几行代码的情况。
泛型中的LSP
ArrayList< String >是List< String >的子类型
List< String > 不是< Object >的子类型
泛型不是协变的,会保持子类型关系。
在这里插入图片描述
在编译过程中泛型会发生类型擦除,把类型占位符都换成对应的类型。所以,不能通过泛型的子类关系来代替整体的子类关系。
在这里插入图片描述
对于 List<?>,有两种情况,无界通配符是一种有用的方法:
如果您正在编写可以使用Object类中提供的功能实现的方法。
当代码使用泛型类中不依赖于类型参数的方法时。 例如,List.size或List.clear。
实际上,经常使用Class <?>,因为Class 中的大多数方法都不依赖于T。
例如下面这段代码:
在这里插入图片描述
由于List< Integer >不是List< Object >的子类型,所以无法传入List< Integer >, List< String >, List< Double >进行输出。但下面这种方法便可以实现:
在这里插入图片描述
通配符还有下面几种用法:
< ? super A > < ? extends A >
前者是A的父类,而后者是A的子类。
在这里插入图片描述

(2)委托和组合

一个分类示例
在这里插入图片描述
如果你的ADT需要比较大小,或者要放入Collections或Arrays进行排序,可实现Comparator接口并override compare()函数。
在这里插入图片描述
看上面这段代码,通过实现Comparator接口,从而满足Collection.sort的要求,从而进行排序。在方法中实例化一个comparator,让Collection来进行排序,实现了代码复用。
另一种方法:让你的ADT实现Comparable接口,然后override compareTo() 方法。
与使用Comparator的区别:不需要构建新的Comparator类,比较代码放在ADT内部。
在这里插入图片描述
实现这个接口以后,可以通过Collection.sort直接排序。
委托
委托是指一个对象依赖于另外一个对象的部分功能,是复用的一种常见形式 。 委托可看做是在实体之间共享代码和数据的低层机制。
在这里插入图片描述
可以看到,委托大体的形式是在内部实例化一个对象,然后调用它的方法。
在这里插入图片描述
通过委托来扩展功能
在这里插入图片描述
从这段代码可以看到,通过委托,实现了功能的扩展。
委托vs继承
继承是通过扩展或重写方法,而委托是通过将工作委托给其他对象进行。
不过,两者并非只能使用一个,很多设计模式同时使用了继承与委托,一方面继承类,另一方面在成员变量中实例化对象。

public class Main {

    public static void main(String[] args) {
        B b = new B();
        b.foo();

        A a = new B();
        a.foo();
    }
}

class A { 
    void foo() { 
        this.bar(); 
    }
    void bar() { 
        System.out.println("A.bar"); 
    }
}
class B extends A { 
    public B() {}
    void foo() { 
        super.foo();  
    }
    void bar() { 
        System.out.println("B.bar"); 
    }
}

输出结果两次都是B.bar,B.bar。
之所以super.foo()没有输出A.bar,是因为B将A的bar重写了。
用委托代替继承
如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现。
一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法 。
比如下面这个例子:
在这里插入图片描述

合成复用原则(CRP)
类应该通过它们的组合(通过包含实现所需功能的其他类的实例)来实现多态行为和代码重用,而不是从基类或父类继承。
简单的说,如果仅仅是提供功能“has_a”,委托就可以,而如果关系是“is_a”,使用继承。
这里对CRP举一个例子:员工与奖金
每个Employee对象的奖金计算方法都不同,在object层面而非class层面。
在这里插入图片描述
对于这个问题,如果仅仅是通过继承来实现,会缺少灵活性,例如两个职位共享同样的奖金计算公式,而两者又是并列的,所以可以看出,奖金自己是本就是一个功能,而不是一属性,是has_a,而不是is_a,所以对于这种比较灵活,功能性比较强的功能采用委托会更好一些。
在这里插入图片描述
看上面这种设计模式,设计了一个奖金计算接口,然后雇员基类再own_a,这样就可以进行奖金计算。然后经理类再继承雇员基类,并调用其奖金计算接口。至于奖金计算接口会通过实现了这个接口的类来实例化。
这种设计模式的好处就就不再解释了,上面已经叙述的比较清楚了。注意几点:
使用接口定义系统必须对外展示的不同侧面的行为;
接口之间通过extends实现 行为的扩展(接口组合);
类implements 组合接口,从而规避了复杂的继承关系。
在这里插入图片描述
再看这一种设计模式:
在这里插入图片描述
在这里插入图片描述
接口继承接口,然后这个接口Ducklike再由Duck实现。
委托的类型
在这里插入图片描述
区别在于两者的耦合度
在这里插入图片描述

(1) Dependency: 临时性的delegation

依赖:对象之间的动态的、临时的通信联系——uses a
在这里插入图片描述
仅仅是在参数中出现,调用了相应方法,甚至都没有实例化对象。

(2) Association: 永久性的delegation

关联:对象之间的长期静态联系——has a
在这里插入图片描述

(3)Composition:更强的association,但难以变化

组合:部分与整体的关系,彼此不可分——is part of
在这里插入图片描述
不同于一般的Association,这里变成了其一部分,并且彼此不可分,在内部创建。

(4) Aggregation: 更弱的association,可动态变化

聚合:部分与整体的关系,但彼此可分——owns a。
在这里插入图片描述
对象存在于另一个之外,在外部创建,因此它作为参数传递给构造方法。对比Composition,Aggregation可以来自外部,并且在外部仍然有意义。而Composition,一旦拥有它的对象被销毁了,它也不再存在。而Aggregation不一样,举个例子,某个学院拥有某个教授,但这个教授完全可以离开这个学院甚至这个大学,所以并不是完全耦合的一部分。
下面这个例子,显然前者是Aggregation,后者是Composition。
在这里插入图片描述
当然,一个类中完全可以存在多种委托。
为了方面理解,可以再看下面这个例子:
在这里插入图片描述

二、设计系统层面的可复用类和框架

实践库和框架
定义关键抽象和接口
定义对象不变量和方法
定义控制流
提供构建的指导
提供缺省实现
之所以library和framework被称为系统层面的复用,是因为它们不仅定义了1个可复用的接口/类,而是将某个完整系统中的所有可复用的接口/类都实现出来,并且定义了这些类之间的交互关系、调用关系,从而形成了系统整体的“架构”
其他相关的内容还有:
API、用户、插件、拓展点(框架内预留的“空白”,开发者开发出符合接口要求的代码(即plugin),框架可调用,从而相当于开发者扩展了框架的功能 )、协议、回调、生命周期方法(一种回调方法,根据协议和插件的状态在序列中调用)

(1)API设计

API是程序员最重要的资产和“荣耀”,吸引外 用户,提高声誉。
建议:始终以开发API的标准面对任何开发任务
面向“复用”编程 而不是面向“应用”编程
难度:要有足够良好的设计,一旦发布就无法再自由改变
API应该专注把一件事做好
功能单一 、命名良好、适合分解和合并模块
API应该尽可能“小”
可以增加功能,但不能移除
追求概念上的”小”,而不 是体积上的”小”
在性能和大小之间找平衡
实现不应该影响API
文档很重要
包括各种用法介绍、前置条件、后置条件、副作用、是否线程安全。
考虑性能表现
API必须与平台兼容良好
类的设计
注意LSP、尽量使用委托和组合,不要因为单纯的复用实现而使用继承。
方法设计
模块能做到的,客户端就不要。例如对所有可访问数据提供String形式的访问方法,避免客户端去解析。代码要么正常结束,产生期望结果;要么整体失败,不产生任何的结果。
用空集合或者0长度数组, 不要用null等等。

(2)框架设计

白盒框架与黑盒框架
下面这个例子并没有使用框架:
在这里插入图片描述
下面这个例子是采用白盒框架后:
在这里插入图片描述
在这里插入图片描述
下面这个例子采用了黑盒测试:
在这里插入图片描述
在这里插入图片描述
下面这个例子还是白盒测试:
在这里插入图片描述
下面这个例子是黑盒测试:
在这里插入图片描述
可以看到,白盒框架更多的是通过继承,而黑盒框架更多是通过委派/组合。
(接下来举了一些框架设计的注意事项和例子,例如Eclipse,这里省略了

(3)Java Collections框架

什么是Collection和Collection框架
举例:
C++ Standard Template Library (STL)
Java collections framework (JCF)
架构
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Collection接口
在这里插入图片描述
迭代器接口
在这里插入图片描述
集合接口
在这里插入图片描述
列表接口
在这里插入图片描述
在这里插入图片描述
映射接口
在这里插入图片描述
在这里插入图片描述
相关接口实现
在这里插入图片描述
相关复杂度
在这里插入图片描述
不可修改的包装:不可变类型,只读的
同步包装
举例:
在这里插入图片描述
一些方便的实现
在这里插入图片描述
可复用的代码
在这里插入图片描述
Example 1:为Comparable元素排序
在这里插入图片描述
Example 2:通过comparator排序
在这里插入图片描述
兼容性
向上兼容/向前兼容(forward),站在旧版软件的立场,基于旧方法编写的软件不经修改就能在新版环境中运行
Vector< E > implements List< E >
Hashtable<K,V> implements Map<K,V>
Arrays.asList(myArray)
向后兼容/向下兼容(downward),站在新版本的立场讨论对过去版本的兼容性问题,新版软件中可以支持旧版中的方法
myCollection.toArray()
new Vector<>(myCollection)
new HashTable<>(myMap)
Java collections的演进
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值