Functions & methods in programming languages
Method:
Method意思是方法。在一个类中可以定义很多个方法,它就像是这个类的一部分,在创建一个这个类的实例化的时候,可以通过调用类中的方法来实现一些功能。有的类中可能会包含一个主方法,可以通过运行这个主方法来实现一个类似客户端的界面
public static void threeLines() {
//STATEMENTS
}
public static void main(String[] arguments){
System.out.println("Line 1");
threeLines();
System.out.println("Line 2");
}
Parameters:
Parameters意思是参数。在定义一个方法或者函数的时候进行传递的就是参数。值得提醒的是,参数类型是否匹配在静态类型检查阶段完成。
public static NAME(String NAME, TYPE NAME) {
//STATEMENTS
}
Return Values:
Return values意思是返回值。在定义方法的时候需要规定该方法的返回值,如果方法无返回值则使用修饰符void
。同样的,返回值类型是否匹配也发生在静态类型检查阶段。
public static void NAME() {
//STATEMENTS
return EXPRESSION;
}
方法可以想象成程序的积木,可以被独立开发、测试、复用。使用方法的客户端,无需了解方法内部具体如何工作,这也就是抽象的含义
一个完整的规约:
public class Hailstone {
/**
* Compute a hailstone sequence.
* @param n Starting number for sequence. Assumes n > 0.
* @return hailstone sequence starting with n and ending with 1.
*/
public static List<Integer> hailstoneSequence(int n) {
List<Integer> list = new ArrayList<Integer>();
while (n != 1) {
list.add(n);
if (n % 2 == 0) {
n = n / 2;
} else {
n = 3 * n + 1;
}
}
list.add(n);
return list;
}
}
Specification: Programming for communication
(1) Documenting in programming
实际上,Java会在编译的时候检查你所做的假设,并且保证你的假设程序得其他地方不会被违反。同样的final关键字决定了你的设计决策,就是不可改变,也会在编译的时候进行静态检查。(注:这里的假设的意思我的理解是:假设所描述的内容就是方法的功能和参数,假设方法使用什么样的数据类型的参数以及返回值来达到某种功能。个人认为其实可以理解为规约。)
为什么要把做出的假设写下来呢?首先,你自己会忘,时时刻刻记住自己的假设是什么是在给自己制造困难;其次,别人也更加难以理解。如果不将这些假设写下来,别人在阅读代码的时候就需要更多时间来理解方法的功能。
代码和规约同样都蕴含着你的设计决策,那么代码中的设计决策就是给编译器读的,而规约中的设计决策就是给自己和其他阅读人读的。
(2) Specification and Contract (of a method)
在团队合作的程序中,规约是团队工作的关键。因为如果没有规约就没法分派任务,就没法进行编程;即使将代码写出来了,也不知道代码运行是否按照要求了。规约以一种类似于合同的方式进行工作。即方法的实现者负责满足这份合同的要求,使用这个方法的客户可以依赖这份合同。Spec给“供需双方”都确定了责任,在调用和实现的过程中双方都要遵守。
Why specifications?
Reality:
其实很多bug来自于调用实现双方的误解,尽管所有程序员在自己的脑子里都有一个关于程序的规约,但是不是所有程序员都会在编程的时候将它写下来。如果不写下来,那么不同的开发者在看这段代码的时候就会产生误解。而且,如果程序运行得不到想要的结果,不懈规约也难以定位错误。
Advantages:
精确的规约有助于区分责任。当你真的身处麻烦中的时候,规约可以救你于水火之间。而且客户端在调用方法的时候,无需读懂被调用部分的代码,只需要裂解Spec即可。
规约还可以隔离变化,相当于一道防火墙,实现过程如果发生了改变也不用通知客户端,只要满足规约就可以了。而且实现者也不需要保证输入的正确性,因为那是属于调用者的责任。同样,实现这也无从得知自己的方法是怎么被使用的。
Behavioral equivalence
要确定行为等效性,问题是我们是否可以用一个实现替代另一个实现
static int findFirst(int[] arr, int val){
for (int i = 0; i < arr.length; i ++)
if (arr[i] == val) return i;
return arr.length;
}
static int findLast(int[] arr, int val){
for (int i = arr.length - 1; i >= 0; i --)
if (arr[i] == val) return i;
return -1;
}
这两个方法的目标分析起来是一样的,都是采用遍历的方法查询指定元素并返回元素下标,只是在细枝末节的地方有细微的差异。当指定元素的值没有找到的时候findFirst会返回数组长度而findLast会返回-1;再有多个元素与目标值相同的时候,findFirst会返回第一个遇见的元素而findLast会返回最后一个
但是,当每一种值在这个数组中只出现一次的时候两个方法的行为就是等价的,所以对于这样的客户来说,两种方法具有行为等价性。总结的来说,行为等价性是站在客户端的角度来讲的。
简短的总结一下:单纯地看代码实现是不足以判定Implmentation是否是行为等价的,需要结合开发者和客户端之间的Spec进行判断。因此,在编写代码之前,需要弄清楚Spec如何协商形成、如何撰写。
(4) Specification structure: pre-condition and post-condition
规范的规约是有规范的结构的,有前置条件和后置条件
前置条件Precondition:
requires。规约的前置条件是对客户端的约束,即客户端在使用这个方法的时候必须要满足的要求。
后置条件Postcondition:
effects。规约的后置条件是对开发者的约束,即开发人员在完成方法后需要满足的条件。
异常行为Exceptional behavior:
规约中做的规定,如果违反前置条件那么方法会怎么做,返回什么值。
规约还定了这样一个契约:如果前置条件满足了,那么后置条件也一定要被满足,即返回合适的值、抛出指定的异常或是修改或者不修改对象等等。那么同样的,如果前置条件没有被满足,那么方法可以做任何事情,客户端违约在先,实现着自然不需要遵守承诺
Java语言中的规约
静态类型的声明:
Java中静态类型的声明是一种规约,这个规约在静态检查阶段由编译器来自动完成。
方法前的注释:
更详细的要求可以卸载方法前的注释,但是这些注释需要人工来去看,判断他们是否在方法中 被满足。我们一般使用@param来标记参数Parameters,使用@return和@throws来标记返回 值和抛出的异常。下方是一个标准的规约实例。
/**
*Determine inside angles of a regular polygon.
*
*There is a simple formula for calculating the inside angles of a polygon;
*you should derive it and use it here.
*
*@param sides number of sides, where sides must be > 2
*@return angle in degrees, where 0 <= angle < 360
*/
public static double calculateRegularPolygonAngle(int sides){
throw new RuntimeException("implemnet me!"); //待实现的方法体
}
再看一个挑错小练习,简述一下图中的问题:
- 规约的开头是/**而不是/*。在IDEA(Java的IDE)中,/**开头/*结尾会被标记为Spec段,编译器会帮助你进行检查参数返回值的个数,不使用/**将无法识别。
- 没有@requires和@effects
- 参数和返回值只需要说明含义,不可以数据类型。
在一个规约中,它可以描述方法的输入参数和返回值,但是他一定不可以涉及到实现着在实现过程中使用的到的本地变量或者方法内部的private
区域(不讨论方法的类的局部变量或私有域)
/**
*requires: list1 != list2
*effects: modefies list1 bt adding the elements of list2 to the end of it,
*and return true if list1 changed as a result og call.
·········
*/
static boolean addAll(List<T> list1, List<T> list2)
/**
*requires: nothing
*effects: return a new list t where t[i] = lst[i].toLowerCase().
··········
*/
static List<String> toLowerCase(List<String> lst)
除非前置条件里声明过,否则方法内部不应该改变输入参数的值。所以在设计规约的时候,应该尽量遵循这个规则,也就是尽量不设计mutating的Spec,避免引发bug。这个原则也是程序员之间的默契和信任:除非Spec上面明确写了,否则就不去改变输入参数的值。
尽量避免使用mutable的对象
可变的对象会使简单契约变得复杂。如果在程序中有多个引用指向了同一个对象的话,也许意味着你的程序的很多个位置都依赖着同一个对象来保持稳定,可能这些位置非常分散也相距很远。
可变数据类型在使用的时候可能对客户端发出修改的要求,这就需要完全相信客户端开发者的良心;也可能实现者在实现的过程中使用了别名,这又需要相信开发者。显然,这都是不可靠的。而解决这个问题的关键就在于不可变,在规约里限定住
Designing specifications
(1)Classifying specifications
How deterministic (不可逆转的)
规约的确定性。对于一个方法,方法的返回值可能不是唯一的,那么规约的确定性就是描述方法的输出是否是确定的。即Spec定义了一种唯一的输出,还是允许实现者从许多可能的输出中选择一个。给定一个满足precondition(先决条件)的输入,其输出是唯一、明确的
How declarative
规约的陈述性。描述的是规约对于问题阐述的程度。即Spec只是简单地描述了一下输出,还是连同计算的方法也一并描述出来。
How strong
规约的强度。描述的是规约限制更强还是更弱。它用于判断哪一个规约更好。下面细说一下规约的强度。
规约强度
规约的强度用于比较两个规约,来判断是否可以用一个规约替换另一个。
如果我们说规约的强度S2>=S1,那么需要满足:
- S2的前置条件更弱于或者等于S1
- S2的后置条件更强于或者等于S1
满足这两个条件那么我们就可以使用S2来替换S1。也就是说Spec变强=更宽松的前置条件+更严格的后置条件。用更少的要求来完成更多的承诺
(2)Diagramming specifications
一个规约会给所有可能的实现确定一个空间范围,也就是画一个圈。对于某个确定的实现,如果他满足规约,就落在规约画的圈子里;反之则落在圈外。程序员可以在规约的范围内自由选择实现方式,客户端在使用的时候也不需了解具体使用了哪个实现。而且,更强的规约,表达为更小的区域。
更强的后置条件意味着实现的自由度更低了——在图中的面积更小
更弱的前置条件意味着 实现时要处理更多的可能输入, 实现的自由度低了——面积更小
(3) Designing good specifications
写一个很棒的方法首先要写一个很好的规约。一个好的方法设计并不是在于代码写得多么好,而是对方法的Spec有一个好的设计。好的规约应该是简洁清晰,有良好结构并且容易读懂的。有这样的规约一方面客户端用着舒服,另一方面开发者编程也更加清晰。
coherent(内聚的)
- Spec描述的功能应该单一、简单、易于理解。他不应该有很多种情况的分述,比如`if-statements``等等。让我们看看下面这个例子:
在这个规约中,除了使用全局变量和使用打印而不是返回的糟糕操作之外,它也不是内聚的——这个方法做了两件事,应该拆分开形成两个方法。
informative(信息丰富的)
根据规约的返回情况应该知道这个方法进行到了什么程度,下图的例子中,如果我们得到了返回值null
那么我们就不知道方法到底是因为key不存在返回null,还是因为key的值就是null。这是不好的规约,会让客户端产生歧义
足够强壮的:
对于一些太弱的规约,客户端在使用的时候也不能放心,因为它给出的承诺太少了,因此,开发者应该尽可能考虑到所有的特殊情况,给出处理措施。下图中的例子就没有阐明如果遇到了null之后参数怎么变化,以及已经改变了的参数是否要保留。
太弱的spec,client不放心、不敢用 (因为没有给出足够的承诺)。 开发者应尽可能考虑各种特殊情况,在post-condition给出处理措施
别太强壮的:
太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难 度(client当然非常高兴)。
The specification should use abstract type(抽象类型):
在规约中可以使用抽象类型,这样会使程序更加的自由,适用性更强,可以给方法的实现提和客户端更大的自由度。例如在规约中使用List
而不是非常具体的ArrayList
或是LinkedList
。
客户端不喜欢太强的Precondition,不满足Precondition的输入会导致失败。惯用做法是:不限定太强的Precondition,而是在Postcondition中抛出异常:输入不合法。是否使用前置条件取决于:
- 检查的代价
- 方法的使用范围
如果只在类的内部使用该方法(private),那么可以使用前置条件(方法内部不需要判断输入是否满足,认为client会保证前置条件),在使用该方法的各个位置进行检查,并将责任交给内部客户端;
如果在其他地方使用该方法(public),那么可以不使用/放松前置条件(在方法内部检查输入是否满足),若客户端不满足则方法抛出异常。