前言
本文包括软件构造课程第4、5节的内容,深入软件构造的具体组成部分(数据类型和类型检验、设计规约)来理解软件构造。
一、数据类型和类型检验
1)数据类型
在Java中数据类型分为两类,分别是基本数据类型和对象数据类型。基本数据类型包括int long float等,有一定编程经验的都会了解;对象数据类型包括String、Integer,。特点是具有继承关系。
二者的具体区别如下图:
可以将基本类型包装为对象类型,但是会降低性能。通常只有遇到<>才会使用,多数情况也会自动转换。
2)静态/动态类型检查
首先要知道的是,Java是一门静态语言,类似于C语言,动态语言的典型代表是Python。
简单来讲,静态和动态的区别就是检查出bug的时间是在程序运行前还是运行后。
静态检查往往检查的是:
1.语法错误,例如多余的标点符号成者错误的关链词,即使在动态类利的语合例如Puthon中也会做汶种检查。
2.错误的名字
3.参数错误
4.错误的返回类型
动态检查往往检查的是:
1.非法的变量值,例如整形变量xy,表达式x/y只有当y=0是才会出错,否期就是正确的。
2.无法表示的返回值。例如最后得到的返回值无法用声明的类型来表示。
3.越界访问,例如在一个字符串中使用一个负数索引
4.使用一个null对象解引用
总结起来就是:静态检查是关于“类型”的检查,不考虑值 ;动态检查是关于“值”的检查
3)可变性与不可变性
不变数据类型:一旦被创建,其值不能改变 ; 如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变指向其他对象。
可以使用final来修饰一个对象,这样对象的引用不可以改变,但引用的对象内部可以改变。若用来修饰一个基础数据类型,则初始化之后不可再赋值。
此外,final类无法派生子,final变量无法改变值/引用 ,final方法无法被子类重写。
常见的如String、Integer等。
可变类型:简单来讲就是如ArrayList等可以改变其引用/值的类型。
1.对于不可变类型,由于不可改变值或引用,每次想要更改值或值的时候,就需要做大量的临时拷贝,造成内存和时间的浪费。而可变类型因为最少化拷贝,可以提高效率。
2.更重要的一点,不可变类型有很好的优点就是比起可变类型更安全。举例,如果返回的是可变类型,一旦在后续处理中发生了不期望的更改,这种错误是非常难以检查的。这就要求我们使用可变类型时要注意,可以使用防御式拷贝、对于容器类数据类型可以使用如Collections.unmodifiableList等方法将其转变为不可变、final修饰等措施。
4)快照图
快照图是一种方便且直观的用来描述程序运行时的内部状态等图,画快照图有如下规则:
1.基本类型的使用箭头表示
2.对象数据类型使用椭圆表示
3. 不可变对象:用双线椭圆
4. 不可变的引用:用双线箭头;引用是不可变的,但指向的值却可以是可变的 ;可变的引用,也可指向不可变的值
二、设计规约
1)规约的形式和优点
Spec给“供需双方”都确定了责任,在调用的时候双方都要遵守;没规约,没法分派任务,无法写程序;即使写出来,也不知道对错。
规约可以隔离“变化”,无需通知客户端 ,客户端不需要知道实现 , 实现者不需要知道如何被使用 。从而很好的实现了解耦。
需要注意的是,规约还有一个重要的作用就是行为等价性,就是说判断两个程序是否等价依据的不是程序的具体实现,而是规约声明的效果是否相同。
规约主要由两部分组成:1前置条件( requires )2后置条件( effects )
前置条件:对客户端的约束,在使用方法时必须满足的条件
后置条件:对开发者的约束,方法结束时必须满足的条件
如果前置条件满足了,后置条件必须满足;反之,前置条件不满足,则方法可做任何事情。
在程序中写成注释的形式就换为@param和@return
2)规约分类
可以将规约分类为确定性的/非确定性的(是否对应一个输入会有一个确定的返回)、陈述式/操作式的(是否将具体实现的每一步都作为规约,推荐使用陈述式的)、强/弱规约(重点讲解)
对于规约的强弱简单理解就是对于输入和实现的限制
对于两个规约,有规约的强度S2>=S1,则对S2:
1.前置条件更弱或相等(不强于),意味着对于客户端限制更少
2.后置条件更强或相等(不弱于) ,意味着对于客户端更有保证
当然,对于两个规约,前置条件和后置条件同时强或同时弱是不可比较的。
总之,越强的规约,意味着实现者的自由度和责任越重,而client的责任越轻。
例子如下:
3)设计好的规约
那我们如何写一个好的规约呢?
评价一个好的规约, 一方面:client用着舒服;另一方面:开发者编着舒服
1. 除非在后置条件里声明过,否则方法内部不应该改变输入参数
2.Spec描述的功能应单一、简单、易理解,如果一个规约描述了多个功能,说明方法不够模块化,应该变成多个方法。
3.信息精炼但不能让客户端产生理解的歧义
4.太弱的规约,客户端会认为实现者的工作不够严密,不敢用;太强的规约又会大大加重实现者的任务。
5.在规约里使用抽象类型,以给方法的实现体与客户端更大的自由度
6.是否使用前置条件取决于(1) 输入检查的代价;(2) 方法的使用范围。如果只在类的内部使用该方法(private),那么可以使用前置条件(方法内部不需要判断输入是否满足,认为client会保证前置条件),在使用该方法的各 个位置进行check——责任交给内部client;如果在其他地方使用该方法(public),那么可以不使用/放松前置条件(在方法内部检查输入是否满足),若client端不满足则方法抛出异常。
总结
本文从数据类型和规约两个具体角度介绍了软件构造的两个重要部分,深刻理解二者,才能更好的应用,实现好的程序。