OO课程目前已经进行了三次的作业,容我在本文中做一点微小的工作。
第一次作业
第一次作业由于难度不大,所以笔者程序实际上写的也比较随意一些。(点击就送指导书~)
类图
程序的大致结构如下:
代码分析
可以看出,整体的功能还是相对零散的,耦合状况也基本还可以。然而类似Main.main
、Polynomial.Polynomial
两个函数的复杂度仍有点高。笔者后来查阅了阿里Java开发规范手册,发现两个问题:
- 单个方法的长度不宜过长,入口点方法(
Main.main
)也是一样 - 不宜在构造函数中携带过多的计算逻辑。手册第七页,第11条中也有明确的规定:
11. 【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
笔者的程序这一点就有待修改,同时,类似
public Polynomial(String str) {
// something inside
};
这样的构造函数,更适合写在类似
public static Polynomial parsePolynomial(String str) {
// something inside
}
的静态方法中。
公测
我方
公测不出意外,笔者的程序在公测阶段没有出现任何错误。
对方
对方的程序在公测被测出了一个bug,是栈溢出,溢出的位置是正则表达式。
对方试图用一个规模庞大的正则表达式来先行判断整个表达式是否合法,于是在遇到规模较大的数据时出现了栈溢出的情况。
互测
我方
不出意外,笔者的程序未被测出bug。
对方
对方的程序被测出了一个bug,对方仍在对象内使用传统的int
数组,而且空间开的非常小,最终导致数组越界,程序出错(然而事实上对方在顶层进行了Exception的catch,故没有出现crash)。
反思
第一次由于任务算法难度很低,且工程量也不很大(小几百行的量,甚至据说部分大佬压缩到了100行内),所以笔者的很多写法较为随意,大约整个工程用时一小时不到。所以在工程性方面没做过多的考量,架构也没有进行很充分的优化。
第一次据笔者观察,很多同学还对java这门语言完全处于不熟悉的状态,对于正则表达式等概念及其具体原理也完全不了解,更不用说面向对象的设计思想了。据笔者所知,像这样试图用一个庞大的正则表达式判断格式的同学并不在少数。然而了解正则表达式相关原理的同学都应该清楚,正则表达式不是让你这么用的。正则表达式更多的用于相对简单且没有复杂的重复和嵌套的一些的模式匹配,以及其内部关键位置信息的提取。(更多信息和算法原理可以翻阅Wikipedia - Regular Expression,大致原理为有限状态自动机,本文不再赘述。)
此外,在C#、Java这类的语言中,直接使用传统数组在大部分情况下并不是明智的选择,Java中没有像C++那样提供很完善的面向用户的指针机制,为的是一定程度上的系统安全性。而传统数组存在灵活性极差的问题。简单来说,数组开小了,会crash;数组开大了,空间的浪费很严重(甚至有部分大佬还在适用oi/acm味满满的长度高达100050超大数组[滑稽])。所以,更推荐各位优先使用Java封装好的数据结构,例如序列类的Vector
、ArrayList
(实际上后者效率更高,推荐ArrayList
),例如Key-Value类的HashMap
(笔者在后来还自行继承封装了一款支持默认值的HashDefaultMap
),而不是去自己实现一个所谓的数据结构。或者也可以这样说,你能想到能做到的算法封装,人家Java作者早就想到了。IT界从来不缺勤奋的轮子工,需要的是聪明的懒人。
第二次作业
第二次作业,是实现一个傻瓜电梯。问题一如既往的简单,但是真正一去写,诶?细节这么多?恩,真正的工程终于来了。(点击就送指导书~)
类图
第二次作业,笔者的程序结构大致如下(图片规模略大):
笔者将所有的输入请求抽象为了Request
类,再将操作和RUN指令分别继承为了OperationRequest
和RunRequest
,OperationRequest
类再继承为内部请求类(InnerRequest
)和外部请求类(OuterRequest
)。
代码分析
可以看到,这一次的耦合状况较上一次有好转(没有出现红色字)。然而依然存在部分方法复杂度略高的情况(比如入口点函数,依然是红字状态)。看来,功能还需要进一步拆散。
公测
我方
第二次作业中,笔者的程序公测出现了一个bug。后来经检查,是因为没有仔细研读guide book上的一个功能需求而导致的功能缺失造成的(没有支持数字前的+
)。
对方
对方在公测中没有出现任何bug。
互测
我方
我方一开始有一处被对方认定为imcomplete,这段代码如下:
package configs;
/**
* 全局设置类
*/
public abstract class ApplicationConfig {
/**
* 层数限制
*/
public static int default_max_floor = 10;
public static int default_min_floor = 1;
public static int default_present_floor = 1;
/**
* 时间戳限制
*/
public static long default_max_timestamp = 0xffffffffL;
public static long default_min_timestamp = 0L;
/**
* 默认电梯数量
*/
public static int default_lift_count = 1;
/**
* 最大可接受的合法请求数
*/
public static int max_valid_requests = 10000;
}
对方给出的理由是,guide book上面规定不准使用public。然而,根据笔者之前长时间大量的工程经验来看,这样的写法还是很常见的(public static
来设置常数的形式)。
况且,笔者在guide book中找到的原文如下:
必须要实现电梯、楼层、请求队列、调度器、请求这五个类,且类中不允许出现 Public 属性。
说的是public属性是不被允许的,那么public static
究竟算不算在内呢?笔者先后问了多个助教,助教们之间意见也不是很统一(总体还是认为可以使用public static
的更多)。
后来笔者基于这个问题与吴际老师进行了交流,老师的意见可以归结为如下几点:
- 这种写法不是不可以,但是
public static
的值仍有被非法篡改的可能。 - 比如,使用
public final
来将设置类的值限定为常量可以有效地防止修改。 - 比如,使用getter方法来进行静态的访问也可以从根本上杜绝非法篡改。
笔者后来再次翻阅了代码规范手册,发现一般常量类的东西需要使用static final
关键字,同时命名方式一般为全部大写并按照单词来下划线分隔(例如:YES
、UNKNOWN_REASON
等)(详见手册第3页,常量定义)。
以及,最终看了下助教仲裁的结果,结果是不算bug。
对方
很不巧,这次没有发现对方的bug。不过可以看的出来,是一个逻辑思维清晰,但是代码规范和工程思维比较欠缺的同学。
总结
第二次作业某种意义上算得上是个真真正正的工程了,笔者这次虽然还是有点小遗憾,但总算是找到了久违的OOP工程的手感。
笔者的程序部分地方还是存在代码质量问题,还是有进一步改进的空间的。
此外,因为guide book读漏了而导致的公测bug这个实在是不应该的,以后必须要注意仔细研究需求。
第三次作业
第三次作业是第二次作业的升级版,采用了相对智能的电梯调度措施,然后需求细节一样较为繁琐。(点击就送指导书~)
类图
笔者的程序结构如下(图片规模还是较大):
笔者在第二次LiftController
类外部套了一个Scheduler
进行更加智能的调度。
此外,笔者为了方便在调试时看清楚整个程序内部的计算逻辑细节,设置了Debug信息输出接口。(详情见【技巧】Java工程中的Debug信息分级输出接口)
代码质量
可以看出,还是老毛病,有些核心方法的规模过大。
公测
我方
笔者的程序在公测环节没有出现任何错误。
对方
对方的程序在公测环节出现了一处错误,出错位置在边界情况测试
-->副请求时间==主请求开门时间,不捎带
。
互测
我方
笔者在公测环节找出了对方程序的两个bug:
- 对方的程序在一个地方存在queue对象非法访问的问题(准确的说,是越界访问)
- 对方的程序在处理一次性处理多个请求的时候,并未严格按照输入顺序进行输出(笔者判断的没错的话,对方应该是将主请求进行了单独处理,从而导致了这样的错误)
对方
不出意料,对方未发现笔者程序的bug。
总结
第三次作业没有再犯第二次作业的低级错误,也没有被挑出bug。
然而实际上,第三次作业仍然有着一些的缺陷:
- 和第二次作业一样功能不够分散
- 由于需求分析花了非常多的时间,导致这次作业起步时间很晚,很多架构实际上并不是很好的设计(笔者写程序的时候自己就已经在这么觉得,然而时间紧迫还是选择了优先完成任务)
等到下次作业,笔者会对一部分的架构进行大改,争取用更优的架构来面对接下来的project。
此外,还是必须吐槽一句,guide book中很多该明确的需求并没有明确到位。也许现实中的开发真的没那么特别明确细致的需求,然而我们的公测环节却有着无比明确的细节需求(而实际项目中的此类细节需求很大一部分是开发者自己定义的,其余的是甲乙双方协商确定的),这两者之间,显然存在着不可调和的矛盾。希望OO课程组就这一问题进行制度上的改善。
总结
以上三次作业,让笔者感到自己还有一些不足:
- 部分方法依然复杂度偏高(有的已经接近100行),应该根据其功能模块进一步拆分
- 对于需求很多时候还是没有弄得特别清楚,导致第二次公测挂掉了一个点(实际开发中,需求还是非常重要的)
经过了研读阿里Java代码规范手册后,笔者意识到自己在OOP方面仍然有不少需要进一步规范和改进的地方。笔者之后会尽力做到:
- 充分明确需求
- 动手写代码之前仔细研究架构的合理性和扩展性
- 严格遵循代码规范
其他
笔者在发布此文章之前,听过不少来自身边同学的各类吐槽,也看了一些其他同学已经发布的文章,感觉不少同学对有些事情的认识还是存在着一些误区,笔者准备在此结合自己在工程开发方面的经验和踩过的坑,和大家聊一聊。
代码越短就越好?
之前看到一些同学的作业,不少作业里面都在说自己的程序写的还不够好,下次争取精简的更短。
其实,这是个很错误的认识。代码短等于代码质量高吗?当然不是!
举个很经典的例子,让我们来看几段程序:
/* 程序一 */
public static void main(String[] args) {
int i = 2;
System.out.println(i++ + ++i);
}
/* 程序二 */
public static void main(String[] args) {
int i = 2;
int j = i++;
int k = ++i;
System.out.println(j + k);
}
/* 程序三 */
public static void main(String[] args) {
int i = 2;
int j = i;
i += 1;
i += 1;
int k = i;
System.out.println(j + k);
}
好了,现在请告诉我,这三个程序的运行结果都是什么。
没错,输出的结果都是6
,这三个程序是等价的。那么请告诉我,哪个程序你最先看懂,哪个程序你最先计算出了结果?
我想大部分人的顺序都是:程序三
->程序二
->程序一
。然而程序从短到长的顺序是什么呢?完全和这个相反。
让我们回到这个问题的定义上来——究竟什么样的代码叫做高质量代码?我们来思考几件事:
- 代码是谁写的?人写的。
- 代码是干什么的?实现功能的。
- 实现功能是干什么的?创造价值的。
那么我们再来思考一个问题:从结构化程序设计,到面向对象编程,甚至到后来的函数式编程,这些东西存在的意义是什么?
毫无疑问,最终目的肯定是创造更多的价值。而决定能否创造价值、创造多少价值最核心最根本的因素,是写代码的人。
这下很明显了,这一切的发展,围绕的都是开发程序的人。说的更直接一些,能有助于提高开发者的开发速度,提高开发者对程序的维护和扩展能力,提高团队合作效率的代码,就是高质量代码。
这么看来,像刚才那样第一个程序,的确很短,也不得不说能驾驭的了这种程序,开发者也肯定挺厉害。但是这种东西真的能让自己以外的人(甚至自己过了一段时间后)快速的理解么?显然不能。所以类似这样的,短小精悍但是实际上不利于整体开发效率和质量的代码,也是很糟糕的代码。
当然,说了这些,并不是说短小的代码有错。而是,不应该为了短小而短小,而应该是为了让程序更加清晰可读而将程序进行适度的精简。
System.out调试很低级?
看到过一些同学(作业里的和身边的都有)之前在抱怨,自己只会输出调试如何如何如何。。。。
然而我还是和上一节一样,一句话:这一切,围绕的都是开发程序的人。说的更直接一些,能帮你更快定位和找出bug(包括程序bug和逻辑bug)的办法,就是好办法。
笔者在第三次作业中,从ubuntu系统ssh
的DEBUG模式获得了一些灵感,自己开发了一个可用于快速debug的分级debug信息输出控制模块。(更多信息详见【技巧】Java工程中的Debug信息分级输出接口)
实践表明,笔者使用此输出调试时,一样可以很快的定位错误,甚至还具备了一般的debugger较为欠缺的逻辑bug发现能力(实际上debugger使用者一般容易更多的倾向于局部的程序bug,而具有连贯前后文的输出调试则可以很立体的将逻辑bug展现在你面前)。
以及,再次声明,说这些,不是在反对使用debugger。而是想告诉各位,适合自己的,就是最好的。
代码规范不重要?
之前,在QQ群上讨论了一下代码风格的问题,然后马上就有一位ACM大佬,出来对这件事情嗤之以鼻。
这位大佬的逻辑大概是——代码风格好就没bug了?代码乱七八糟就有bug了?
好,不说别的。首先,问问各位,出自你们自己之手的电梯程序,如果全篇没有一个注释,代码写的乱七八糟,到处都是奇奇怪怪的英文缩写甚至中英文混搭,大小写也在乱用。
请问,一个月后你们自己拿来再看看,你还能记得多少,随便找个方法你能讲得出来参数都什么意义返回的是啥不?事实证明,基本没人能做到,甚至可以说大部分代码都不记得了(是的,不要对人类的那点记性太过乐观)。
然而实际上,独立的开发是很少见的,哪里有项目开发,哪里就有teamwork。你拿着一个自己都看不明白甚至都不愿意看的程序,来和别人合作完成项目,指望别人比你自己还了解你的程序?然后别人根本用不了,你得改来改去。后来,别人终于调用上去了,然后由于各种奇奇怪怪的问题导致不兼容(不可能?自己去看看阿里Java手册,这种事情多得是)。这开发效率可能有多高呢?在这个时间就是金钱就是机遇的时代,把大量时间浪费在这种本可以避免的地方,无异于自掘坟墓。
然后好不容易终于跑起来了,下次再维护,发现之前的代码又带来了各种坑,又得继续从自己挖的坑里头爬出来继续挖坑再跳进去。。。。毫无疑问,这分明是一个恶性循环。
试想,如果你是个人开发者,你的代码严格遵循一定的规范,并按照规范写了注释。那么无论过了多久,你都能很快的看明白自己的程序并进行有效的维护。
如果你是开发团队,严格遵循了代码规范,那么在不同人代码对接的时候,大家遵循的都是同一个标准,可以减少很多不必要的沟通麻烦和兼容性问题,极大的提高开发效率。
而对于纯算法竞赛选手,他们写程序的模式永远就是 写代码
->调试
->AC
->AC不掉继续调试
->AC
->写题解
->扔掉
这么几个步骤。而比赛场上(ACM为例)一共就五个小时(而且一般还都是各写各的,所谓的teamwork也仅仅是交流思路),平时刷题一个题撑死了也没几天(甚至很多题就是几小时几十分钟的事),完全没有任何长期维护和团队开发的需求在内,他们当然会选择怎么用的爽怎么来。
忽略代码规范重要性的,只能说他们根本没体验过被自己挖的坑坑无数遍的痛苦,更不懂效率对于一个项目(尤其是初创项目)的重要性。
此外,对于有些常数优化控,且不说在现代的编译环境下你们那一套还能不能奏效,就算都奏效,请自己上腾讯云看看云服务器一个月多少钱,再看看一个科班出身的程序猿一个月工资多少钱,谁轻谁重自己掂量掂量吧。。。