软件构造之设计可重用的类

如果q(x)是为一个关于T型对象x的属性,S是T的子类型,那么q(y)为S类型的对象y的属性是可以证明的。

行为子类型化和Liskov替代原则

行为子类型化

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

具体体现为:
子类型具有更强的不变量
子类型的方法具有更强的规约

在编程语言中,LSP依赖于以下限制:
每个方法的前置条件不能强化
每个方法的后置条件不能弱化
每个子类型的不变量要保持
子类型的方法参数满足逆变要求
子类型的方法返回值满足协变要求
子类型的方法不应该抛出新的异常,除非这些异常本身是由超类型的方法抛出的异常的子类型。

协变和逆变

协变和逆变指的是宽类型和窄类型在某种情况下的替换或交换的特性。
协变就是用一个窄类型替代宽类型。
软件构造之设计可重用的类
逆变则用宽类型覆盖窄类型。
软件构造之设计可重用的类
数组是协变,考虑到子类型化的Java规则,类型T[]数组可能包含类型T的元素或任何T的亚型的元素。

软件构造之设计可重用的类

泛型中的LSP

在实际运行中,我们会发现,ArrayList<String>是List<String>的子类型 ,List<String> 不是 List<Object>的子类型。这是因为在代码编译完成后,编译器会丢弃类型参数的类型信息;因此,此类型信息在运行时不可用。这个过程被称为类型擦除。Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。在编译之后,List<Object>和List<String>将变成List,Object和String类型信息对于JVM来说是不可见的。在编译阶段,编译器发现它们不一致,因此给出了一个编译错误。因此, List<Integer>不是 List<Number>的子类型,即使 Integer 是 Number的一个子类型. 给定两个具体类型A和B(例如,Number和Integer),MyClass<A>与MyClass<B>没有关系,无论A和B是否相关。

泛型中的通配符

未绑定的通配符类型使用通配符(?)指定,在java泛型中,?代表未知类型,<? extends Object>表示上边界限定通配符,< ? super Object>表示下边界限定通配符。比如List<? extends Person>表示任何泛型List类型,它的类型参数是Person以及Person的子类,如List<Student>。当使用上界通配符<? extendsT>时,只能get数据而不能set数据;而下界通配符<? super T>刚好相反,当使用下界通配符时,只能set数据而不能get数据。 

委派与组合

委托仅仅是一个对象依赖另一个对象来实现其功能的某个子集(一个实体将某个东西传递给另一个实体),它是复用的一种常见形式。委托模式是一个用于实现委托的软件设计模式,它依软件构造之设计可重用的类赖于动态绑定,要求给定的方法调用可以在运行时调用不同的代码段。

组合重用原则:在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
好处
新对象存取成分对象的唯一方法是通过成分对象的接口。
这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
这种复用支持包装。
这种复用所需要的依赖较少。
每一个新的类可以将焦点集中到一个任务上。
这种复用可以在运行时间动态进行,新对象可以动态的引用与成分对象类型相同的对象。
缺点就是用组合复用建造的系统会有较多的对象需要管理。

委派的类型

临时性的delegation
软件构造之设计可重用的类

使用类的最简单形式是调用它的方法;这种形式的两个类之间的关系被称为“使用”关系,即一个类使用了另一个类,却不将其作为自身的一个属性,而是作为类中某个方法的参数。 

永久性的delegation
软件构造之设计可重用的类
一个类将另一个类的对象作为属性,它指定了一个类的对象与另一个对象的对象连接,而不表示行为。

组合
软件构造之设计可重用的类

一个类拥有另一个类作为属性/实例变量——一个对象包含另一个对象,对象在类中创建。

聚合
软件构造之设计可重用的类

对象存在于类的外部,是在外部创建的,需要作为一个参数传递给构造函数。