程序设计阶段性作业 4-6 综合复盘 —— 数字电路模拟程序迭代开发总结
(1)前言
这三次连续迭代作业分别是数字电路模拟程序第 1 版、第 2 版、第 4 版,跳过了第 3 版时序电路内容,整体属于同一个项目的渐进式开发。三次作业都围绕数字逻辑门电路仿真展开,每一次迭代都在上一版代码基础上增加新功能,难度逐步拔高。
作业集 4 也就是第一版程序,只包含五种基础逻辑门:与门、或门、非门、异或门、同或门。题目只需要解析输入文本、搭建电路连线、逐级计算每一个门电路的输出电平。题量适中,一共需要编写 8 个实体类外加解析类、打印类,合计 10 个 Java 类。难度中等,核心难点在于字符串解析,拆分引脚名称与元件编号,同时还要控制信号的计算先后顺序,如果先计算下游元件,上游门电路还没有算出结果,就容易出现空值问题。这一版只需要保证正常用例顺利运行,不用处理非法输入,只要完成引脚赋值和逻辑运算就可以拿到满分。
作业集 5 对应第二版程序,在原有 5 种基础逻辑门之上,新增了三态门、译码器、数据选择器、数据分配器 4 种组合电路元件。本次改动幅度非常大,代码量几乎翻倍。原来所有元件都只有单一输入引脚和输出引脚,新版本增加了控制引脚,并且严格规定引脚排序:控制引脚排在第一位,之后是输入引脚,最后才是输出引脚。除此之外输出格式也发生了变化:普通逻辑门打印引脚电平,译码器只输出唯一低电平引脚编号,数据分配器要输出带有无效符号 “-” 的字符串。这一版难度直接提升到困难级别,最大的难题是对父类方法进行重写,适配 0 号起始的控制引脚,同时区分元件工作状态,三态门控制端为 0 时输出直接置空,不能参与运算。
作业集 6 对应第 4 版程序,舍弃了时序电路内容,新增两大核心模块:子电路自定义引用、连线语句的异常检测。这是三次作业里难度最高的一次。代码量再次扩充,不仅需要编写子电路管理类,还要独立开发异常校验模块。题目给出 5 级异常,必须严格遵守优先级,高优先级错误优先输出,一旦检测到第一条错误就直接终止后续判断。子电路可以独立定义,内部自带一套完整门电路,子电路元件编号可以和主电路重复,输出结果时还需要拼接子电路编号。另外还要增加引脚冲突检测,同一个输入引脚不能接收两路信号,一旦出现冲突立刻输出错误信息。
整体来看,三次作业是标准的增量迭代项目,开发节奏循序渐进。第一版锻炼字符串处理、抽象类与继承;第二版练习方法重写、多态、多样化输出格式控制;第三版学习组合思想、语法校验、优先级判断、嵌套电路的分步运算。测试用例也从简单的串联电路,慢慢延伸到多级嵌套、非法语句、引脚冲突等边界场景。
(2)设计与分析
2.1 整体代码结构分析
本次项目全程使用面向对象的开发思想,顶层设计了抽象父类 LogicGate,它是所有门电路的基类。父类中统一定义公用成员变量:元件编号、输入引脚总数、引脚数值数组、已接入信号计数、输出值 output。同时封装通用赋值方法 setValue (),并且定义抽象计算方法 calc (),每一种门都要重写这个方法来实现自身的逻辑运算。第一版搭建好这套结构之后,后续两次迭代都没有修改顶层父类,严格遵循对扩展开放、对修改关闭的原则。
第一次作业(作业集 4)在父类基础上派生了 5 个子类:AndGate 与门、OrGate 或门、NotGate 非门、XorGate 异或门、XnorGate 同或门。子类只需要重写 calc 运算函数即可。配套工具类一共包含:InputParser 输入解析类,逐行读取文本,区分 INPUT 输入语句和 [] 连线语句;InputSignal 用来存放外部输入的 0、1 电平;WireList 存储所有连线语句;GateList 统一管理所有门对象,可以根据元件名称快速查找实例;Circuit 主电路类,完成引脚之间的信号传递与赋值;OutputPrinter 打印类,严格按照元件类型、编号升序输出结果。
第二版作业(作业集 5)完全复用原有代码,仅新增 4 个元件子类:TriStateGate 三态门、Decoder 译码器、Multiplexer 数据选择器、Demultiplexer 数据分配器。这里遇到了结构适配问题:普通门引脚从 1 开始计数,但是三态门 0 号引脚是控制端,所以必须重写 setValue () 方法,放开引脚编号限制,同时重写 isFull () 方法,保证控制引脚和输入引脚全部接收到信号才会进入运算环节。我还新增了 getOutputPinNumber () 方法,用来区分不同元件的输出引脚,普通门输出引脚固定为 0,三态门输出引脚是 2,译码器无单一输出引脚。GateList 和 Circuit 几乎不用改动,只需要在创建元件的分支中新增 S、M、Z、F 四类标识符。打印类改动较大,因为三种元件输出格式完全不同,只能拆分出普通门、译码器、数据分配器三段打印逻辑,用分支语句区分。
第四版作业(作业集 6)是改动最大的一轮,引入了组合组件思想。我新建了 SubCircuit 子电路类,每一个子电路都是独立的小型电路,内部自带门电路、连线、输入输出端口,再配套 SubCircuitList 集合统一管理所有子电路。除此之外单独拆分出 ExceptionChecker 异常检测类,逐条校验每一条连线语句,严格按照优先级排查 5 类错误。Circuit 主电路大幅修改,增加子电路端口解析逻辑,当连线指向 C 开头的子电路引脚时,会把外部信号传入子电路内部,等到子电路所有输入全部齐全后,再执行内部电路运算。打印类也需要分层遍历,先输出所有子电路内部的门电平,再打印主电路元件,并且在元件名称前面拼接子电路编号。
2.2 代码优缺点心得
优点:项目全程保持继承结构稳定,基础门电路代码全程没有大面积修改,只做新增扩展,三次迭代不需要推翻重写旧代码,大大降低了迭代开发的工作量。信号读取采用懒计算机制,只有当前元件引脚全部接满信号时,才会自动调用 calc () 计算输出,不会出现上游还未运算就读取空值的问题。所有容器采用数组实现,运行效率更高,可以满足题目时间限制。
缺点同样十分明显,也是后续优化的重点:
第一,异常校验代码和主电路逻辑高度耦合,ExceptionChecker 直接读取全部连线文本,如果后续新增错误类型,必须修改类内部的判断代码,不符合单一职责。
第二,子电路内部和主电路重复编写了连线解析 connect 方法,出现大量重复代码,代码冗余度偏高。
第三,打印类分支语句过多,九类元件的打印逻辑全部挤在同一个类中,后续再新增元件就需要不断追加 if 判断,代码会越来越臃肿。
第四,数组容量直接写死为固定数值,只能应付现有测试用例,如果输入数据量变大,很容易出现数组越界崩溃。

pta最终通过结果
(3)采坑心得

pta多次提交记录
三次作业我在 PTA 平台前后一共提交了近 70 次,大部分失败都是逻辑漏洞和格式问题,下面结合程序运行截图、测试数据,把调试过程中遇到的问题逐一总结,避免空话。
坑 1:串联电路运算顺序错乱(作业集 4 最主要 bug)
问题现象:多级门电路串联时,下游元件引脚始终接收不到有效值,程序直接忽略该元件的输出。
测试场景:样例 4,与门输出接到非门,非门输出再送入或门。程序一开始会按照元件创建顺序批量执行计算,下游的或门先运算,此时上游非门还没有执行 calc (),输出为空,引脚计数无法达标。
调试过程:反复打印引脚数值,发现上游元件还未运算,下游就已经开始读取信号。
解决方案:改造 getSignal 信号读取方法,在读取上游门输出时,自动判断元件输入是否齐全,如果满足运算条件但是还未计算,就立刻执行 calc ()。实现信号按需计算,谁调用信号,就先完成上游电路运算,不需要手动对电路做拓扑排序。修改之后,串联电路的运算顺序自动跟随信号流向,这一问题顺利解决。
坑 2:三态门控制引脚无法赋值(作业集 5 重大故障)
问题现象:三态门的测试用例一直无法通过,控制引脚始终接收不到数值,isFull () 一直返回 false。
根源:父类 LogicGate 的 setValue 方法限制引脚必须大于等于 1,但是三态门 0 号引脚为控制端,赋值语句直接被拦截,计数 count 永远达不到 2。
调试:打印引脚编号与传入数值,发现 0 号引脚根本无法进入赋值分支。
修复手段:重写 TriStateGate 内部的 setValue 函数,把引脚判断条件修改为 pin>=0,同时重写 isFull (),必须填满控制端和输入端两个引脚才判定输入齐全。译码器引脚分为控制区、输入区,也单独重写赋值逻辑。这个 bug 前后耗费了两个小时,一开始一直在排查字符串解析,最后才定位到父类引脚范围限制。
坑 3:输出格式分支混乱,多次格式错误
第二版有三种截然不同的输出格式:普通门打印 “元件名 - 引脚号:电平”;译码器只输出选中引脚编号;数据分配器输出带 “-” 的字符串。
最开始我把所有打印逻辑写在同一个循环里,没有拆分方法,导致译码器多余打印了引脚号,和样例格式不一致,连续十几次提交都卡在格式错误上。
最后拆分出三个独立打印函数:printNormalGates、printDecoders、printDemultiplexers,先判断元件类型,再调用对应打印方法,同时对同类元件按照编号从小到大排序,输出顺序才符合题目要求。
坑 4:异常优先级颠倒(作业集 6 最难处理的问题)
题目明确规定错误优先级:多输出源 > 无输入 > 无输出 > 顺序颠倒 > 引脚冲突。多条错误同时出现时,只输出优先级最高的一条。
我最初写判断语句时,先检测引脚冲突,再检测连线格式,导致高优先级错误被覆盖。例如一条连线同时存在多输出源和引脚冲突,程序优先输出了引脚冲突,直接违反题目规则,连续 8 次提交失败。
最终解决方案:严格按照题目给出的先后顺序从上到下编写 if 判断,只要任意一条条件成立,直接 return 结束检测,不再执行后续语句,保证优先级严格生效。
坑 5:子电路引脚识别出错,主电路信号无法传入子电路
子电路引用格式为 C1-A,代表 1 号子电路的 A 输入端口。最开始拆分字符串时,无法区分这是子电路输入引脚还是输出引脚,程序把输入端口当成输出端口,拒绝赋值。同时子电路内部门对象和主电路门对象放在同一个集合里,出现元件查找混乱。
修复:给每一个 SubCircuit 子电路单独配置独立的 GateList 容器,主电路和子电路的元件互相隔离,编号不会冲突。再编写判断函数 isSourcePin,严格区分外部输入、门输出引脚、子电路输入端口、子电路输出端口,分清信号源与接收端。
坑 6:引脚信号冲突只检测主电路,漏掉子电路内部校验
一开始我只检查主电路连线的引脚重复赋值,忽略了子电路内部连线,子电路同一个引脚接收两路信号时,程序不会抛出异常。
后续修改 ExceptionChecker 代码,循环遍历所有子电路内部连线,主电路、每一个子电路都单独做引脚冲突校验,只要出现多路输入,立刻输出 ERROR 并终止程序。
坑 7:定长数组导致下标越界
所有存储字符串与对象的数组都固定了长度,当测试用例连线数量过多时,会触发数组越界运行异常。临时解决办法是适当放大数组容量,如果长期优化,需要把固定数组替换为动态集合。
(4)改进建议
结合三次迭代暴露出来的代码缺陷,我从代码耦合度、分支结构、容器、扩展性、异常校验五个方向给出可持续优化方案,为后续拓展时序电路、多层子电路打好基础。
改进一:拆分类职责,降低代码耦合
当前 Circuit 电路类同时承担连线解析、信号传递、元件运算多重工作,代码耦合度太高。可以按照单一职责原则拆分:
新建 SignalManager 信号管理类,统一管理所有外部输入、门输出、子电路端口信号,统一处理懒加载运算;
把连线文本解析逻辑抽离成独立的 WireParser 类,主电路只接收解析完成后的引脚关系,不再处理字符串分割;
将 ExceptionChecker 完全解耦,只接收连线文本字符串数组,只返回错误文本,不参与电路运行逻辑。
拆分完成后,后续新增异常规则、新增元件都不会改动主电路代码,真正实现开闭原则。
改进二:利用多态消除打印类大量 if 分支
现在 OutputPrinter 充斥大量类型判断语句。优化方案是在 LogicGate 抽象父类中新增抽象方法 printResult (),每一个门子类自行实现打印逻辑。普通门打印引脚电平,译码器打印选中编号,分配器打印带 “-” 的字符串。主循环只需要遍历所有门对象,直接调用 printResult (),不需要再判断元件类型。后续新增时序元件,只需要重写打印方法,不用修改主打印循环。
改进三:把定长数组替换为动态容器
当前所有集合都是固定长度的数组,一旦数据超出容量就会崩溃。后续将 String [] 数组全部替换为 ArrayList 动态数组,自动扩容,不用手动预估最大数据量,避免运行时下标越界,提升程序健壮性。
改进四:完善组合模式,支持子电路多层嵌套
当前程序只支持主电路调用一级子电路,无法实现子电路内部再嵌套其他子电路。按照题目给出的组合模式,把 SubCircuit 和 LogicGate 继承同一个顶层组件类,门电路作为叶子节点,子电路作为组合节点。无论是基础门还是子电路,都统一拥有输入、输出、运算方法,实现无限层级的子电路嵌套。
改进五:扩充语法异常校验
目前只检测了引脚冲突和连线格式问题,还没有校验非法元件名、非数字引脚、括号不匹配等文本语法错误。后续可以新增词法校验,严格限制元件标识符只能是 A/O/N/X/Y/S/M/Z/F,关键字拼写错误、引脚字符非法都可以给出对应的错误提示,完善第 4 版全部异常检测需求。
改进六:用拓扑排序替代懒加载运算
现在依靠懒加载来保证运算顺序,电路规模变大之后会产生大量重复计算。优化方案:先把整个电路抽象为有向图,对所有门元件进行拓扑排序,生成严格按照信号流向排列的运算队列,依次执行计算,避免重复调用 calc 方法,提升大型电路的运行效率。
(5)总结
经过三次迭代开发,我完整走完了小型项目从基础版本、功能扩展再到结构重构的全过程,收获远远超过单纯刷题写代码。
首先,面向对象思想从书本理论落到了代码实践。第一次写基础门电路时,我只会零散地编写多个独立类,完全不理解抽象类的作用。在写完五个基础门之后,我把重复的变量和方法向上抽取到 LogicGate 父类,子类只保留独有的运算逻辑。第二版新增组合元件时,我不再修改原有代码,只新增子类并重写方法,切身感受到继承与多态带来的扩展性。第四版实现子电路组合结构,让我初步理解了设计模式在项目迭代中的实际用处。
其次,我熟练掌握了文本解析与字符串处理。三次作业的核心难点就是拆分不规则输入文本,截取元件名称、引脚编号、子电路标识。我反复练习了 indexOf 截取括号内容、split 分割多空格字符串、拆分横杠分隔的引脚文本,学会分步解析语句,先拆分整行内容,再分离元件与引脚,区分外部信号和内部端口。同时养成了分步调试的习惯,打印中间变量,快速定位空值、字符串拆分失败这类隐性 bug。
第三,我学会了增量迭代开发,而不是一次性写完全部代码。如果直接上手开发带子电路和异常检测的完整版程序,代码一定会杂乱无章。分三版逐步开发,每一版只新增一小部分功能,保证当前版本稳定可用,再在下一版迭代升级,极大降低了开发难度。迭代过程中保留稳定版本代码,只做扩展、不随意修改底层结构,这也是工程开发最基础的规范。
同时我也清晰看到自身不足,还有很多内容需要深入钻研:
图论知识薄弱,拓扑排序掌握不熟练,目前只能依靠懒加载规避运算顺序问题,后续需要系统学习拓扑排序,优化电路运算流程;
代码工程化能力不足,类职责划分不够清晰,代码耦合度高,分支语句过多,后续需要持续练习代码重构,减少冗余代码;
异常处理经验匮乏,本次 5 级优先级异常校验耗费了大量调试时间,今后养成先校验输入、再执行业务代码的编程习惯;
设计模式运用比较生硬,组合模式只实现了单层子电路,多层嵌套组件还需要继续打磨。我打算继续完善本项目,补全时序触发器、多层子电路嵌套功能,完成整套数字电路仿真程序。
总体来说,这三次作业已经脱离了单纯的 PTA 刷题,属于一次完整的 Java 面向对象小型项目实战。从基础逻辑门,到多路组合电路,再到嵌套子电路与非法输入校验,整个开发流程锻炼了我的代码设计、排错调试和迭代开发能力。本次复盘把所有 bug、代码缺陷和优化方案全部记录下来,不仅可以完善当前项目,也能为后续课程设计积累经验。
浙公网安备 33010602011771号