25202214第二次BLOG作业

作业集4-6阶段性总结
一、前言
本阶段的三次作业集围绕电路元件、信号传递、组合器件以及子电路建模逐步展开。和前面几次偏基础语法、单类设计的练习相比,作业集4到作业集6的变化比较明显:题目不再只是要求写出某个单一算法,而是要求把输入数据解析、对象建模、连接关系维护、信号求值、错误处理和结果输出组合在一个相对完整的程序中。也就是说,代码的重点逐渐从“能算出答案”转向“能把复杂对象组织清楚,并且在规则变化时保持可维护”。
作业集4主要处理基本逻辑门电路,包括与门、或门、非门、异或门、同或门等元件。它的知识点集中在字符串解析、集合类使用、队列式信号传播、继承与多态等方面。作业集5在前一次基础上扩展了更多器件,如三态门、译码器、数据选择器、分配器等,难点明显从基本门电路求值转移到复杂器件的有效状态判断和递归求值。作业集6进一步引入子电路、输入输出端口、连接合法性校验和作用域转换,程序不仅要能求值,还要能判断连接语句是否满足题目规则。
从题量上看,三次作业的题目数量并不算特别多,但每一道题内部的规则密度较高,尤其是作业集5和作业集6,单个测试点可能覆盖多种元件、多级连接、无效信号和异常连接情况。对我来说,第四次作业的难度主要在于建模起步,第五次作业的难度主要在于器件规则细节,第六次作业的难度则主要在于输入合法性判断和子电路命名空间管理。三次作业之间有明显的迭代关系,前一次设计中的优点和缺陷会直接影响后一次扩展时的成本。
为了更客观地分析源码,我使用 SourceMonitor 对三次提交的 Java 文件进行了统计,同时结合自己对类结构和处理流程的整理,形成了下面的分析。需要说明的是,本文原则上不粘贴完整源码,而是从数据结构、类设计、流程和复杂度几个角度进行复盘。
二、SourceMonitor 指标概览
下面是 SourceMonitor 导出的关键指标。三次作业都只有一个 Main.java 文件,但代码规模和复杂方法的位置发生了明显变化。
作业 Lines Statements Classes and Interfaces Maximum Complexity Maximum Block Depth Most Complex Method
作业集4 242 168 7 29 8 Main.main()
作业集5 289 263 2 39 8 Main.evaluateComponent()
作业集6 305 244 4 35 8 Main.main()

从表中可以看出,作业集4虽然代码行数不是最高,但类数量最多,原因是我在第四次作业中采用了抽象类 Component 和多个具体逻辑门类的设计。作业集5的代码行数增加到289行,语句数达到263,最大复杂度也提高到39,最复杂的方法变成 evaluateComponent(),说明复杂度已经从主流程转移到器件求值规则本身。作业集6代码行数继续增加到305行,但语句数比第五次略少,最大复杂度为35,复杂度重新集中在 Main.main(),这和第六次需要同时完成解析、校验、求值和输出有关。
这些数据说明一个问题:代码规模的增加并不只是行数增加,更重要的是职责是否集中在少数方法中。第四次作业虽然类多,但每个具体门类的计算逻辑很短;第五次作业为了快速处理多种器件,把规则集中放入一个 switch 方法中,短期内实现方便,长期看则形成了一个复杂度较高的“中心方法”;第六次作业虽然增加了 Conn、Gate、Type 等辅助结构,但主流程仍然承担了较多职责,后续仍有拆分空间。
三、作业集4设计与分析
作业集4的核心任务是根据输入的连接关系和初始输入信号,求出各类基本逻辑门的输出。我的设计以三个主要容器为基础:pinState 保存每个引脚当前的逻辑值,connections 保存从源引脚到目标引脚的连接列表,components 保存已经注册的元件对象。在读取输入时,程序先解析 INPUT: 后面的初始引脚值,再解析连接语句中的方括号内容,并根据引脚名登记对应元件。
在求值方式上,作业集4使用了类似广度优先的信号传播策略。初始输入引脚先进入队列,每次从队列中取出一个当前引脚,如果它有后继连接,就把该信号传播到目标引脚;如果当前引脚属于某个元件的输入端,则尝试计算该元件输出端。当输出发生变化时,再把输出端放回队列继续传播。这个思路比较直观,适合处理基本门电路的单向传播。

类结构上,作业集4采用了比较典型的面向对象设计。抽象类 Component 保存元件名称、类型、输入端数量和编号,并提供统一的 evaluate 方法。具体的 AndGate、OrGate、NotGate、XorGate、XnorGate 只需要实现自己的 compute 方法。这样的好处是每种门的计算规则独立,阅读时可以直接定位到对应类;排序逻辑也可以通过 Comparable 放在共同父类中,避免主程序到处写重复比较代码。
不过,作业集4也暴露出一些问题。第一,主函数过长,SourceMonitor 显示 Main.main() 的复杂度达到29,说明解析、建图、求值、输出都挤在同一个方法中。第二,程序使用 MAX_ITER 作为循环上限来防止异常情况,这是一种兜底办法,但不是对环路或非法连接的真正建模。第三,引脚名仍然主要依靠字符串拆分,如果题目格式继续扩展,字符串处理逻辑会变得越来越脆弱。
总体来看,作业集4的对象设计是三次作业中相对清晰的一次。它让我体会到,哪怕题目本身是算法题,只要存在多种对象类型,就应该尽早考虑抽象层次。多态不是为了让代码看起来复杂,而是为了把变化点放到合适的位置。
四、作业集5设计与分析
作业集5在作业集4的基础上加入了更多类型的器件,求值规则也更复杂。和第四次不同,我在第五次作业中没有继续为每种器件建立单独子类,而是采用了一个内部 Component 数据类保存类型、编号、输入规模和名称,再通过 evaluateComponent 方法中的 switch 分支分别处理不同器件。
这一版设计的核心数据结构包括 inputSignals、srcOf、components 和 cache。其中 inputSignals 保存外部输入,srcOf 用来记录目标引脚来自哪个源引脚,components 保存元件定义,cache 缓存已经求出的引脚值。求值方式也从第四次的队列传播变成递归追溯:当程序需要某个引脚值时,先查缓存,再查外部输入,如果该引脚有来源,就递归求来源值;如果它是某个元件的输出端,则根据该元件的类型和输入引脚计算结果。
这种写法对第五次作业比较有效,因为复杂器件存在“只有某个输出端有效”或者“未选中端口无效”的情况。递归求值可以按需计算,不需要像第四次那样把所有信号都主动传播一遍。缓存的加入也减少了重复计算,特别是在多个输出依赖同一批输入时,可以避免反复追溯。
但从 SourceMonitor 数据看,第五次作业的问题也很集中:Main.evaluateComponent() 的复杂度达到39,语句数达到102,是三次作业中最突出的复杂方法。它同时处理与门、或门、非门、异或门、同或门、三态门、译码器、数据选择器、分配器等多种器件,每增加一种器件,就需要继续扩展 switch。这种结构在短时间内比较容易写完,但后期维护成本较高。
我在这一阶段最大的体会是:当题目规则开始快速增加时,不能只追求把分支补全,还要考虑规则之间的共同抽象。例如,很多器件其实都在做“读取若干控制端”“判断是否有效”“根据索引选择输入或输出”这几类事情。如果把这些共同动作封装成辅助方法,evaluateComponent 就不会变得那么长。另一个需要注意的问题是无效值 -1 的使用。它让程序能直接表示“当前引脚无有效信号”,但也要求每个器件都严格处理 -1,否则就会出现无效信号被错误传播的问题。
五、作业集6设计与分析
作业集6的重点不再只是器件求值,而是增加了子电路和连接合法性检查。程序中新增了 Conn 表示连接关系,Gate 表示门元件,Type 枚举表示引脚在连接语句中的源端、目标端或未知状态。相比前两次,第六次更像是在实现一个小型电路解释器:先读取主电路和子电路定义,再把局部引脚转换为全局引脚,最后检查连接是否合法并进行求值。

第六次作业中最关键的函数之一是 toGlobal。当程序处于子电路作用域时,局部引脚名会被转换为带子电路前缀的全局名,这样可以避免不同子电路中同名引脚发生冲突。另一个关键函数是 classify,它根据当前作用域、主输入、子电路输入输出、引脚编号等信息判断一个 token 是源端还是目标端。validateConnection 则负责检查一条连接语句是否存在多个输入、没有输入、没有输出、输入输出顺序错误以及输入信号冲突等问题。
从数据看,作业集6的最大复杂度为35,最复杂方法显示为 Main.main(),其次是 Main.classify() 和 Main.registerGate()。这说明第六次的复杂度更多来自整体流程和分类判断。尤其是 classify,它需要理解主电路、子电路、元件输出端、元件输入端、子电路输入端和子电路输出端之间的区别。如果这个分类出现错误,后面的连接校验和求值都会受到影响。
第六次作业让我认识到,子电路问题本质上是命名空间问题和边界问题。仅仅把字符串拼接成 scope + "-" + tok 可以解决一部分冲突,但不是最理想的长期方案。更好的做法是建立 Pin 类,明确保存作用域、元件名、端口编号和端口方向,而不是每次都从字符串中重新拆解。这样既能减少重复解析,也能让错误信息更准确。
六、采坑心得
第一类问题是引脚名解析。第四次作业中使用 lastIndexOf('-') 拆分引脚名,相对稳妥;第五次作业中部分位置使用 split("-"),在当前格式下可以运行,但如果以后出现更多层级的命名,可能会产生隐患。第六次作业引入子电路后,字符串中出现多个横线的可能性更高,这说明引脚名不应该一直作为普通字符串处理。
第二类问题是复杂方法堆积。第五次作业的 evaluateComponent 是最典型的例子。写的时候会觉得每个分支都很清楚,但等所有器件都写完后,方法已经变得很长。复杂度高的方法不仅难读,也更容易在修改一个器件规则时影响其他器件。后续如果继续扩展,应该把每种器件抽成独立求值策略,或者至少把控制端读取、索引计算、有效性判断等公共逻辑拆出来。
第三类问题是无效信号处理。第五次作业中大量使用 -1 表示无效状态,这个约定必须贯穿所有器件。比如数据选择器只要求被选中的数据端有效,未选中的端口可以不影响结果;分配器只有被选中的输出端显示输入值,其他输出端应显示无效或占位。这里最容易出错的是把所有输入端都要求有效,导致本来应该通过的情况被错误判为无效。
第四类问题是错误输出和程序退出。第六次作业中为了满足题目要求,检测到连接错误后直接输出错误信息并 System.exit(0)。这种写法对单次运行的在线评测比较直接,但在本地调试时不方便,因为一旦出现错误,后面的状态无法继续观察。更好的方式是把错误封装成对象或异常,在统一位置处理输出。
第五类问题是测试覆盖不系统。三次作业都应该为每类元件准备最小测试数据,例如全0输入、全1输入、缺失输入、非法输出端、重复连接、子电路输入输出冲突等。如果只依赖题目样例,很难发现边界问题。尤其是第五次和第六次,测试点很可能组合多个规则,单个规则看起来正确,并不代表组合后仍然正确。
七、改进建议
第一,建立更清晰的数据模型。后续可以把程序拆成 Pin、ComponentSpec、Circuit 三层。Pin 负责表达引脚身份,ComponentSpec 负责表达器件类型和端口规则,Circuit 负责保存连接图和求值状态。这样字符串解析只在入口处进行一次,后续逻辑都操作结构化对象。
第二,把器件求值从大 switch 中拆出。可以定义统一接口,例如 Evaluator,每种器件实现自己的输入读取和输出规则。这样新增器件时只需要新增一个类或策略,而不是修改一个越来越长的方法。对于简单门电路,第四次作业的继承结构其实已经提供了一个可参考的方向。
第三,主流程需要拆分。读取输入、解析连接、登记元件、校验连接、传播信号、排序输出应该分成多个方法。主函数只保留流程编排,不直接承载所有细节。这样 SourceMonitor 中 Main.main() 的复杂度会明显降低,程序也更容易定位错误。
第四,建立统一的错误处理机制。第六次作业中的错误信息有固定格式,因此可以定义 CircuitError,里面保存错误类型、原始连接语句和相关引脚。校验阶段只负责发现错误,输出阶段统一负责打印格式。这样既符合题目输出,又方便调试。
第五,补充回归测试。每次修改器件规则后,至少运行一组基础门电路测试、一组复杂器件测试、一组子电路测试和一组非法连接测试。对于容易出错的递归求值,还应加入缓存清空测试,确保多组输入或新电路不会复用旧结果。
八、总结
通过作业集4到作业集6,我最明显的收获是对“建模”的理解加深了。第四次作业让我认识到,面对多种逻辑门时,面向对象封装可以让规则更清晰;第五次作业让我认识到,当器件类型快速增加时,如果不及时拆分职责,复杂度会集中到少数方法中;第六次作业则让我认识到,子电路和连接校验不仅是求值问题,更是命名空间、方向判断和错误处理问题。
这一阶段我还体会到,代码能通过部分测试点并不代表设计已经足够好。在线评测更关注结果,而阶段总结更关注自己为什么这样写、哪里写得不合理、下一次应该怎样改。SourceMonitor 的数据给了我一个比较直观的提醒:复杂度最高的方法往往就是最需要重构的位置。如果一个方法承担了过多职责,后续扩展时就会越来越被动。
27111f40da1043f1064d4247b342f394
131ed8415940fec02166fe91b8bf5b65

e6743d162800e26e41aa7fe0ca97332b

后续需要继续学习的内容主要有三点。第一是面向对象设计,尤其是如何识别类、接口和策略模式的使用场景。第二是图结构建模,因为电路连接本质上可以看作有向图,信号传播、环路检测和拓扑关系都可以用图的思想解释。第三是测试方法,特别是如何围绕边界条件设计小而有效的测试用例。只有把设计、实现和测试结合起来,才能让程序在题目继续迭代时保持可持续改进。

posted @ 2026-06-24 21:44  CR-NCHU  阅读(17)  评论(0)    收藏  举报