309
社区成员
发帖
与我相关
我的任务
分享| 类名 | 属性数 | 方法数 | 方法规模(行/分支) | 总行数 | 平均方法行数 | 平均分支数 |
|---|---|---|---|---|---|---|
| DerivativeFactor | 2 | 4 | 3‑8 / 0‑1 | 28 | 7 | 0.5 |
| ExpFactor | 2 | 5 | 5‑16 / 0‑3 | 58 | 11.6 | 1.4 |
| Expr | 1 | 7 | 3‑11 / 0‑1 | 52 | 7.4 | 0.3 |
| ExprFactor | 2 | 5 | 4‑14 / 0‑2 | 45 | 9 | 1.0 |
| Factor | 0 | 3 | 0 / 0 | 6 | 2 | 0 |
| FuncFactor | 1 | 4 | 6‑10 / 0‑1 | 35 | 8.8 | 0.3 |
| Lexer | 4 | 23 | 3‑13 / 0‑4 | 215 | 9.3 | 1.1 |
| Main | 3 | 5 | 9‑27 / 1‑3 | 117 | 23.4 | 2.2 |
| NumFactor | 1 | 6 | 3‑5 / 0 | 34 | 5.7 | 0 |
| Parser | 1 | 17 | 4‑28 / 0‑6 | 314 | 18.5 | 2.5 |
| Poly | 1 | 10 | 5‑34 / 1‑3 | 154 | 15.4 | 1.7 |
| RecurFuncDef | 6 | 8 | 3‑6 / 0 | 48 | 6 | 0 |
| RecurFuncFactor | 2 | 5 | 3‑7 / 0 | 31 | 6.2 | 0 |
| Result | 2 | 12 | 5‑32 / 1‑5 | 265 | 22.1 | 2.8 |
| ResultFactor | 1 | 4 | 3‑6 / 0 | 24 | 6 | 0 |
| SelectFactor | 4 | 6 | 4‑12 / 0‑2 | 49 | 8.2 | 0.8 |
| Term | 2 | 9 | 7‑28 / 0‑4 | 133 | 14.8 | 2.1 |
| Token | 2 | 2 | 3 / 0 | 12 | 6 | 0 |
| TokenType | 0 | 0 | 0 | 10 | - | - |
分析:
Parser 和 Lexer 的方法数最多,它们是解析的核心,复杂度较高。Result 类的平均方法行数(22.1)和平均分支数(2.8)最高,说明表达式化简和求导逻辑较为复杂。Main 类的 parseRecurFunction 方法长达27行,分支数3,负责递推函数定义的解析,逻辑较绕。Poly 类的 pow 方法采用了快速幂,分支数2,设计合理。Object,除 Factor 接口外无继承关系,DIT=1。Factor 接口的类有 10 个:DerivativeFactor, ExpFactor, ExprFactor, FuncFactor, NumFactor, RecurFuncFactor, ResultFactor, SelectFactor, VarFactor。Result 类为例,它依赖 Poly, BigInteger, Map, List 等,并与 Result 自身递归引用,CBO 较高。Result 和 Poly 之间双向依赖,Parser 依赖 Lexer 和各个 Factor 类,耦合较紧。但通过接口 Factor 隔离了部分依赖,降低了与具体实现的耦合。Result 为例,它的两个属性 polyPart 和 expPart 几乎被所有方法共同操作,内聚性高。Parser 类包含多个私有辅助方法,它们都服务于解析任务,内聚性较好。Lexer 中每个 handleXxx 方法只处理一种 token,职责单一,内聚性高。(由于无法直接绘制图片,以下用文本描述类图结构)
类图结构描述:

优点:
Factor 接口统一了所有因子的行为,使得 Term 和 Expr 可以统一处理。Expr 可以包含 ExprFactor,ExpFactor 可以包含任意 Factor,使得表达式可以无限嵌套。Result 作为求值结果,独立于语法树,便于化简和微分。Factor 接口,并修改 Parser 和 Term 的 deepCopyFactor 等辅助方法即可。缺点:
Parser 既要解析表达式,又要解析递推定义,还包含一个内部类 RecurPart,职责不单一。Term 中的 deepCopyFactor 和 replaceFactor 方法存在大量相似的模式匹配,若新增因子需多处修改。Main 中的静态变量 funcDef 和 recurDef 使得程序难以测试和复用。instanceof 进行类型判断,违背了多态原则,增加了维护成本。第一次作业:仅支持常数、幂函数和带括号的表达式(括号深度至多3层),因子只有 NumFactor、VarFactor、ExprFactor。Result 仅包含多项式,Term 和 Expr 结构简单。解析时采用了递归下降法,通过 Parser 和 Lexer 将输入字符串解析为语法树。
第二次作业:增加了指数函数 exp 和自定义函数 f(x),同时引入选择式因子 [(A==B)?C:D],要求程序能判断 A 与 B 的数学恒等性。为此扩展了 Factor 接口,新增 ExpFactor、FuncFactor、SelectFactor,并在 Result 中增加了 expPart 来表示指数函数的组合,同时实现了 substitute 方法用于函数实参代入。选择式因子通过在 toResult 时计算 A 和 B 的差并判断是否为零来决策。
第三次作业:表达式扩展为双变量 x, y,新增求导算子 dx、dy、grad,以及自定义递推函数 f{n}(x)。为此新增 DerivativeFactor 和 RecurFuncFactor,并扩展 Result 支持双变量多项式(Poly 中增加 y 指数)。递推函数在 Main 中使用静态缓存和记忆化搜索展开。求导算子通过 Result 的 diff 方法实现,支持偏导和梯度展开。
(这个架构麻烦是麻烦,不过好就好在可拓展性还算可以
假设课程组要求新增一种函数类型:对数函数 ln。那么只需:
LnFactor implements Factor,实现 toResult(返回 Result 表示 ln(arg)),diff 按公式 (1/arg)*arg' 实现,replaceX 递归替换参数。Lexer 中增加对 ln 关键字的识别。Parser 中增加 parseLnFactor 方法。Result 中增加一种新的表达式类型(例如 Map<Result, Result> logPart),并实现相应的加法、乘法、微分、代入等运算。Result,也可以将 ln 视为一种特殊的因子,在 toResult 中直接返回一个包装了 Ln 的 Result 对象,但需要扩展 Result 的表示能力。当前的架构中,Result 已经支持多项式与指数函数的组合,但对数、三角函数等新函数需要扩展 Result 的内部表示。这是当前架构的一个局限性——Result 的表示形式固定为多项式加指数函数,难以在不修改核心类的情况下添加新函数。一个更灵活的设计是采用访问者模式或代数数据类型,将表达式类型定义为树状结构,但那样会牺牲一些性能。
经过代码审查,我发现以下问题和潜在问题:
Main.parseRecurFunction 使用字符串包含判断(contains("f{0}"))来区分递推定义中的初始值行和递推行。如果递推公式本身也包含 f{0}(如 f{n-1}(x) 不会出现,但若参数中意外出现 f{0} 则会误判)。另外 Parser.parseRecurDefLine 假定递推公式严格为 coeff1*f{n-1}(arg1) + coeff2*f{n-2}(arg2) + extra,且顺序固定,若输入顺序颠倒会导致系数赋错。虽然题目输入格式固定,但设计不够鲁棒handleDerivative 中检测 grad 永远不会触发,因为 d 后跟 g 不构成 grad,该分支冗余。handleRecurFunc 添加 RECUR_FUNC token 后仅移动 pos++,未跳过 {,依赖后续 token 顺序,逻辑正确但易出错。DerivativeFactor.replaceX 返回新导数算子,若替换后表达式不含变量,最终求导结果仍为0,但保留算子不产生错误,因为 toResult() 时才求导。我统计了本程序中可能出错的几个方法(如 Main.expandRecurFunc、Parser.parseRecurDefLine、Result.substitute)的圈复杂度(按控制流语句数近似估算),并与一些稳定方法(如 NumFactor.toResult、VarFactor.toResult)进行对比:
| 方法 | 代码行数 | 圈复杂度(估算) | 是否出错 |
|---|---|---|---|
Main.expandRecurFunc | 23 | 5 | 潜在 |
Parser.parseRecurDefLine | 28 | 6 | 潜在 |
Result.substitute | 21 | 4 | 潜在 |
Result.simplify | 15 | 3 | 稳定 |
NumFactor.toResult | 3 | 1 | 稳定 |
可见,复杂度较高的方法更容易隐藏逻辑错误。降低方法复杂度的方法包括:拆分方法、使用状态模式、将复杂解析交给专门的类。
我采用了单元测试+随机测试+边界测试相结合的策略:
toResult 和 diff 编写独立的测试用例,验证已知结果(如 dx(x^2) 应为 2*x)。有效性:随机测试发现了导数算子替换后未化简导致的冗余项等问题。
在互测中,我特别注意了被测程序的代码结构。例如,如果发现某同学的 Result 类没有 simplify 方法,那么我会构造类似 0*exp(x) 的表达式,观察输出是否包含冗余项。
Result.pow 和 Poly.pow 均采用快速幂算法,避免 O(n) 的乘法。Result.add 和 Result.multiply 中,对指数部分使用 Map 自动合并,避免结果膨胀。simplify(如求导后、代入后),避免重复化简。Main.expandRecurFunc 使用 Map 缓存已展开的递推函数结果,避免重复递归。这些优化没有破坏正确性,因为它们都是代数恒等变换(快速幂与普通幂等价,合并同类项不改变值)。但缓存机制存在潜在风险(如前所述),需要保证键的唯一性。另外,simplify 方法中的化简规则需要保证不丢失信息,目前实现了零项去除、合并同底数指数等,是安全的。
arg.toResult().simplify().hashCode() 的建议。本单元三次作业让我深刻体会到了迭代开发和面向对象设计的重要性。第一次作业时,我设计了一个简单的表达式树,只考虑了常数和幂函数。第二三次作业引入指数函数和导数后,我不得不重构 Result 类,增加了 expPart 来支持指数运算,同时扩展了求导规则。递推函数和条件表达式让我进一步体会了延迟求值等的威力。
最大的收获是学会了如何设计一个可扩展的表达式系统:通过接口抽象、组合模式、递归结构,使新功能能够以较小的代价加入。同时,我也认识到过度设计(如过早引入复杂结构)和设计不足(如将求导直接写在 Factor 中)都会带来麻烦。
我本人也在尽量在非常有限的时间和学习两个小专业的课程容易疲惫的状态内做到自己思考,不过度偷懒,但是因为有时候自己想的情况会大幅度挤压完成其他任务和最起码的休息时间,还是不可避免着急过量使用AI。的确很遗憾,我确实会因同学讨论的架构听不懂的状况有些无奈又不甘,其实也希望如果精力充沛,一定会进行一个全面而系统的梳理(土下座
总之,希望自己之后能找到在不吃速效救心丸的基础上平衡多方面精力的方法,毕竟自己之前面对超大量代码,大块的最优时间是半夜,而熬大夜想东西一定会有一段时间心脏不舒服……