北航面向对象(oo)第一单元作业(HW1)作业总结

不想打代码QAQ 学生 2024-03-22 10:45:28

HW1

1 Introduction

  • 这次作业共包含三次迭代
    • 第一次实现了包含x、()、+、-、*、^的符号计算式去括号并化简
    • 第二次实现了嵌套括号处理、自定义函数处理、指数函数处理
    • 第三次实现了表达式求导与自定义函数嵌套
  • 笔者在第二次迭代时重构,因为第一次构建代码没有深入理解递归下降的思路,教训++
  • 其实总体而言设计中规中矩,并没有什么非常突出的创新设计,主打一个标准
  • 我想在这里分享以下几点
    • 第一,基于度量分析代码,着眼于重构前后变化对比
    • 第二,整体设计架构
    • 第三,局部设计分享
    • 第四,目前的问题与改进可能
    • 第五,hack
    • 第六,经验分享

2 度量分析

在这一部分,我将主要对比1、3两次的代码,以展示不足与改进

0.代码思路介绍

​ 因为在后文需要对比分析,所以我先来介绍一下代码设计思路:

​ 第一次作业:天才般的史山

在第一次作业中,我的想法很简单:直接写一个while循环,寻找括号,并对括号里面的东西重新调用该循环,之后用字符串操作的方式拆掉括号,任务结束。主打一个简单粗暴

​ 第2&3次:标准化递归下降 + 单多项式

设计了poly与mono,将建树与计算分开来,先建立表达式树,然后调用poly,最后再将poly转化为string

1.Complexity Metrics(复杂度分析)

关于复杂度分析方法的复杂度分析主要基于循环复杂度的计算。循环复杂度是一种表示程序复杂度的软件度量,由程序流程图中的“基础路径”数量得来。

  • CogC 认知复杂度: 表示程序中的独立路径数目

  • ev(G) 基本复杂度:是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。

  • iv(G) 模块设计复杂度:设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。

  • v(G) 圈复杂度:是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。

    HW3

    MethodCogCev(G)iv(G)v(G)
    DxFactor.DxFactor(Expr)0111
    DxFactor.toPoly()0111
    ExpFactor.ExpFactor(String)2112
    ExpFactor.toPoly()0111
    Expr.Expr()0111
    Expr.addToterms(Term)0111
    Expr.toPoly()1122
    Formalizer.Formalizer(HashMap<Character, String>, String, ArrayList)0111
    Formalizer.addFunc(char, String)0111
    Formalizer.call(char)0111
    FuncFactor.FuncFactor(Expr)0111
    FuncFactor.toPoly()0111
    Functions.Functions(ArrayList, String)0111
    Functions.operate()6456
    Lexer.Lexer(String)0111
    Lexer.getNextToken()0111
    Lexer.next()1421016
    Main.main(String[])4255
    Main.stringProduce(String)0111
    Mono.DxMono(Mono)9577
    Mono.Mono(String, String, String)0111
    Mono.changeNum(BigInteger)0111
    Mono.generateString()35101116
    Mono.getExpIndex()0111
    Mono.getIndex()0111
    Mono.getNum()0111
    Mono.monoMult(Mono, Mono)5455
    NumFactor.NumFactor(String)0111
    NumFactor.toPoly()0111
    Parser.Parser(Lexer)0111
    Parser.parserDx()7136
    Parser.parserExpFactor()9248
    Parser.parserExpr()1122
    Parser.parserFactor()3381620
    Parser.parserFuncFactor()151912
    Parser.parserNum(int)2122
    Parser.parserTerm()1122
    Parser.parserVari()2122
    Poly.Poly()0111
    Poly.addToPoly(Mono)6144
    Poly.generateString()3133
    Poly.polyAdd(Poly, Poly)3133
    Poly.polyDx()3133
    Poly.polyMult(Poly, Poly)10155
    Poly.polyPower(Poly, Integer)4233
    Term.Term()0111
    Term.addToFactor(Factor)0111
    Term.toPoly()1122
    VariFactor.VariFactor(String)0111
    VariFactor.toPoly()0111
  • HW1

  • Expression.Expression(String)0111
    Expression.deleteBracket(int, int, String)1471012
    Expression.extending(String)19278
    Expression.findAdd(String)3323
    Expression.findIndex(String)3123
    Expression.findLeftBracket(String)3323
    Expression.findOperation(String)4345
    Expression.findRespondBracket(String)8535
    Expression.getStr()0111
    Expression.rebuild(String)8446
    Main.findAdd(String)3323
    Main.main(String[])1112
    Term.Term(String)0111
    Term.changeNum(BigInteger)0111
    Term.findIndex(String)3123
    Term.findMul(String)3323
    Term.findName(String)5356
    Term.getName()0111
    Term.getNumber()0111
    Term.getProduced()0111
    Term.initializeName()2222
    Term.operate()5152124
    Term.refreshProduced()6134
    Term.returnForm()0111
    VariFactoer.VariFactoer(String, String)0111
    VariFactoer.changeIndex(String)0111
    VariFactoer.getIndex()0111
    VariFactoer.getName()0111
    VariFactoer.gettype()0111
    VariFactoer.operate()0111
  • 现在,让我们对两次作业的核心方法进行分析:

  • 可以明显看出,两次作业的核心方法复杂度都不低,但是可以看出,hw1中数值相当集中,比如Term.operate()方法。这是由于暴力计算违反了职责单一化原则,并且与其他方法关系紧密,作为核心很容易出现难以检查的错误,这也正是hw1可扩展性差最终导致第二周重构的原因

  • 这种C-style的编程方式其实并不是一无是处,在处理简单问题时,这类方式可以大大降低代码量和思维量,但在多次迭代开发中可维护性太差,所以这次重构算一次深刻教训了

  • 而对于HW3,其中依然有复杂度高的方法,这类方法通常是出口过多,分类太多导致的,比如poly.generateString 这个方法,我为了可以生成常数、变量、exp三种因子各种组合的字符串而设计了8个出口,而poly本身又需要两层hashmap,导致了复杂度过高。这里其实可以考虑进一步封装并细化职责

2.Dependency Metrics(依赖度分析)

先简单介绍一下

  1. Cyclic:指和类直接或间接相互依赖的类的数量。这样的相互依赖可能导致代码难以理解和测试。
  2. DcyDcy*:计算了该类直接依赖的类的数量,带表示包括了间接依赖的类。
  3. DptDpt*:计算了直接依赖该类的类的数量,带表示包括了间接依赖的类。
  • HW1
ClassCyclicDcyDcy*DptDpt*PDcyPDpt
Expression0231111
Main0140010
Term0121211
VariFactoer0112311
InterfaceCyclicDcyDcy*DptDpt*PDcyPDpt
Factor0001401
  • HW3

  • ClassCyclicDcyDcy*DptDpt*PDcyPDpt
    DxFactor931211111
    ExpFactor931211111
    Expr941251111
    Formalizer00021201
    FuncFactor03130010
    Functions00011201
    Lexer00031201
    Main05130010
    Mono941271111
    NumFactor931211111
    Parser9121221111
    Poly9112111111
    Term931221111
    VariFactor931211111
    InterfaceCyclicDcyDcy*DptDpt*PDcyPDpt
    Factor911281111
  • 这里出现了一件很有意思的事情:作为一坨*山的第一次作业代码依赖度反而低得多,这其实是因为当时代码构筑思路过于简单,除了主类之外,每个类只会与一个上级与一个下级交换数据。但这种构筑模式违反了单一职责原理,使得每个类都有许多事要做,其实反而不便于理解与debug

  • 在hw3中,可以看到,越是核心的类,比如parser,依赖度就不可避免的高。这确实对debug造成了一定影响,在单步调试时反复横跳十分麻烦,所以我认为仍存在优化空间。应该可以对一些类实现进一步包装,放在同一个接口下,合并依赖,来降低依赖度,这将有利于阅读

3 结构介绍

话不多说,直接上图(快端上来罢)

第一次作业

img

  • 这份代码十分“朴素”(这是笔者觉得众多评价中最贴切的词了),职责分配十分清晰:main进行字符串预处理与最后输出前的加工,然后将处理好的字符串直接生成为expression,expression负责拆括号,拆到没有括号就分割成项,项再分割成因子,然后因子合并,项合并,最后返回main
  • 全程都是用字符串传递数据,清一色的字符串操作。好处就是写的时候很通畅,跟着直觉写代码,就像c语言一样,字符串操作都是现成的也不难,问题就是睡一觉就看不懂了,根本不知道自己哪一步字符串处理之后会变成什么,可读性极差
  • 其实在一开始迭代时已经想到了这个问题,所以在每个字符串出入口都会写一个标准化传输格式,但最后还是出了bug,面对400行连续代码,相当于大海捞针,根本找不出来

HW2

img

  • 这次代码进行了重构,与其浪费时间在想上周我究竟在写什么,不如写一份人可以看懂的东西。这里使用了标准的lexer进行文法分析,同时用parser进行类关系处理,类之间传输数据只使用poly(多项式)。
  • 想起来oo课上的一个例子:现在有一个图书馆类,有一个学生类,那么学生去借书这件事应该写在哪个类?答案是新建的日志类,这便是parser的作用。这个类解决了第一次代码中职责混乱的问题,单一职责带来了可读性强的优点,代码变得更加清晰了,最后debug只用了不到十分钟
  • 这个代码另一个特点就是“两步走”:先建树后遍历。这是一个十分重要的思想。第一次的混乱极大程度源自于一边解析一边生成,不仅浪费时间,还浪费空间,每个类都要记录处理前与处理后两种数据,还都要各自实现获取修改方法,在构造器中有时候还不能一次性全初始化,还要设置初始化检查,十分复杂。而这次代码的方法使得在解析时只是生成了一棵树,树的叶节点保存的是解析后的原始数据,生成结果不再用成员实现,而是利用一个方法:topoly,这样就使得合并与计算的便捷性都远远优于字符串
  • 这份代码问题也不小:对于自定义函数类,我没有把它变成一个因子,而是用了预处理的方式,即读到了自定义函数就直接字符串替换。当时感觉没什么问题,现在看来其实很傻,因为当作因子进行计算更符合我的设计逻辑

HW3

img

  • 这次与HW2没什么本质区别,不过是一个扩展性验证,加入了dx因子。这次学聪明了,直接扩展一个因子,其他什么也没改。
  • 迭代就结束了

4 局部设计分享

a. lexer&parser

这里我想说一说文法分析器的重要性。一位不愿透露姓名的岳某雨学长曾说,lexer就是一个提取器,它根本不知道自己读的是什么,它只做匹配,读什么就弹出什么。那么我为什么一定要写他呢?因为这个lexer更底层的作用是控制程序进程。这就好像我把字符串放在一个箱子里,按一下按钮就弹出来一个碎片,parser看见碎片,把碎片拼在表达式树合适的位置。有些碎片是负责定位的,比如“)”就说明该回到上一层了,有些碎片是数据,比如“1268162”这种数据,但这和我lexer没关系,lexer只知道把最小单元切下来。那么什么时候就知道树建好了呢?当然是按按钮吐不出来东西的时候。这种程序控制分担了职责,独立于递归之外,无论在那一层递归都使用同一个lexer返回的碎片,进行信息分析。这便是lexer的优越性。

另一个好处就是debug很方便。当你发现某个地方读不下去了,那必然是没有按按钮,就是没有读下一个token,其他括号对不上或者数据读不全的bug在原理上就不可能存在

b. 字符串预处理(取巧)

这个其实是我很得意的一个设计,但很遗憾在重构之后没有勇气能进一步实现,只进行了一些小尝试

我在读入字符串后,用大量replace进行了字符串标准化,比如连续符号替换,以及把“-”替换为“+-1*”

后者是一个很有意思的设计:用这样的方式替换使得项之间的运算只剩下了加法,使得分割与生成都十分方便,BigInt这个类也能识别负号,所以只需要最后输出时候替换回来就好了

更神奇的是我第一次的设计,我将“-”替换成了“+i*”,i是一个符号标记,最后在合并时候将偶数个i替换为+,奇数个替换为-,这样处理后表达式中只剩下了+ * ^ 更加有利于字符串操作,但可惜在后续的迭代中并没有使用,显得有些鸡肋

c.可扩展性

关于可扩展性,由于在第一次吃了大亏,第二次就进行了全面调整与布局,主要体现在两个方面:基本项与因子。基本项由不可拆解因子组成,如变量、常数、exp;而因子则是各类新增功能

当加入一个新功能时,如果是不可拆解的,就多一个就基本项的成员,如果是运算类,比如求导,就多一个因子。基本不需要修改其他代码

5,目前问题与改进可能

目前代码不存在bug,但优化不怎么多,这可能是将来可以改进的地方

至于底层逻辑的问题,我认为还是基本项的扩展性:当修改基本项时,不可避免会对多项式的运算与存储造成影响。目前已经是两层hashmap,如果加入更多因子,会导致进一步的嵌套,不是很美观,需要进一步考虑

6,Hack与Hacked

我在互测的主要想法其实很简单,测试歪门邪道的数据点。比如单个常数,变量,比如计算结果是0但是过程极其复杂的式子。其实我没有评测机,所以在这个部分我并没有多少发言权QAQ

毕竟也是第一次互测,问题也真不少,只能说之后再慢慢思考慢慢解决

7,经验分享

技术性的东西已经说完了,这里其实只是一些习惯上的思考。先抛出一个问题:

究竟什么时候应该开始写代码?

这并不是说周二或者周三的问题,而是在准备到哪一步开始动手的问题:袁老师在第一节课就说,不能直接写,也不应该全想明白再动手期望写出一个“perfect code”

先叠甲,袁老师的建议是对的,但问题在于这个回答太模糊了,就好比让你从0到1之间挑选一个数字,然后说0和1是错的,剩下的范围还是很大。

我想在这里给出我的思考:写出一个底层逻辑,然后动手

什么是底层逻辑?其实就是实现方法,它包括具体的类大概有几种,每种承担什么职责,以及,可扩展性怎么样。这个并不是说“貌似可以扩展就行”,而是一定要思考代码量与可读性,可读性低 = 不可扩展

当然这是我给出的建议,各位看官如果觉得“这不是是个人都知道?”或者“荒谬至极根本没用”就当看个笑话吧

这次作业总体来说感觉真的不错,能学到不少东西,有一点点体验差的地方就是重构的那周。重构真的很需要勇气,也很需要时间,但这大概是学习所必须经历的罢(笑)

关于未来方向,我觉得其实没什么需要提升的,这个单元真的很不错了

(或许可以在课上深入讲讲递归下降?以免让我这类看不懂的蒟蒻走弯路?或许诸位大佬不需要所以课程组没加吧mol2333)

...全文
134 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧