BUAA 面向对象课程 第一单元总结
面向对象设计与构造 第一单元已经结束,在此进行总结。
第一次作业
综述
第一次作业由于是简单表达式的求导,不含有复杂因子,不含有嵌套的表达式因子。那么这个结构就可以简单的描述为:
表达式 由 若干项 构成
那么结合正则表达式工具的应用,对输入的表达式进行拆分处理。每当输入一个项,利用正则将这个项拿出来,放到相应的对象中进行解析。
这个解析的思路是清晰的,由于难度不大,为了快速写出本次作业,设计中未考虑对嵌套需求的增量支持。
基于度量来分析程序结构
类图:
其中,RegConst存储正则表达式。
对于Form类,设计考虑是这样的。在本次作业不考虑嵌套需求的增量支持下,为了支持后续可能出现的新函数(如三角函数等),特意设计了Form类。对于每个简单项,最终都能化为标准形式。在本次作业中,标准形式即指a*x^b
。其中,决定是否是同类项的因素是除了系数a
之外的其余数,本次作业中即为b
。那么,把这些因素抽象出来,构成一个Form类。(本次作业中,由于只有b
决定了是否为同类项,若不考虑可能出现的新函数,Form类是可以不抽象出来的)
其余类的含义为综述中所描述。
复杂度分析
方法复杂度
总体来讲复杂度较为分散,但Term类构造方法及toString方法的圈复杂度和设计复杂度较高。对于构造方法,设计中是传入字符串,直接将其转换并同时合并了同类项。因将化简的过程都放入了构造中,导致的复杂度过高。而对于toString方法,由于是在输出环节对相应的特殊情况进行优化处理,导致有较多的分支判断在这个方法中,无法避免。这样的好处是结构简单,写代码是更加方便,缺点是会不可避免的增加复杂度。
类复杂度
Term类复杂度稍高,在方法复杂度中有分析。
其余类复杂度情况较为良好。
优缺点分析
优点
- 将整条表达式分解为若干项,每提取出一个项就进行处理,避免了过于巨大的正则表达式,同时也不会出现 使用完整大正则 直接解析整个多项式而造成的时间/空间巨大消耗。
- 整个逻辑结构清晰,代码易于构建,同时只要没有逻辑bug,在检查过了细节问题后,便能保证程序的正确性以及健壮性。
- 合并同类项的优化是基础的,效果也不错,
缺点
- 缺点也是明显的,未留有对嵌套表达式增量开发的余地。
- 为了代码编写的效率,牺牲了部分模块设计,将方法耦合在一起。这样耦合度控制的就不好。
第二、三次作业
综述
由于从第二次作业开始,便加入了对嵌套表达式的处理。于是抛弃掉第一次作业的结构,进行新的设计。这里二三次作业实现的效果很好的增量开发,故将这两次作业放在一起进行叙述。
基于度量来分析程序结构
类图:
结构设计:
-
Factor
:抽象类,万物基于Factor
。内部声明了三个方法simplify()
,derivation()
,print()
-
Poly
: 多项式,其中的优化方法simpify()
下述 -
Term
: 项 -
可理解为基类:
Sin
、Cos
、Power
、Const
(其中Power
仅指代x^n
幂函数) -
Parser
:将递归下降过程封装在此类中
对解析类(Parser)单独解释:
采用递归下降法,构建树结构。此方法构建完成后的树严格满足如下结构:Poly - Term - 基类|(Poly - Term - ....)。也就是说,parse()后建成的树结构中,Poly内部的容器中存的必定是Term,Term内部容器中存储的必定是基本因子或者表达式因子。(而在simplify之后,才可能会把某些元素向树结构中的浅层提,如下图中所描绘的)
对每个类中共有的三个方法进行解释:
-
simplify()
:- 有如下几种功能:
- 对当前元素(本对象)进行同类项合并的化简,对容器内(若本对象含有容器)的元素进行检索,若含有类似于 一个Term元素,但此Term中仅有一个因子的情况,进行 ”抽丝剥茧“,将这个单独的因子提取出来,直接取代Term的位置。
- Term中进行简单Power,Const的合并
- Poly中进行简单Const的合并。这是微优化,但是却很重要,因为表达式中会出现很多的常数项,幂函数项
-
derivation()
:求导。对于基类直接求导,对于Poly意味着加法,Term意味着乘法、链式求导。 -
print()
:实际上返回String,对于树结构逐层返回出正确格式的字符串。
总体解释:
整体流程类似于:
Parser.parse()
simplify()
derivation()
simplify()
print()
代码复杂度分析:
方法复杂度:
由于method总数有60余个,完全截图的话图片太长,故只截取飘红的部分。
Parser.getTrig()
方法飘红,由于把对Sin和Cos的提取放到了一起,导致条件判断语句稍多了一些,逻辑还是比较清晰的。
Parser.getFactor()
因为因子有四种,用了几个if语句判断。同时表达式因子左右的括号也用了if,相当于在if中又嵌套了一个if。
Const.valueOf()
是一个静态工厂方法,本身复杂度不应该高,但是我在Const类中缓存了常用的几个对象(0, -1, 1),导致用了几个if来特判需要产生的对象是否已缓存,显示飘红,实际没有问题。
App.main()
是入口方法,我在main方法中对式子进行了一些init操作,并没有写到单独的方法中,所以导致了main方法飘红。
对于其他方法的复杂度,耦合性,都在合适的范围内,符合预期要求。
类复杂度:
可以预见的,Parser类复杂度较高,因为在设计的过程中给,Parser类用了递归下降分析法,对某个元素的提取写在了Parser类的某个单独的方法中,同时充斥着无法避免的条件判断,字符特判。这导致了本类的复杂度很高。
而排除对表达式的解析外,其余类的复杂度都比较好,总体基本符合高内聚低耦合。第一次作业中Term类飘红的情况本次也没有出现(两次架构不一样也没有可比性)
分析自己程序的Bug
三次作业中,在公测和互测中均未出现Bug。
第一次作业
编写代码的过程较为顺畅。测试时,针对三个加减号连续出现的情况进行了手动构造测试,程序能正确输出结果。回忆设计的时侯,正负号的归属问题(是归于表达式,还是归于项的开头,还是归于因子)有些细节问题。我针对这一点的处理是,将每项开头的不属于”项“的正负号寻找出来,单独处理。
当时写的注释:
// NOTE:
// BECAUSE if the first appeared number has [+-] in front of it,
// the [+-] will be belonged to this number
// so classified discussion will be difficult
// Instead, search for the first match, find the first capture's index
// then we could know how many [+-] should be handled independently.
第二次作业
在完成代码编写后进行了简单测试,发现了一个输出的细节问题。sin内部如果有表达式,则要输出形如sin((Poly1))这样的双重括号。出现了这个编码时的小疏忽。
后来在同学们的交流中,我意识到可以将自己的输出重新输入回去,利用自己程序的WF判断功能来检测输出合法性。
第三次作业
由于第二次作业的架构设计是十分合适的,在增量开发中仅需修改数十行即可完成功能添加,而这对于程序正确性的影响很小。
第三次作业新增了WrongFormat格式判断,我在第二次作业中就已经构建了WF异常类,并在开发中随手进行的部分的判断,但是不全面。在第三次作业中,正式审视递归下降过程,在相应需要特判的地方进行异常的抛出,将WF判断功能完善。在测试中手动构造各种错误格式,包括空串,甚至unicode空字符都尝试过,程序都能够正确输出。最终,程序通过了强测和互测。
分析自己发现别人程序Bug所采用的策略
本人仅在第一次作业中发现了他人的Bug,第一次作业用了自己编写的简单评测脚本。利用项的正则表达式,使用xeger生成随机数据。互测屋中,有人出现了大数的bug,问题出在正则表达式中,对某个字符进行+的匹配错误写成了{1,100}。
第二次作业随机生成测试数据并未找出他人bug。在互测公布后,发现屋内有人的程序在特定情况下会出现无输出的情况,这是由于部分细节处出现0而未正确处理。
第三次作业未找出bug。
重构经历总结
可以说,第一次作业和二三次作业完全是两个不同的项目。两次的设计于构造都是完全独立的。
心得体会
这一单元作业,能够体现面向对象思想的主要在第二三次作业,对整体架构的思考及设计在整个项目进行中处于最前期,且花了一定的时间。而从开发过程以及最终的结果来看,前期的准备与设计是必要的也是十分重要的。在这个时期要充分思考各种问题,并预测需要用到的技术/工具,进行简单的搜索及预习。针对项目的需求,进行信息的提取,进行抽象,把他们转换为变成语言中的”类“。有良好的对整体的审视,便有利于写出适合的implement关系,extends关系。拿二三次作业为例,Factor抽象类中的三个方法,就是不同元素的行为共性。
本人在性能优化上并没有多么深入。从开始设计时就把程序的正确性以及健壮性放在首位,所以只把基础的合并优化以及树结构的优化纳入考虑范围内(事实证明树结构的优化还是很有必要的,比如(1+(2+sin(0)+3))就能够优化成6),将面向对象重点放在架构的设计和具体的实现上。另外,最终不错的评测分数也体现了,本项目中的优化也是可以满足要求的。
© 著作权归作者所有
发表评论