转眼间第三次作业了,似乎需要说点啥,那就说点。
规格&工业
说到这个,不得不提一下软件开发的发展史。
历史的进程
早在上世纪50年代,就已经有早期的编程语言出现,也开始有一些程序编写者出现(多为资深电子工程师,和半路出家的数学家)。
然而那个时代,软件开发基本局限于自己或自己部门内部的使用,需求并没有很多,也并不复杂(或者说人们并没有意识到有那么多事情可以通过软件解决)。所以基本都是靠早期的程序猿们的自由开发,也并没有现代代码规范的概念。
然而等到了60到70年代,随着硬件技术和工业界思想的进步,软件层面上的需求越来越多,越来越杂,且不再局限于自己的使用,开始有了各类外包的需求(早期的软件作坊、外包公司)。这时候,人们发现代码规模一上来,工程质量将不再那么可控。不仅开发成本随规模急剧上升,且开发过程中的人员与工程管理也出现了很大的麻烦。失败的案例不在少数,工期拖延数月甚至数年的也常常发生,而且还很有可能无法满足用户的需求或者可靠性极差。软件工程迎来了一个动乱的年代。
1968年,北大西洋公约组织的计算机科学家在联邦德国召开的国际学术会议上第一次提出了“软件危机”(software crisis)这个名词。
概括来说,软件危机包含两方面问题:
- 如何开发软件,怎样满足对软件日益增长的需求
- 如何维护数量不断膨胀的已有软件
1968年秋季,NATO的科技委员会召集了近50名一流的编程人员、计算机科学家和工业界巨头,讨论和制定摆脱“软件危机”的对策。在那次会议上第一次提出了软件工程(software engineering)这个概念。软件工程正式被列为工程的一种,并开始走向专业化系统化,以全新的面貌重新步入工业界。
现代的软件工程是一门研究如何用系统化、规范化、数量化等工程原则和方法去进行软件的开发和维护的学科。通过前人的不断努力,人们逐渐解决了软件危机,并认识到规格化设计的重要性,在此期间,一些重要的文档格式的标准被确定下来,包括变量、符号的命名规则以及源代码的规范式。后来随着发展,这些规范逐渐形成了软件开发中的规格化设计,并且由于其高效性与高可靠性,越来越受到软件开发人员的重视。
个人的奋斗
结合我个人嘛,其实也是能说上一些的。由于笔者在早年长期处于自由成长阶段,所以其实很多发展历程,和历史的进程十分相似。
笔者从2年级初学编程(2005年,豆腐块+win2000的年代),最初写的都是一些很小的程序(基本不存在超过100行的情况),用的是面向过程语言和原始的面向对象语言。(Pascal、VB6,后来进阶VB.NET)
说实话,笔者之所以喜欢编程,喜欢计算机,很大程度上喜欢的是创造的快感,尤其是计算机这种低成本高回报即刻见效的创造。于是呢,笔者逐渐在试着编写规模更大程序,来满足一些更加实际的需求。在那个阶段,笔者的开发完全是怎么顺手怎么来,毕竟急于获得那种看到成果的快感,且还没有形成工程的思维(和很多同学现阶段的状态类似)。
然而,笔者在初中那会就发现了一件很头疼的事情——但凡程序超过500行,无论再简单的需求,再明确的目的,自己都会开始控制不住局面。具体的表现为,只要规模一上去,哪怕原本早就想清楚了一切,在debug的时候还是不是这里出错就是那里出错,甚至自以为改好了一边,然后另一边又被带出毛病的情况也时有发生。
那个阶段,可以说,笔者自己软件开发的投入成本随代码量上升而上升的幅度是指数级别的。
笔者曾经被这个问题困扰了非常久,一直处于这样的一个瓶颈期,难以突破。
直到高中,接触了github,上去看了一些各个语言的开源代码之后,才恍然大悟。笔者发现:
- 很多的开源代码,都配备了相关的说明文档,而且格式整齐内容齐全,查找相关信息相当的方便。
- 点开代码之后,发现很多地方都有注释(甚至注释行数和代码行数比例已经接近
1:1
),说明相关位置的功能与需求。(类似于规格的requires和effects) - 不仅如此,连类、方法,甚至局部变量的命名,格式都十分整齐。(求读者别笑,笔者曾经一切数组命名均为
a
、b
、b
、d
,其他变量都乱用单个字母) - 各个类、方法,各司其职,或者说谁都自扫门前雪,不管他人瓦上霜。
于是,解决方案就非常明显了——代码规范化。因为,多花这些时间做好这些工作,对于全局而言,实际上并不降低效率,正所谓磨刀不误砍柴工。
当笔者获得进一步能力提升之后,就开始开发更大规模的程序。
然而,当正式步入工业界时,另一个很现实的问题产生了——对于上层的工程师或架构师,该如何对全局化系统化地设计一个系统?
亲手码代码显然速度太慢,且只能一个人干,不具备可合作性,无法发挥并行优势。然而设计不实现好的话,又该如何描述设计?
答曰——规格。架构师只需要将各个部分的规格设计好,规定好出入口条件等信息,由下位程序猿进行完成即可。既准确描述需求又提供了测试根据,一举两得。
规格与bug相关性分析
规格错误
笔者未被报过规格错误。
bug分析
笔者
在第十一次作业中,被友善好心素质优良情商天下第一的测试者找到了bug,分别是:
- 未过滤
map.txt
内的空白字符 - 未过滤
lights.txt
内的空白字符 - 在直路或者断头路加上红绿灯后未输出错误信息
很明显,这三个均不是内部功能性上的错误。
可以说是笔者的需求分析失误。(前两个隐藏在指导书上的一个角落中,后一个隐藏在第十次作业的issue区。)
相关性分析
由于笔者未被报过规格错误,且bug均为需求研究层面上的疏漏,故不存在相关性。
规格举例
笔者认为,实际上,只要搞清楚规格的作用与意义,很多问题就应该迎刃而解,无需举过多的例子。
反面教材
说到规格的一些不太好的用法,其实最典型的有以下几种
过度使用自然语言
字面意思,有些很好用布尔表达式表达的却偏偏用了自然语言
/**
* @requires: true;
* @modifies: None;
* @effects: result will be the equation between a and b;
*/
应该改为
/**
* @requires: true;
* @modifies: None;
* @effects: \result == (a == b);
*/
使用具体算法表示规格
规格,讲究的是个对于局部抽象功能的描述。说白了,它只关心这个方法或者类应该实现什么样的一个功能,而不关心里头具体是怎么实现的,同时,给出的规格条件必须具备可判定性。
例如程序(GenericPair为泛型二元对类):
public static GenericPair<Integer, Integer> whatCanYouSee(int a, int b) {
int c = a + b;
b = c - b;
a = c - a;
return new GenericPair<>(a, b);
}
仔细看的话,应该不难发现其实功能是交换了a
和b
的顺序后存入了GenericPair
内。
然而规格很容易被写成如下的形式:
/**
* @requires: true;
* @modifies: None;
* @effects:
* c == (\old(a) + \old(b));
* b == (c - \old(b));
* a == (c - \old(a));
* result will be new GenericPair<>(a, b);
*/
而正确的应该是改为前后置约束条件。
/**
* @requires: true;
* @modifies: None;
* @effects: (\result.first == b) && (\result.second == a);
*/
方法内局部变量写入modifies
实际上,笔者见过一些同学,在初学阶段把局部变量的修改也写入了modifies。类似这样:
/**
* @requires: true;
* @modifies: z;
* @effects:
* z = x * y;
* \result == (z + x + y);
*
*/
public static void privateVariableSample(int x, int y) {
int z = x * y;
return z + x + y;
}
不过,还是那句话——规格只关心这个方法或者类应该实现什么样的一个功能,而不关心里头具体是怎么实现的。
局部变量完全是属于方法内部的东西,并不属于外部设计者需要关心的范畴。所以,不应写在modifies内,与之相关的表达式在effects关键字中也应该展开表达。
/**
* @requires: true;
* @modifies: None;
* @effects:
* \result == (x * y + x + y);
*/
public static void privateVariableSample(int x, int y) {
int z = x * y;
return z + x + y;
}
不符合javadoc基本格式规则
其实,这一点是ppt上的错误示范导致的。笔者在第十次作业报了对方一个这样的JSF错误,然而鉴于对方认为ppt上有类似的格式,所以笔者只好先选择了仲裁。
实际上,JSF这样的东西,从一设计,就是要面向自动化的,同时继承了doclet(笔者有幸阅读过JSF源代码),就是为了完美兼容和扩展javadoc。而违反javadoc基本格式规范的行为,显然与这一设计初衷不符。建议课程组看到之后对相关部分进行全面的修改。
错误示范一(第九次PPT,P16):
javadoc基本格式中间行开头都是需要*
的,且首行(两个*
的行)不要写东西。开头的*
也最好严格对齐。
错误示范二(第十次PPT,P9):
理由同上
类似这样的错误示范还有非常多。
此外,其实关于javadoc的格式,在idea(或者说jetbrains系列IDE)中,直接键入/**
并回车即可生成正确规范的格式。
感想
关于规格
其实,根据笔者的了解,严格工程开发是这样的一个流程:
可以看出,规格、不变式在开发、测试环节中扮演了相当重要的角色:
- 为开发组提供了实现程序内部具体代码的需求依据。
- 为测试组提供了编写单元测试的需求依据。
- 为架构师提供了架构依据,和严格论证正确性的依据(在航空航天等对软件质量有极高要求的行业内,这一点尤其重要)
当然,实际上,在一些非高度严格的工程代码中,甚至不写规格也是很常见的。例如笔者参与过的创业公司内的开发,多为敏捷开发,且攻城狮开发经验均较为老道,完全有能力牢牢掌握全局。
不过,敏捷开发对攻城狮自身素养要求是很高的。而如果想广泛地扩大生产力,则这样子成本无疑过高。
这时候,强类型面向对象语言、代码规范、文档规范、规格就得以体现其作用:
- 对底层开发者而言,开发只需要严格按照规格需求,即可撰写程序和文档。程序员工作技能门槛变得很低,有利于广泛招募廉价生产力,创造更多效益。
- 严格约束底层开发者的开发行为,并且可以进行严格的量化考核,管理起来很方便。
- 对于上层的攻城狮和架构师,其编程行为即为规格编写,不再需要具体地去实现每一个内部的细节(或者说只要规格需求能满足,内部实现细节并不那么重要)。工程师工作效率得以提高。
生产力决定生产关系,生产力量变引发社会质变,这样的结论在计算机行业一样适用。因为,工业界,其终极目标永远只有一个——创造更多价值。
关于JSF
JSF根据笔者了解,似乎是以前的某位学姐的毕业论文。以及,似乎课程组对这样一个东西情有独钟。
我们先来看看JSF为人称道的地方:
- 采用布尔表达式,便于自动化生成单元测试*(的确,有了require、effects条件,就完全具备单元测试的基本属性了)
- 轻量级,继承自doclet,可扩展进javadoc
看似,的确是个好东西,而且理论上确实如此。然而,理论和实际总是存在着不可忽视的差距的:
- 采用布尔表达式,于是很多东西变得不再有正常表达的可能(例如,正则表达式判定,难道使用者还需要把整个正则表达式庞大的逻辑像写
verilog
似的表达出来?) - 对override,继承的支持相当贫乏(这意味着,如果想要完全完整的表达表达式,在继承层数较多的情况下,表达式规模将无限膨胀,友好性极差)
- 容错性将是另一个很棘手的问题。用布尔表达式为了什么?很明显最终目的是实现自动化。然而用户只要写的稍有欠缺(哪怕小到完全不影响人工编程和人工测试的地步),单元测试的生成都将出现巨大问题。
- 不仅如此,对于一些无法表达的东西怎么办?
- 用自然语言?那么还打算怎么自动化?自动化生成一小部分然后人工再写?这样高不成低不就的解决方案用户体验并不好。
- 进一步扩大语法?我们暂且先认为存在这种完备的语法设计,就算全都能用布尔表达式表达了,那么这样一个东西轻量化的优点将不复存在。同时,你也会发现,这种完备的表达式,已经和直接写单元测试差不多少了。那么自动化难道就是帮人近似的抄一遍程序?很抱歉,这样的自动化毫无意义,工程师们不可能买账的。与其这样还不如直接写单元测试,然后用脚本将单元测试程序拷贝到对应的方法注释上。
这么看下来,似乎唯一还可以的地方就是javadoc的兼容性设计了。
JSF的设计宗旨是为自动化提供可能,并且具备轻量级特性。然而就目前的JSF而言,可以说是非常尴尬的存在——本是追求自动化的,可是自动化却做得局限这么大,做得这么不到位,而且实际写起来对用户要求还极高,和自然语言比起来体验只差不好。
综上,笔者觉得,如果JSF不良好的解决自动化问题,或者重新对这种工具进行需求定位的话,这样的东西将不存在实际应用到工业界的可能性。(笔者也对这种东西的前景持消极态度)
关于某位大佬
emmm。。。说在前面,如果读者您看不懂这段话在说啥的话,那说明不是写给您的,您可以直接跳至下一个段落,感激不尽 。
我相信您老人家这次一定又在看我的博客。嘛。。。本菜的博客有您这样的粉丝,实属蓬荜生辉。
之前已经给您写了一些话,我已经深刻沉痛深切地反省了我的过错,还望大佬您海涵。
作为本菜对友善好心素质优良情商天下第一的大佬您的赔礼道歉,我为大佬送上一句大佬自己说出来的金句——“对了,情商是个好东西”。
emmm。。。笔者认为,对于这样的大佬这一句还不够,那么,再来两句——“种瓜得瓜种豆得豆”、“己所不欲勿施于人”。
其他更多的话,本菜诚惶诚恐,不敢多言。因为您这样神一般的大佬哪里是我这样的人可以教育的了的呢。
好的,可爱的笔者抽风完毕。接下来正文继续。
关于规格制度设计上的个人意见
其实,规格的意义与重要性,笔者在上面均已经进行了论述。这一点丝毫不值得怀疑。
但是,这样的制度直接照搬进了一个面向小白的OO课程,而且还如此草率地纳入互测考核,真的合适么?
笔者给出的答案是否定的。
首先,在目前这样的课程制度设计下,完全不能体现规格的重要性。最典型的一点,就是先写程序后写规格。这显然是错误的做法,然而,这样做的不仅仅是大部分同学们,还包括课程的设计——出租车作业第一次并不要求规格,从第二次才开始要求。这意味着什么?这意味着大家第二次出租车作业将花费大量的时间补第一次的规格(甚至花在这上面的时间远远多于正确使用规格的时间)。给第一次接触规格的同学们上来就是这样的完全错误的引导,显然是不合适的。
其次,JSF的各种槽点,上面已经吐槽过了,用户体验相当差,此处不再赘述。而且当JSF纳入考核之后,由于不得不使用部分自然语言,而导致大家都使用自然语言描述的情况不断地发生,而后课程组还给出了require必须布尔表达式的要求,然而根据笔者的调查和了解,并没有起到预期的效果。可以说,JSF的强行推广是相当失败的。
不仅如此,我们来回想一下,JSF互测,评判者都是些什么人——一样尚未形成工程思维,一样不熟悉规格设计的小白同学。把这样的东西的评价权力直接交给自顾不暇的初学者,或者说得更直接点,让不懂工程的人强行评判工程的好坏,这样的做法显然很荒唐。这样只会导致测试者无从下手(甚至干脆选择不讲道理乱扣分),而被测试者的公平与利益毫无保障。
不同于BUG互测(实际上笔者支持保留BUG的互测),JSF这东西很多同学都是第一次接触,工程思维很多同学都还没有形成。也许对于职业攻城狮和其他从业人员而言,他们心里都一把评判的尺子,也具备最最基本的职业道德和职业素养。但是对于初学阶段,各方面水准良莠不齐的学生而言,这样的做法显然太过于理想化了。
此外,课程组还似乎单纯的想依靠高伤害和仲裁机制来保证大家的重视与制度的公平性。但是,在目前这样双方都一脸迷茫的状况下,这样是毫无效果的(甚至可以说弊远远大于利)。盲目的高伤害只会导致大家越来越没有努力的动力(因为再努力再踏实也没有有些良心从来不会痛的投机分子过得舒服,自己的努力在高伤害面前一文不值),而滞后相当严重的仲裁制度更是导致大家不再那么相信正义的存在,可谓雪上加霜(迟来的正义,常常和没来没有区别)。
然而,据笔者所知,课程组似乎在根据通过这样收集到的数据来进行数据分析,而且似乎还分析了与bug的相关性,不仅如此,甚至还得出了同学们不够重视JSF的结论。对此,我只想说,这样的数据,与其说是JSF评判的最终结果,倒不如说是博弈的最终结果,公平性真实性根本保证不了。用了个错误的前提条件,获取了所谓预期的分析结果,是毫无意义的。
综上,笔者觉得不应该在使用互测制度来考核JSF,而应该将评价权交给更加专业的人士。一方面保证同学们能受到正确的引导,另一方面也保证制度的公平与合理,此外,也保证数据收集工作真正的客观性与有效性。