BUAA OO 第一单元总结
程序结构
第一次作业
-
类图
上图(图1)中Gram是语法分析类,Expression是多项式类,Item是项类。
-
基于类的度量
Class | CSA | CSO | LOC |
---|---|---|---|
Expression | 1 | 16 | 42 |
Gram | 3 | 27 | 187 |
Item | 2 | 18 | 62 |
MainClass | 0 | 13 | 8 |
参数解释:
CSA: 计算每个类的属性(或字段)总数。
CSO: 计算每个类的操作(或方法)总数。
LOC: 计算类代码行数。
由上表(表1),第一次作业功能计较简单,所以只有语法分析类Gram的代码行数较长。
-
基于方法的度量
方法数量较多,只展示较复杂的方法与关键的方法。
Method | CONTROL | LOC |
---|---|---|
Gram.number() | 5 | 25 |
Item.toString() | 8 | 32 |
Item.derive() | 1 | 10 |
Expression.toString() | 4 | 20 |
Expression.derive() | 1 | 7 |
参数解释:
CONTROL: 计算每个方法控制分支数目。
LOC: 计算每个方法的操作(或方法)总数。
由上表(表2),以上是控制分支数量最多的几个方法,其中toString方法是因为要优化输出,有许多判断分支。
-
类的内聚与耦合
Class | CBO | LCOM |
---|---|---|
Expression | 3 | 1 |
Gram | 3 | 1 |
Item | 2 | 1 |
MainClass | 2 | 1 |
参数解释:
CBO:计算每个类“耦合”的类或接口的数量。如果一个类依赖于另一个类,或者这个类依赖于另一个类,则将它声明为与另一个类耦合。由于继承而产生的依赖项不计算在内。
LCOM:计算一个类的内聚程度。使用Hitz和Montazeri设计的LCOM度量的一个变体,它更适合Java。这个度量表示,如果一个类的两个方法共享一个变量使用,或者一个方法调用另一个方法,那么它们是相关的。然后,度量是方法关系图的组件数量的计数。值1表示一个高度内聚的类,它不容易被划分为更小的类。较高的值可能表明类可能“做得太多了”,应该拆分。
由上表(表3)可知,类的内聚性很好,因为第一次作业功能比较简单,类数目较少,所以耦合的类的数量占总类数量的百分比较高。
-
优缺点分析
-
优点
-
基于递归下降实现,可以较为清晰的实现函数的识别,也为后续作业打下基础。
-
-
缺点
-
没有为之后的作业留下接口,实现之后的作业需要重构
-
-
第二次作业与第三次作业
由于本人设计二三次作业基本没有什么区别,只在第三次作业中补全了错误处理接口error,所以在一起展开,此外相比于第一次作业基本上算是重构了。
-
类图
虚线是继承关系,实现是依赖关系。
上图(图2)中Gram是语法分析类(递归下降)。
Add是加法类,Multiply是乘法类,Tri是三角函数类,Bracket是嵌套函数类,这四个类都能嵌套其他类,Power是多项式类,Constant是常数类,这两个是基本类。这六个类都实现了接口Drive。
-
基于类的度量
Class | CSA | CSO | LOC |
---|---|---|---|
Add | 3 | 37 | 264 |
Bracket | 3 | 37 | 170 |
Constant | 2 | 34 | 80 |
Gram | 4 | 29 | 295 |
MainClass | 0 | 13 | 17 |
Multiply | 3 | 36 | 235 |
Power | 2 | 34 | 116 |
Tri | 4 | 36 | 200 |
由上表(表4)可以看出能嵌套的类Add,Multiply,Tri,Bracket代码行数都比较多,Gram语法分析类的代码行数也很多。
-
基于方法的度量
方法数量较多,只展示复杂度高的方法和关键方法
Method | CONTROL | LOC |
---|---|---|
Add.addMember() | 15 | 50 |
Multiply.addMember() | 15 | 58 |
Tri.addContent() | 11 | 41 |
Bracket.addContent() | 7 | 27 |
Add.toString() | 14 | 53 |
Multiply.derive() | 5 | 32 |
Add.simply() | 3 | 16 |
由上表(表5)可以看出addMember(或addContent),toString方法控制分支数量过多,这是由于在这两个方法内都有化简操作,addMember要实现合并同类项与去嵌套,toString要实现优化输出,比如x**1要输出x。个人认为addMember函数的控制分支数量有下降空间,其控制分支很高是因为每次addMember时,要判断instance of Add、Multiply、Bracket、Tri,然后再调用simply函数。如果将Add、Multiply、Bracket、Tri继承一个抽象类Nest(嵌套),然后Nest来实现Derive接口,这样的架构会更好,就不用分别判断了,只需判断instance of Nest即可。
改写后类图如下(没有具体实现):
Derive为接口,Nest(嵌套),Basic(基本)为抽象类,虚线为继承关系,实线为依赖关系。
本人虽然没有进行重构,但是在构思时也能发现,按照上图(图3)的架构实现的话,会使方法控制分支数量大大降低,也可以减少类的代码行数(可以继承抽象类的方法,避免重复造轮子),也会降低不同类之间的耦合度。
-
类的内聚与耦合
Class | CBO | LCOM |
---|---|---|
Add | 6 | 1 |
Bracket | 5 | 1 |
Constant | 6 | 4 |
Derive | ||
Gram | 9 | 1 |
MainClass | 2 | 1 |
Multiply | 6 | 1 |
Power | 4 | 2 |
Tri | 8 | 1 |
-
优缺点分析
-
优点
-
采用递归下降分析,很容易进行错误处理以及表达式树的建立
-
之前构建表达式树为二叉树,但是二叉树很难优化,最后用列表来存储加法、乘法中的成员,使得表达式树可以有n个分支,便于合并同类项等优化
-
-
缺点
-
层次深度不够,耦合度过高,采用以上流图的结构重构后,耦合度应该会下降,架构会更清晰。
-
一些方法还可以进行分离成两个以上方法来重复调用,实现高内聚。
-
-
BUG分析
- 第二次作业由于本地测试不充分,出现的BUG很多:
-
没有进行深拷贝
-
出现BUG的方法,derive,addMember,由于没有进行深拷贝,传入的参数经过 这两个方法后值发生改变,导致了BUG。
-
代码行与圈复杂度差异对比
method v(G) LOC Add.addMember() 17 50 Multiply.addMember() 17 58 Tri.addContent() 14 8.0 Multiply.addMember() 17 13.0 Average 3.5 12.6 表7 参数解释:
v(G): 计算每种非抽象方法的圈复杂度。 循环复杂度是对每种方法中不同执行路径数量的度量。
LOC: 计算方法代码行数。
由上表(表7)可以看出出现BUG的方法的圈复杂度v(G)明显高于平均值,此外addMember方法的行数也明显多于平均值。可以说,圈复杂度高,代码行数长的方法是出现BUG概率较大的地方。
-
-
对合并同类项的eqauls方法与approach方法设计逻辑上出错
-
出现BUG的方法equals,approach,这两个方法是用来合并同类项的。
-
代码行与圈复杂度对比
method v(G) LOC Bracket.equals() 6 16 Tri.approach() 6 16 Multiply.equals() 5 15 Add.approach() 4 14 average 3.5 12.6 表8 由上表(表8)可见,出现BUG的方法的圈复杂度与代码行数也略高于平均值。以后设计时要重点关注这部分。
-
-
- 第三次作业强侧没有出现BUG,互测中有一处笔误的BUG,就不进行对比了。
HACK策略
我对与HACK时用的数据是自己本地测试时的数据,没有进行具体读代码的白盒分析。
在互测过程中,有一部分同学设计的程序遇到嵌套层数过多的函数会出现CPU_TLE的BUG,该BUG出现的频率较高,一部分原因是因为调用toString过多,没有用临时变量保存。
重构经历
我的重构是在第一次作业到第二次作业时,如下图(图4):
第一次作业没有继承关系,只有表达式类和项类。第二次作业新建立了了一个Drive接口,Power、Constant、Add、Multiply、Tri和Bracket实现了这个接口。其中Power和Constant是最基础的函数,不依赖Derive,其它4个实现Derive接口的类还要依赖于Derive,需要嵌套。
这次重构是因为第一次作业的架构无法实现后续作业的功能,重构后继承层数增加,更加归一化。
在第三次作业后,在研讨会上听了同学们的分析,也重新构思了一下架构,但没有实现代码,如下图(图5):
由于在前文分析图3时就已经对重构后的架构做了阐述,这里就不赘述了。
心得体会
第一次作业和第三次作业完成较好,而第二次作业完成较差,究其原因还是本地没有测试充分,我在进行及三次作业时就吸取了教训,每增加一个功能,就针对这个功能进行测试,这样可以尽可能的充分测试。
在正确性的基础上,还需要对性能进行追求。第一次作业对性能分要求比较严格,但第一次作业的性能分也比较好拿,也有一些细节我没有注意到,比如x**2可以写成x*x,负项提前这些小细节,以后也要多在细节上下功夫。对于第二三次作业,课程组给的性能比得分函数也体现了,要先保证正确率,只要性能分过了山崖,就可以拿到不错的性能分。第三次作业中,我只实现了合并同类项,常数优化和减少嵌套层数这三样优化,就拿到了不错的性能分,所以还是要先保证正确率,第二次作业的惨痛经历难以忘记...
通过这单元的作业结合老师课上的讲解以及研讨课同学的分享,我对自己面向对象的一些思想有了提高,意识到了架构的重要性,一个好的架构可以事半功倍。
也希望之后的课程也能顺利完成吧,通过这门课能够学到知识,锻炼自己的面向对象的思维能力。
© 著作权归作者所有
发表评论