这篇博客主要摘录我在复习软件构造的可复用部分的感悟以及对一些重点内容的摘录

可复用主要包括了两个重点内容:

  • 继承与面对对象编程
  • 设计模式

其余部分就是以这两个重点为核心的一些补充,这也是软件构造的的一个非常重要的思想部分

 

这一部分内容实际上是对前面ADT,specification相关内容的继续,那些内容制定了编程过程中的一些规范,这一章就是在规范的基础上研究软件的顶层设计。正因为有了specification的保护,使得我们可以不用关心类的内部实现,而能够把注意力放在功能块之间的设计上来。

 

继承与OOP

接口,继承与泛型

 关于继承的约定

如果B是A的子类型,那么A与B之间应该有这样的关系:

  • 所有的B都是A

而这个关系在程序中的体现就是:

  • B的specification一定要强于A的specification

而这个约定又分为两种,一种是能够通过静态检查发现的。也就是A中所有的方法都必须在B中出现,而且方法的返回值,签名等等都必须相同。

当我们使用extends关键词来定义A的子类型的时候,静态检查会帮我们保证这一性质。

但是还有一种是静态检查所发现不了的,也就是pre-condition和post-condition

继承保证了共性,但是子类型也一定有其个性。子类型可以通过添加自己的方法来为自己添加细化的性质,也可以重写父类的方法,因此就有可能改变父类的方法的precondition与postcondition

要想满足B的specificatioin强于A的specifition这一性质,就必须保证:

  • B的重写方法的precondition不能强于A中的原方法
  • B的重写方法的postcondition不能弱于A中的原方法

这是ide无法静态检查得到的,需要程序员去保证。

一道例题

MIT的readingd提供了一道例题:

这道例题可以这样思考

题目中父类是rectangle,然后尝试添加一个square的子类。

乍一想是很合理的,每一个square当然是一个rectangle了,specification明明强化了——多了rectangle相邻边必须相等的保证。

但是square中对rectangle的setSize方法的重写,却没有一个能够满足条件的,为什么?

因为题中给定是mutable的类型,应该这样想:

每一个square一定是一个rectangle,但是能够任意设定两条边长的square并不是能够任意设定两条边长的rectangle

 

另一道例题

这题选B

我是这么理解了,

Double对Number是加强了postCondition,所以是符合的

事实上根据老师上课时ppt5-2中的内容,在继承关系中,子类中对父类方法的重写,变量可以逆变是没有错的,只是在java中并不支持这种写法。

 

因此,我们在需要用到变量的逆变的时候,应该不使用@Override标记,而是直接改写。这样对编译器来说是父类方法的重载,只是在概念上我们会将之当成重写罢了。

根据Liskov替换原则,这样的改写是满足条件的,也就是如果把任何对父类的引用改成子类,也不会产生任何问题,子类的结果只会更严格。

 

关于Liskov原则的理解

LIskov对继承的规定有如下几点:

  • 前置条件不能强化
  • 后置条件不能弱化
  • 不变量必须保持
  • 子类型方法参数:逆变
  • 子类型方法返回值:协变
  • 异常类型:协变

其实我倾向于将子类型方法参数与子类型方法返回值的规定归于对前置条件与后置条件限制

也就是:协变是一种强化,缩小范围,更加具体。而逆变是一种弱化,扩大范围,更加抽象。

在此我有一个疑问:为什么liskov原则不把子类型方法的参数与变量归于前置条件与后置条件呢?

泛型的继承规则

一个让初学者感到出人意料的事实:

ArrayLIst<String> 是 List<String> 的子类型

List<String> 不是 List<Object> 的子类型

java是完全屏蔽了泛型中的继承检查,只要是不一致的类型,都会被简单的当成不同类型

除非用上通配符 <?>

 <?> 是任意E : <E> 的超类型

 

 这样也没有问题:

 

 这样也可以:

通过这几个小例子可以发现,使用了通配符的泛型与外面的类型是同等进行静态检查的。

也就是说: ArrayList<Integer> 是 List<?> 的子类型,因为内外都是

 

关于equality的讨论

在离散数学当中,等价性的原则有三

  • 自反性
  • 对称性
  • 传递性

而在java当中,不同ADT的等价性在满足这几个条件的基础上,还存在着更加严格的要求。

我在这里说的是ADT的等价性而不是对象的等价性,因为对象等价的概念是物理层面的,简单的理解就是调用“==”或者equals()方法能够得到真值,详细一些来说就是从java的运行时角度来看,两个对象的数据值是一样的。

而ADT则是抽象层面的东西,是我们在设计程序的时候自己定义出来的,可能有不同的规则:
比如说,如果我在讨论动物与植物,那么我可以在概念上把兔子和猴子看成等价的,而猴子和树是不等价的。

而我们在设计ADT时,就必须使我们定义的等价遵循上面的三个条件。

 

额外的规则

程序设计中的等价是为程序行为服务的,因此应该具有更加多的限制

  • equality应当遵循等价的基本原则,也就是自反,对称和传递,这是基础
  • equals应当具有行为连续性,如果equals中比较的元素的值没有改变,那么对该实例的重复调用某一个方法应当始终返回相同的结果。
  • 若x不是null,x.equals(null)应当永远返回false
  • equals应当与hashcode的比较具有相同的结果

观察等价性与行为等价性

观察等价性

即“看起来一样”。

指的是两个引用,如果它们是观察等价的,在不对它们调用mutator方法的情况下,任何obsever方法的调用都无法区分这两个引用。

也就是说,client可以只通过observers就区分两个具有观察等价性的实例对象。

观察等价性侧重于两个对象在程序运行的当前时刻是处于相同状态的

行为等价性


即“用起来一样”

对于哪怕引用,哪怕调用了mutators方法,两个ADT的行为始终是一样的(这个行为当然也可以包括observers,所以行为等价性是包含观察等价性的)。

行为等价性侧重于两个对象现在是一样的,在未来也会一直是一样的。

行为等价性的规定如此严格,一般来说,只有两个指向同一个对象的引用才能满足这个条件。

这对mutable对象来说是合理的,因为mutable对象当中存在着太多的不确定性,比如说经典的“将list放入set中”的例子

 设计模式

设计模式相关的内容汗牛充栋,对于设计模式本身,我没有什么好补充的,因为我写博客的目的是记录感想大于总结知识点,我主要想要谈一下对设计模式的理解。

设计模式是顶层设计。

如果说继承与面对对象是代码层面的复用,那么设计模式就是模块层面的复用,通过一些很好用的框架来方便我们的程序的编写。

起初我对设计模式十分抵触。原因是我对java语法的掌握不够熟练,因此在使用设计模式中的工厂模式的时候,在继承,泛型,类型转换的各种错误之中摸爬滚打了很久,吃了很多苦,最后也没有体会到工厂模式的妙处所在,反而为其所累。

我是这么想的:

  • 设计模式不是目的,如果为了使用设计模式而使用设计模式,反而会使程序更加复杂而难以理解。

我的代码量实在太小,功能也很简单,所以没有尝到设计模式的好处。