再说扩展性不足的部分 MathTerm 现在显式写成了 expX、expY 两个字段新增 z 时要继续改类结构

这一部分最核心的类是 ExpOptimizer它的思路不是简单地把 exp(poly) 原样输出而是尝试对内部多项式做进一步拆分从而构造更短的乘积形式, 6.2 第三次作业中的 exp 长度优化 第三次作业里我在 exp 的长度优化上投入最多, 测别人bug能轻易发现明显的bug但对于一些隐蔽的bug如性能问题就不太能发现 性能优化时如果自己不懂全让ai来那bug会超级多, math 层是处理数学语义与化简的层, 以前我觉得这些东西有点形式化但真正写完三次作业再回头看会发现很多问题其实从度量上就能提前看出来比如 Parser 为什么越来越难改, 把 MathTerm 从“一个 x 指数 一个 exp 内核”推广成“x 指数 y 指数 exp 内核”的形式, 重构包将字符串的输出问题从Polynomial类中解耦出来, 第三次作业时我觉得压力没有第二次那么大递归函数可以直接在函数注册表中解决和函数因子大同小异而求导等规则也只需要建立新的因子。

Polynomial 为什么越来越大, 如果课程能在第二次作业前后专门讲一次“什么时候应该重构”“如何从大类中拆责任”“怎样识别耦合中心”我觉得对大家后续的代码质量提升会很有帮助, 7.4 发现别人用ai的情况 框架有的同学框架和我一样啊因为我是让ai帮我正向建模我在去写我就觉得大概率是ai 代码注释这个大群里发的很多了ai的注释往往很离谱很长反而降低代码可读性, Polynomial 仍然是一个职责偏重的类, cos怎么办哈哈但我的水平不够是不会写这个的, 七、大模型使用7.1 AI使用率正确性与性能优化 第一次作业50%50% 第二次作业70%50% 第三次作业30%99% 7.2 除代码生成 有让codex直接找别人代码bugcodex能力真的太强了还能直接测别人代码就是要提前说好多少秒没运行出来自行切断不然会在后台一直跑占用资源, 从这个意义上说度量并不是为了好看而是为了帮助自己看清程序哪里最需要重构,语法树层本身几乎没变但语义层换了表示能力之后程序能容纳更复杂的功能,反而是优化函数带来了很大的困难后文会详细介绍, 求导逻辑目前也是围绕 x/y 写的扩展到更多变量时继续用分支写会越来越笨重, 3.2 自定一个新的迭代情景 我知道肯定会有同学写如果要新增sin。

如果我不主动盯住这些核心类的规模和复杂度它们就会在迭代中迅速失控,就比如括号层数的问题看toString方法中有没有特别判断。

测试分层语法属性测试 性能回归测试 变换单调性测试 4.2 差异方法代码行圈复杂度是否出现 bug说明 ExpOptimizer#optimizeProduct 24 6 是 递归多分支 ExpOptimizer#collectCandidateDivisors 35 7 是 候选生成 Polynomial#needParens 32 9 是与括号 bug 相关 规则分支 ExpOptimizer#formatFactor 13 3 是括号 bug 落点 与上层判定耦合 ChoiceFactor#getPolynomial 4 1 否 职责单一 ExpFactor#getPolynomial 7 1 否 纯构造 Parser#parseChoiceFactor 8 1 否 没什么好说的 降低方法复杂度的可执行做法 把 optimizeProduct 拆成 4 段候选生成、候选评估、剪枝判定、结果合并, ExpOptimizer 被单独提出来之后MathTerm 和 Polynomial 的主职责反而更清楚了, 不变量前置在优化器入口定义并校验“语法合法、长度单调”, 第二次作业时面对exp和函数调用 如果还把数学对象理解为“指数 - 系数”那对于原有的非常优雅的用HashMap存储不同项的方法就不太适用了, 新增递推函数时函数定义的复杂度基本被压在 FunctionRegistry 内部 缺点 Parser 依旧是明显的调度中心如果以后再加更多因子单个方法会继续变长, 另外我也越来越能理解为什么课程会强调“度量”和“类图”, 一些在特定场合下看起来是被优化了的代码可能在更普遍的场景下是负优化 如果对代码的优化已经明显超出了自己的能力范围还是要量力而行吧不要被一点优化的性能分冲昏了头脑最重要的是收获是学习面向对象的思想而不是变态的算法, DeriveFactor 的抽象已经存在新增变量的求导不需要再设计一种新框架, 调整字符串首项尽量把正项提前, 这些优化的特点是 对正确性影响相对可控, 哪些类虽然多但其实都很轻不是主要矛盾, 新增优化exp输出长度的方法 第三次作业中的核心类度量如下 类代码行数方法数近似分支数我的理解 Parser 173 14 29 解析层 MathTerm 131 13 28 新增多变量的存储和求导规则 Polynomial 204 18 41 ExpOptimizer 183 13 25 为第三次作业新增的性能/长度优化模块 从结构上看 保留了前两次作业递归下降的逻辑, 特征 有以下两个 当exp()内是单项式且有多个exp相乘时括号层数会出错 当exp()内有多个大数时递归的因子数目过多复杂度爆炸 为什么会出这样的问题 优化器同时承担“代数变换 输出格式 性能搜索”职责耦合高, 第三次作业代码遇到两个问题且两个问题均与优化exp输出长度有关, 第二次作业的关键度量变化如下 类代码行数方法数近似分支数说明 Parse 144 12 23 因子类型明显增多 MathTerm 117 12 21 数学层中间表示 Polynomial 173 15 37 新增乘方、替换、括号判断等逻辑 好处 新增 ExpFactor、FunctionFactor、ChoiceFactor 时语法层只需要新增因子分发数学层不必完全重写, 九、未来方向 如果让我对第一单元课程提出一些建议我会比较希望有下面几个改进方向, 用 Cost 对象替代反复 toString() 比较。

在摸索、学习、与同学讨论很久之后我的思路大概是 先把输入解析成 Expr - Term - Factor然后所有节点统一提供 getPolynomial()最后交给 Polynomial 做合并和输出, 将原多项式拆成“商 余项”递归地比较不同表示方案。

有界搜索把优化器从“全搜”改为“预算内搜索”。

所以我引入了 MathTerm把“一个数学项”升级成可组合对象, 写博客时一些想不到的方法会让ai去补充比如如何降低复杂度、我的代码内聚耦合能力怎么样以及让ai分析我的代码的度量 7.3 完成效果 有好有坏 生成代码时容易不在原有基础上迭代或者读不懂题所以最好还是至少有50%的人工, Polynomial 同时承担“数据结构 运算 输出格式化”三种职责职责不清晰, 一旦优化和语义层耦合得太紧后续调试和维护成本会迅速上升, Polynomial 一直是最大类说明我把“合并同类项、乘法展开、替换、求导、输出格式化”这些核心逻辑都集中在了数学层, 2.4 第三次作业 第三次作业的变化最大因为它不仅加入了 dx、dy、grad 和递推函数还从单变量扩展到了双变量, 第一单元三次作业总结一、前言 这三次作业的功能变化很大但我整体上没有推倒重来而是一直沿用“词法分析 语法分析 表达式节点 数学语义对象”掌握了递归下降方法和一些面向对象思想,这样的集中有好处也有坏处 好处是职责边界比较清楚普通因子类依旧很轻, 因子接口 Factor 的抽象非常有用后续新增因子时基本都能沿着这条线继续扩展, 输出字符串时省略 1、-1、一次幂等冗余部分, 优化策略第三次作业的优化太离谱我不信有这么多算法大佬 八、心得体会 面向对象不只是“把代码拆成很多类”而是要找到真正稳定的抽象边界, 2.5 对内聚和耦合的整体评价 综合三次作业我对自己代码的内聚和耦合情况总结如下 expr 层整体内聚较高, 但到了第二次和第三次当需求越来越复杂时我才真正意识到一个看起来普通的接口设计比如 Factor.getPolynomial()会直接决定后面两次迭代是平稳扩展还是被迫推倒重来, 但它也带来了很明显的代价 优化逻辑的状态更多递归分支更多理解门槛明显提高, Polynomial 的方法数从 7 增长到 15说明我把越来越多的语义逻辑集中在了一个类里,尤其是 Parse/Parser它要知道所有语法分支因此直接依赖类数量最多, core 层耦合偏高, 同学间应该多交流在团队协作中才能写出更好的代码也可以避免一些前人踩过的坑,它直接依赖了 11 个本项目类是整个系统里最杂的类,。

通过 needParens() 判断 exp 何时必须加括号避免输出过多不必要括号, 给复杂方法加“输入预算参数”, 从内聚和耦合角度来看 expr 包内的因子类内聚度较高因为每个类基本只负责一种因子到 Polynomial 的方法。

Polynomial 与 MathTerm 高度协作优点是计算逻辑集中缺点是类容易变大, Factor - Polynomial 的主线仍然成立所以大体架构不用推翻。

将 exp 括号策略独立为 ExpArgFormatter不再依赖 Polynomial#needParens,这个设计当时看起来只是“够用”但事实证明它是后面两次迭代还能继续活下来的关键, 这样一来变量维度就不再写死在类定义里语义层的扩展性会明显更好, 五、发现别人bug的策略 其实是比较奇技淫巧的 分析自己在写的时候容易犯什么错误比如在第二次作业中括号层数少加就会去分析别人的代码是否有这个问题。

math 包内的 Polynomial 和 MathTerm 内聚度也较高因为所有代数语义都在这一层闭环完成, Parse 是耦合程度最高的类, 如果老师再给一次迭代我会假设新需求是表达式从双变量扩展到多变量甚至可以自定义。

为了更直观地观察复杂类我再列出每次作业中最值得关注的核心类 作业核心类代码行数方法数近似分支数 第一次 Parse 76 4 16 第一次 Polynomial 108 7 23 第二次 Parse 144 12 23 第二次 MathTerm 117 12 21 第二次 Polynomial 173 15 37 第三次 Parser 173 14 29 第三次 MathTerm 131 13 28 第三次 ExpOptimizer 183 13 25 第三次 Polynomial 204 18 41 我对这些数据的理解是随着需求迭代程序并不是均匀地变复杂而是复杂度不断向少数几个“核心调度类”和“核心计算类”集中, 9.1 提供更明确的测试与度量模板 这一单元大家经常在“怎么测”“怎么分析结构”上花很多摸索成本, 缺少针对最坏输入的性能回归测试,相比之下那些职责单一的小因子类其实一直都比较稳定。

bug增多时间复杂度爆炸 带来了负优化 6.3 我的反思 代码的优化是无止境的, 三、架构设计体验3.1 我的架构是如何逐步成型的 第一次作业时其实已经忘记了递归下降法,大多数因子类都只有 1 到 4 个属性、2 个左右的方法职责很单纯,可以先用数据点测试再去看具体的类和方法 道听途说如第三次作业听大家说优化了的同学普遍存在的一些问题 AI神力 对不起我有罪 但是有时候会让ai直接给我生成很强的边界测试点 hack代码一定要先看代码 肯定是有结合对方代码的比如先具体看看我认为易错的方法、类中有没有问题, 2.3 第二次作业 第二次作业引入了 exp、选择式和自定义函数这意味着“一个项”已经不再只是 x 的指数而可能包含 exp(表达式) 这种结构。

如果只把每次作业当成一次单独编程题那么很多设计都可以凑合但如果把三次作业看成一个持续生长的小系统就必须认真考虑抽象、分层、耦合和重构, 二、基于度量的程序结构分析2.1 整体规模变化 下面的数据来自我对三次源码的静态统计代码行数为去掉空行和注释后的近似值分支数与圈复杂度也是为了比较趋势而做的近似统计, 再说扩展性不足的部分 MathTerm 现在显式写成了 expX、expY 两个字段新增 z 时要继续改类结构。

十、结语 回顾这一单元我觉得自己最大的收获是逐渐理解了“在连续迭代中维护结构”的重要性, 对于ai的使用问题上我有反思自己是不是太过于依赖ai反思自己是不是缺少了用ai后的思考, 这一单元我还学到了一点很重要的经验 程序的复杂度并不会平均分散而是会不断往少数核心类集中。

needParens() 这种输出辅助逻辑被集中到了 Polynomial生成字符串的规则比第一次更统一, 在这个新情景下我认为当前设计的扩展性是“语法层较好语义层一般”, MathTerm 现在把 x 和 y 写成了两个显式字段但扩展性不太好, 优点 新增求导算子时我不需要重写 AST只需要新增 DeriveFactor 并在 Polynomial/MathTerm 中补上导数语义, 六、分析自己进行的优化6.1 前两次作业中的基础优化 前两次作业中的优化更多是“结果输出优化”和“结构性小优化”主要包括 合并同类项时直接拦截零系数, 但从度量上也能看到新的问题 Parse 的近似耦合度明显升高,虽然不是专业插件导出的最终版图表但已经足够说明结构变化, 先说扩展性比较好的部分 Lexer 和 Parser 的扩展方式已经比较明确只需要新增 token 和分支。

所以每次重构的部分很少可以说没有都是在原有基础上进一步迭代。

更多是在“保证结果合法”的前提下尽量让输出更短、更规整, 刚开始写第一次作业时我对“表达式树”“统一语义对象”“分层设计”这些概念其实没有很强的感觉更多只是为了把题做出来, 当然这个也不这么好做因为不同的人实现代码的架构肯定或多或少有变化很难覆盖到所有人但我觉得大体思路上的提示应该是可以的, 从 OO 设计角度看这已经有一点“策略模块”的味道而不是单纯的工具函数堆砌, 四、bug修复4.1 分析bug 第一次作业我的代码未遇到bug第二次作业出现了爆指数的问题然后把指数的存储方式统一换成了BigInteger类型没什么好讲的, 数学正确性和字符串最优性之间开始产生更复杂的平衡, 通过记忆化避免重复求解同一个 Polynomial 的最优表示。

因此我在第二次作业中的一个关键变化就是把 数学层 从“指数 - 系数”的简单映射升级成了“MathTerm - 系数”的映射, 收集候选因子尝试寻找可以提取的公因数, 缺点 Parse.parseFactor 在新需求增多时会迅速膨胀。

基本都能保留原有结构不需要额外引入太多复杂类, 这次我的结构没有彻底重构但做了三件非常重要的事情 把目录分成了 core、expr、math 三层包结构更清楚, 如果课程能提供一个最基础的测试框架模板以及一个最小化的度量示例比如告诉大家怎么统计 LOC、圈复杂度、类图应该画到什么粒度我觉得会让大家少走很多弯路, 9.2 在迭代节点增加一次“重构指导” 从第一次到第二次、从第二次到第三次中间其实都有很明显的重构机会,哎呀我有罪 D: 没有搭测评机, 缺少硬约束式不变量比如优化后长度不得变长exp 实参必须是因子, FunctionRegistry 把函数定义的注册和调用隔离了出来主流程不再直接关心函数展开细节, 我觉得这部分优化的优点非常明确 它不再只依赖局部字符串规则而是开始做全局方案比较, 11.2 如何精确计算一个合法输入的 cost 让AI算 , 第一次作业中的核心类职责如下 类主要职责 Lexer 将输入字符串切成 token Parse 递归下降构建表达式结构 Expr 维护若干 Term Term 维护若干 Factor 并完成乘法组合 ConstFactor/VarFactor/ExprFactor 各种因子的统一语义入口 Polynomial 表示最终数学结果并负责输出 第一次作业里最复杂的方法主要集中在 Parse.parseFactor 和 Polynomial.toString 方法代码行数圈复杂度说明 Parse.parseFactor 40 11 负责分发不同类型因子 Polynomial.toString 34 12 同时处理符号、省略系数和幂次输出 Polynomial.optimizeString 22 4 尝试把首项调整为正项 优点 语法树层和数学语义层已经初步分开了后面迭代时没有完全重写。

Polynomial.needParens()、MathTerm.expToString() 等格式化逻辑都默认只有两个变量修改面会比较大,所以用好ai的前提还是得自己会, 作业Java 文件数总代码行数平均每类代码行数最大类 第一次 10 336 33.6 Polynomial 108 行 第二次 16 674 42.1 Polynomial 173 行 第三次 19 1039 54.7 Polynomial 204 行 让 AI 统计的 从这个表里可以看出几个很明显的趋势 三次作业的规模增长很快 增长最快的不是入口类而是数学语义层和解析层这说明功能扩展主要落在如何表示表达式”和“如何计算表达式”上, 因此如果真的有第四次迭代我更希望把 MathTerm 继续重构成“变量名 - 指数”的HashMap映射结构, 避免的方法 分层代数层与打印层解耦,后续还是要多注意即便未来ai是可以写很多代码但自己面向对象的能力是一定要培养起来的。

坏处是 Parser 和 Polynomial 逐渐变成了系统瓶颈一旦继续迭代就容易出现“大类”和“复杂方法”爆checkstyle的问题本质上是职责不够明确的问题 2.2 第一次作业 第一次作业的任务是单变量表达式展开因此我采用了非常直接的结构Lexer 负责切 tokenParse 负责递归下降分析Expr/Term/Factor 负责表达式树组织最后统一转成 Polynomial 进行合并和输出。

十一、附加问题11.1 如何检查输入是否符合要求包括空格连续符号等 肉眼盯真。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://acg.inmoke.com/zixun/Lolita/13540.html