301
社区成员
发帖
与我相关
我的任务
分享目录
第一单元 主要是完成表达式的解析,展开,化简和求导。在本课程之前,有java 的基础,所以我并没有参加OO pre 课程,因此这是我第一次接触这样的Java项目,实现时很多地方架构混乱,设计的不太理想,解耦合不完善,因此本篇博客仅作为个人思路,想法和交流学习。
通过对数学概念中的表达式构造进行深入建模,实现单变量多项式的展开运算,从而初步领略到分层设计理念在工程实践中的应用与实现。
这个项目就像是一支专业的乐队,每个成员都扮演着特定的角色,共同演奏出一曲精彩的数学表达式之歌。在这个乐队里,Lexer是乐谱的解读者,它负责将复杂的数学表达式乐谱(即字符串)拆解成易于理解的音符和节奏(即词素)。ExpressionParser则像是指挥,根据乐谱的指示(词素),指挥各个乐器(Term、Factor等类)合作,构建出整个乐章(数学表达式对象)。
例如,当观众(用户)提出想听的乐曲(输入表达式"3 * x + 2")时,Lexer首先将这个乐谱拆解,识别出3、*、x、+、2这些音符。接着,ExpressionParser指挥,将这些音符编排成一段旋律(解析成表达式对象)。在这过程中,Term和VariableFactor等类就像是演奏不同乐器的乐手,每个人都专注于自己的部分,最后协同演绎出完整的乐章。
Main.java则是整个乐队的经理,负责接收观众的请求,组织演出,并向观众展示最终的演奏结果。比如,它接收到表达式"3 * x + 2"的请求后,便开始组织这场演出,最终向观众展示了这个数学表达式的内在美(就是展开并化简后的结果)。

这张UML图展示了数学表达式处理系统的类结构和它们之间的关系。核心部分由Factor接口和它的几个实现类(VariableFactor、ConstantFactor和ExpressionFactor)组成,它们共同定义了表达式的构成元素。Term类聚合了Factor对象,代表一个乘法表达式。Expression类则包含了多个Term对象,表示一个完整的数学表达式,能够进行自身的化简和转换为字符串表示。
Lexer类负责将输入字符串转换为词素,而ExpressionParser利用Lexer产生的词素来构造Expression对象。Main类作为程序的入口点,使用ExpressionParser来解析用户输入的表达式,并执行相关的操作。
本项目通过多个精心设计的类和接口组成,涵盖了从词法分析、语法解析到表达式构建和处理的全过程。虽然代码量适中,但通过模块化和责任分离,展现了复杂功能的高效实现。整体而言,代码规模体现了功能丰富性与代码管理的良好平衡,既不臃肿也不过分简化,适合作为处理数学表达式的基础架构


这张方法复杂度图呈现了项目中每个方法的复杂度。图中几个关键指标包括:
Cognitive Complexity (CogC):在图中,大多数方法的认知复杂度较低,但expression.Expression.merge和simplify方法的复杂度显著高于其他,分别为35和34,这可能是因为它们涉及到了表达式合并和化简的复杂逻辑。
Cyclomatic Complexity (CC):通常用来衡量一个程序模块复杂度的指标,代表了程序中独立路径的数量。在这里,它没有明显显示,但可以从高认知复杂度中推断merge和simplify方法可能有较高的CC值。
Nesting Depth (ev(G) and iv(G)):表示嵌套深度,ev(G) 是最深的退出点深度,iv(G) 是最深的嵌套点深度。在这里,Term.toString()方法的嵌套深度最高,达到了5和6,表明有多层循环或条件判断。
Halstead Volume (v(G)):这是由程序的操作数和操作符决定的,反映了方法的“体积”,或者说信息内容的多少。Term.organizeFactor的值最高,为53,可能因为该方法中有较多的操作和逻辑分支。
图中的数值告诉我们Expression和Term类中的某些方法可能是代码中最复杂的部分。具体来说,merge和simplify方法因其高复杂度,可能是优化和重构的关键目标。这些方法在功能实现上可能较为复杂。

这张类复杂度图给我们展示了项目中各个类的一些关键度量指标。从这些数据可以看出,expression.Expression和expression.Term是这个项目中最复杂的两个类,可能需要更细致的注意和测试,以确保整体的质量和可维护性。这个度量图为我们提供了哪些类可能需要重构或优化的重要线索。

在此次作业中,所需完成的任务包括:解析一系列自定义函数的定义及一个涵盖幂函数、指数函数、自定义函数调用的表达式,并输出经过恒等变换后展开所有括号的结果。
具体而言,展开所有括号的过程定义为:对原始输入表达式$E$进行恒等变换处理,以获得新的表达式E。在此过程中,E'不应包含任何自定义函数和求导操作符,且仅保留必需的括号。
第二次作业允许用户定义自己的数学函数,为此,创建了SelfDefine类来处理这些自定义函数的定义和计算 。这个类将用户定义的函数视为一个整体,包括函数名、参数列表和函数体。
自定义函数的实现思路:
解析定义: 当用户给出一个函数定义,比如f(x, y) = x^2 + y,SelfDefine类解析这个字符串,分辨出函数名f、参数x和y,以及函数体x^2 + y。
存储函数体: 函数体被转换成内部的表达式对象,以便后续能够对其进行操作和计算。
计算函数值: 当计算函数值时,如f(2, 3),系统将实际的参数值2和3替换进函数体的对应位置,然后对表达式进行求值。
具体例子:
如果用户定义了函数g(x) = 3 * exp(x),系统首先解析出函数名g和函数体3 * exp(x)。当用户后续调用g(2)时,SelfDefine查找g的定义,将2代入exp(x),并计算结果,这里就会是3 * exp(2)的值。
通过SelfDefine类的设计,第二次作业能够有效地管理和执行用户定义的复杂数学函数,提供了系统的高度可扩展性和灵活性。
第二次作业中的指数函数部分,核心由 Exp.java 文件中的 Exp 类来实现。这个类专门负责处理表达式中的指数运算,是对数学中的指数函数的程序化表达。
表达式封装:Exp 类将指数表达式封装起来,它持有一个 Expr 类型的对象,该对象代表指数运算的底数部分。
计算与表达:Exp 类提供了计算指数值和求导的功能,同时也能够生成指数表达式的字符串表示。
例如,如果表达式为 exp(3x),Exp 类将封装一个 3x 的 Expr 对象。当需要计算该表达式的值时,它会应用数学中的指数规则。若要求导,则运用链式法则计算导数,假设x为变量,其导数将为 3exp(3x)。
这种实现方式使得第二次作业能够灵活地处理包含指数的数学表达式,并能够扩展以支持更复杂的指数类型运算,例如指数嵌套等。它为表达式的求值和求导提供了必要的基础,是数学运算功能的关键部分。

这张 UML 图展现了第二次作业的类结构。在这个框架中,Factor 接口是核心,它连接了不同类型的数学实体,如 Number, Variable, Exp, 以及它们在表达式中的组合形式 Expr 和 Term。Lexer 类负责分解字符串形式的表达式,而 Parser 类则将这些分解后的组件构建成结构化的表达式对象。SelfDefine 类处理用户自定义的数学函数,MainClass 则作为程序入口,协调解析流程。StringSimplify 类提供字符串预处理功能。



从图示中可见,除了首次作业中出现的一些复杂度较高的方法之外,Expr的 TriMatchPro 方法同样显得异常复杂。这一复杂性源于它作为三角函数化简过程中最关键的方法,频繁地调用了其他函数,并且其内部还涵盖了递归调用的情况。

在第二次作业的类复杂度图中,可以注意到几个关键的度量指标:
OCAvg(平均操作复杂度):这个指标反映了类中所有方法的平均复杂度。Expr 和 Term 的平均复杂度较高,这可能意味着这些类的方法逻辑更为复杂。
OCmax(最大操作复杂度):这个指标显示了单个类中最复杂的方法的复杂度。Expr 类有最高的最大操作复杂度,达到了 14,这表明它至少有一个方法的复杂度远高于平均水平。
WMC(加权方法复杂度):此指标为类中所有方法复杂度的总和。再次看到,Expr 类的 WMC 值最高,达到了 83,表明这个类在项目中承担了相当多的复杂逻辑。
从这些数据可以得出,Expr 和 Term 类可能是代码最复杂的部分,是由于它们负责处理表达式的多个方面。高复杂度可能意味着在未来的维护和开发中,这些类需要更多关注。
本次作业所需完成之任务涉及:解析一系列精心设计的自定义函数定义及一则蕴含幂函数、指数函数、自定义函数调用、求导算子之复杂表达式,并输出该表达式经恒等变形且彻底展开所有括号后的精炼形态。 在此作业中,彻底展开所有括号之定义为:对初始输入表达式 E 进行恒等变形处理,以获得新的表达式 E。在此,E 不再包含任何自定义函数和求导算子,且仅保留绝对必要的括号.
函数定义和解析:
定义解析:当用户定义一个函数时,比如f(x) = x^2 + 3x,SelfDefine类接收这个定义为字符串,然后解析这个字符串,分离出函数名称f、参数x和函数体x^2 + 3x。
函数嵌套:如果有嵌套函数的情况,例如g(x) = f(x) + sin(x),解析器同样通过SelfDefine类来处理g,同时注意到g的函数体中调用了另一个函数f。
参数替换和递归求值:
参数替换:在计算如g(2)的值时,系统首先查找g的定义,发现它依赖于f(x)。系统然后将x=2代入f,计算f(2)的值。
递归求值:系统递归地解析和计算函数体中的每个函数调用。在我们的例子中,系统先计算f(2)得到一个中间结果,然后继续计算sin(2),最后将这些结果相加得到g(2)的最终值。
具体例子说明:
假设用户定义了两个函数:f(x) = x^2和g(x) = f(x) + 3。当用户请求计算g(2)时,系统首先识别到需要计算f(2),这会触发f(x)的计算过程,得到4(因为2^2=4)。然后,将4加上3,得到g(2)的结果为7。
在项目 3 中,求导的实现思路具体而言是通过分解表达式的不同部分,逐步应用数学上的求导规则。这个过程中,每种表达式部分由特定的类(如Variable, Number, Exp等)表示,并且这些类都实现了Factor接口,这样它们就可以在求导过程中统一处理。
求导过程的具体步骤:
分析表达式:首先,Parser类将输入的表达式字符串转换成一个结构化的表达式对象,这个对象由Term和Factor组成,其中Factor可以是变量、数字、更复杂的表达式等。
应用求导规则:针对不同类型的Factor(如Variable, Exp等),Derivative类中的方法会被调用来计算它们的导数。
对于一个变量x,如果求x对x的导数,结果是1(由DeVariable处理)。
对于一个数值常量,其导数为0。
对于复杂表达式,如e^(x^2),会递归地处理表达式中的每个部分。首先确定e^(x^2)是一个指数函数(由Exp类表示),然后根据链式法则,需要计算内部函数x^2对x的导数(2x),最后将外函数的导数(仍然是e^(x^2))与内函数的导数相乘,得到最终结果2x * e^(x^2)。
组合结果:在求导的每一步中,计算得到的导数结果会被适当组合和简化,形成最终的导数表达式。

这张UML图揭示了各个类如何相互作用来处理数学表达式,特别是求导和函数定义的功能。
核心接口Factor是各种表达式组件的基础,如常量、变量、复杂表达式等。
Term和Expr类构成了表达式的两个层次;Term代表乘除项,而Expr代表整个表达式,可能由多个Term组成。
Lexer和Parser类协同工作,将字符串形式的表达式转换为结构化的Expr对象。
Derivative类负责执行求导操作,它可以处理Factor的不同实现所代表的表达式部分。
用户自定义函数通过SelfDefine类实现,它可以解析函数定义并在需要时计算函数值。
DeVariable和Exp类处理特定的求导情况,如对变量的求导和指数函数的求导。
Number和Variable类代表表达式中的基础数学元素,分别对应数字和变量。
最终,MainClass作为程序入口点,协调上述各个组件的操作,处理用户输入,并输出结果。
这个图反映了一个层次化、模块化的设计,各个类都具有专门的职责,这为代码的可维护性和扩展性提供了良好基础。




在第三次作业的方法复杂度图中,可以看到方法的数量和复杂度都有所增加,这是因为新增了求导功能,同时对现有代码进行了优化,进一步提炼了方法来增强代码的可读性和可维护性。
从图中可以注意到几个关键点:
方法总数的增加:这说明了项目中新增了更多的功能,尤其是与求导相关的功能,也可能包含了代码重构的结果,将复杂的功能分解成了更多的小方法。
复杂度的变化:之前复杂度较高的方法,如Parser.parseFactor()的复杂度有了显著降低,这表明代码优化有效地简化了这些方法的逻辑。
耦合度的观察:多个方法显示出较高的耦合度,尤其是Expr类中的merge和simplify方法,以及Parser类中的parseFactor方法。这表明这些方法可能需要互相协调多个类和组件才能完成它们的工作,指出这些方法之间的联系可能过于紧密。
综合这些观察,可以得出结论,第三次作业在增加新功能的同时,也注重提高了代码质量。

关注点主要是三个指标:平均操作复杂度(OCAvg)、最大操作复杂度(OCmax)和加权方法复杂度(WMC)。
Expr 类显得尤为复杂,平均操作复杂度(OCAvg)接近 5,最大操作复杂度(OCmax)为 14,说明有个别方法复杂度较高。它的加权方法复杂度(WMC)为 84,远高于其他类,这意味着这个类在整个项目中承担了相当复杂的逻辑处理工作。
SelfDefine 类也有较高的复杂度,平均操作复杂度(OCAvg)为 6.67,表明它的方法普遍比较复杂,可能因为它处理用户自定义函数的逻辑。
Parser 和 Derivative 类的加权方法复杂度(WMC)也较高,分别为 41 和 32,这可能反映了它们在解析和求导逻辑上的复杂性。
这些数值暗示了哪些类可能需要重点关注以简化方法和提高代码清晰度。特别是Expr和SelfDefine可能是优化和重构的主要候选对象。
在完成第一单元的作业之后,我深刻体会到了架构设计在面向对象编程作业完成过程中的重要性。一个好的架构不仅能够帮助我们在后续的迭代和设计中减少大量的代码重构工作,而且还能使代码更加清晰、逻辑顺畅。在第一次作业的开发过程中,我没有太注重整体架构的可延展性,这导致了第二次作业的推进变得艰难,最终只能对表达式的计算部分进行小范围的重构。因此,我意识到一个好的架构对后续的迭代和设计十分有帮助,能够最大化地减少代码的重构量。
此外,我也更加深入地理解了面向对象编程的封装特性,这种特性允许每个类只提供外部接口而隐藏内部实现细节,这与面向过程编程有着本质的区别。
在编码过程中,我体会到了良好架构对于后续作业的影响,充分的思考是减少重构的前提。我在测试过程中学会了充分考虑特殊样例和边界压力测试样例,这使得代码更加可靠。
此外,我还意识到了对自动评测机搭建和数据生成器完成过程中对指导书理解的重要性,这对避免理解不到位的问题大有帮助。经过三次作业的迭代,我更加清楚地看到了良好架构对后续工作的积极影响,并认识到充分思考是减少重构工作量的前提。同时,我也体会到了在测试过程中考虑特殊样例和边界压力测试样例的重要性,这些都是确保代码可靠性的关键。
总之,通过这一系列作业的完成,我不仅加深了对面向对象编程的理解,也认识到了一个条理清晰、逻辑顺畅的架构对整个项目开发和迭代的重要性。我还学会了如何更有效地进行代码测试,以及如何避免一些常见但容易忽视的问题。