数字电路模拟程序设计——作业集4~6总结
一、前言
本阶段三次PTA作业围绕“NCHUD-数字电路模拟程序”展开迭代开发,从基础逻辑门模拟到复杂组合逻辑元件,再到子电路与异常处理,完成了一次完整的数字电路仿真系统构建。三次作业环环相扣、层层递进,完整复刻了数字电路从基础元件到模块化电路的工程实现逻辑。
1.1三次作业集概览
| 作业 | 题目 | 得分 | 核心知识点 | 代码量 |
| 第一次 | 数字电路模拟程序-1 | 100 | 五种基本逻辑门(与、或、非、异或、同或) | 约200行 |
| 第二次 | 数字电路模拟程序-2 | 100 | 新增三态门、译码器、数据选择器、数据分配器 | 约350行 |
| 第三次 | 数字电路模拟程序-4 | 81 | 子电路定义与引用、异常输入检测(组合模式) | 约500行 |
1.2知识点覆盖
三次作业涉及的知识点可以归纳为以下几点:
(1)面向对象设计:
类的封装与继承、多态的应用、组合模式(Composite Pattern)的实践。第三次作业明确要求采用组合模式,将子电路视作复合元件,与基础门电路统一管理。
(2)数据结构与算法:
HashMap用于信号存储和元件查找、拓扑排序思想处理信号依赖关系、字符串解析(正则表达式与手动遍历)。
(3)数字电路基础知识:
五种基础门电路的逻辑功能、三态门的高阻态概念、译码器/数据选择器/数据分配器的工作原理、信号传播与组合逻辑电路的仿真机制。
1.3题量与难度感受
三次作业虽然每次只要求完成一道题目,但内部逻辑复杂性逐次递增。第一次作业相对友好,主要挑战在于理解电路连接格式和设计信号传播机制;第二次作业难度明显提升,新增四种元件各有不同的引脚定义和输出格式;第三次作业最大的挑战在于子电路的引入使系统复杂性大幅增长,同时还要处理五种异常情况的检测与优先级输出
二、设计与分析
2.1第一次作业
(1)类图设计

(2)设计分析
第一次作业我采用了极简的数据驱动模型。程序的核心是一个通用的 Gate 类,内部仅用 type 字段(A/O/N/X/Y)区分元件种类,配合 String[] inputSources 记录信号来源。信号传播采用深度优先递归策略:当需要获取某个门输出引脚的电压值时,getPinValue() 方法会递归地向后追溯其所有输入引脚的来源,直到找到外部输入信号(INPUT: 行定义的值)或已计算完毕的门
(3)设计亮点
代码密度高:用一个类 + 一个递归函数覆盖了五种门电路的全部场景,没有冗余的继承体系。
隐式处理级联:递归传播天然支持任意深度的门级联,无需手动维护计算顺序,对于无反馈的组合电路非常优雅。
解析通用性强:通过正则匹配 A(8)1 和 X5 两种命名模式,统一提取了输入引脚数和编号。
(4)设计不足
严重违背开闭原则(OCP):若后续迭代新增元件(如三态门、译码器),必须修改 Gate 类中的 compute() 分支和 getPinValue() 逻辑,极易引入bug。
“输出即0号引脚”写死:代码强依赖 pinStr.endsWith("-0") 判断是否为输出,这在第二次作业遇到多输出元件(译码器、分配器)时将彻底失效。
缺乏引脚角色抽象:将“输入”和“控制”混为一谈,无法表达三态门中控制端对数据通路的使能作用,扩展性为0。
(5)SourceMonitor分析



由 SourceMonitor 报表可见,第一次作业代码总语句数为 162 条(Gate.java 16 条,Main.java 146 条),Main.java 注释比例达 41.8%,说明编码规范意识良好。程序最大圈复杂度为 7,平均圈复杂度为 4.50,均处于健康区间(≤10 为良好),表明分支逻辑虽因五种门类型的分发处理而略显复杂,但整体仍清晰可控。最大嵌套深度为 6 层,平均深度仅 2.02 层,说明大部分代码平铺直叙,仅递归解析引脚信号时出现较深嵌套。整体而言,第一次作业代码质量良好,但 Main 类独揽 146 条语句的集中式设计,已隐约暗示后续迭代中该类将面临膨胀风险。
2.2第二次作业
(1)类图设计

(2)设计分析
第二次作业是架构的第一次大重构。我将 Gate 升级为 Component,并引入了独立的 Pin 类,通过 CONTROL / INPUT / OUTPUT 枚举显式区分引脚角色。每个元件在 buildPins() 中严格按照题目要求的顺序(控制→输入→输出)构建引脚列表。信号传播从递归改为迭代宽度优先驱动,外层使用 while (changed) 循环反复扫描所有元件,直到某轮迭代没有新的输出值产生为止。
针对译码器、数据选择器和分配器的多输出特性,我分别采用了 decoderZeroIdx(记录输出0的引脚编号)和 demuxOutStr(拼接字符串代表所有输出状态)来适配题目特殊的输出格式。
(3)设计亮点
引脚角色分离:Pin.type 的引入成功解耦了控制逻辑与数据逻辑,使得三态门的使能判断(ctrl == 1 时才导通)能够独立编码,不再污染普通门的计算逻辑。
适配复杂输出格式:为译码器和分配器单独开辟字段存储格式化结果,避免了输出阶段再做复杂的二次解析,这是一种空间换时间的实用策略。
迭代收敛机制:while(changed) 循环配合 computed 标志位,有效避免了因输入依赖顺序导致的空指针异常,鲁棒性优于第一次的递归方案。
(4)设计不足
Component 类依然不符OCP:虽然分离了引脚,但 Component 内部依然塞入了 9 种元件的计算逻辑,计算分支的圈复杂度飙升至 12+,阅读和调试极其困难。
元件创建与逻辑耦合:addComponentFromPin() 方法既做正则解析,又做 new Component() 实例化,且 param 字段在“与门”中代表输入引脚数,在“选择器”中代表控制引脚数,语义模糊,极易误用。
缺乏统一的输出接口:普通门输出 name-0:val,译码器输出 name:idx,分配器输出 name:str。输出逻辑在 main 方法中通过 if-else 类型判断分别处理,没有利用多态,导致主流程代码臃肿。
(5)SourceMonitor分析


第二次作业代码规模急剧膨胀:总语句数从第一次的 162 条跃升至 363 条,增幅达 124%。最大圈复杂度从 7 飙升至 69,远超一般推荐的 10 阈值警戒线,这意味着 Component 类中承担了过重的计算职责——三态门、译码器、数据选择器、数据分配器四种新增元件的逻辑全部挤在同一方法中,导致分支路径呈指数级增长。平均深度从 2.02 升至 4.05,翻倍增长,表明代码整体嵌套结构显著加深,可读性与可维护性均受到挑战。注释比例从 41.8% 急剧下降至 0.2%,几乎为零,说明新增代码完全缺失注释,这其实严重影响后续迭代时对逻辑的理解和修改。
2.3第三次作业
(1)类图设计

(2)设计分析
第三次作业引入了组合模式(Composite Pattern) 的雏形。我新增了 SubCircuit 类作为复合节点,内部持有独立的 gates 映射表、inputValues / outputValues 信号映射,以及子电路内部的连接列表 connections。主电路中的 C1-A 引脚会被解析为跨层级的信号传递:主循环先驱动子电路的输入端口,再进入子电路内部完成闭环计算,最后将子电路的输出端口值冒泡回主电路的信号表 signals。
同时,我构建了 checkConn() 和 checkConflict() 两个检测方法,用于覆盖题目要求的 5 种异常情况(多输出、无输入、无输出、顺序错误、引脚冲突),并按顺序返回优先级最高的错误信息。
(3)设计亮点
命名空间隔离:子电路内部元件编号可与主电路重复(如都有 N1),输出时强制添加 子电路编号- 前缀(如 C1-N1-0),完美解决了编号冲突问题,体现了良好的封装性。
内外双层迭代驱动:设计了“主循环驱动子电路输入 → 子电路内部闭环收敛 → 回写主信号表”的三段式传播机制,理论上支持子电路的嵌套和复杂组合逻辑。
异常检测模块化初探:将引脚角色判断(是输入还是输出)集中封装在 checkConn 中,并利用 outs 和 ins 两个列表的计数进行规则校验,逻辑相对集中。
(4)设计不足
异常优先级顺序逻辑颠倒(致命伤):题目要求的优先级顺序为“多输出 > 无输入 > 无输出 > 顺序错误 > 冲突”,但我的代码中 ins.isEmpty() 对应了 "none output",outs.isEmpty() 对应了 "none input",将第二和第三优先级彻底写反。这导致样例8等同时缺失输入和输出的复杂异常场景直接输出错误,是本次失分的主要原因。
Main 类沦为“上帝对象”:Main 类此时承担了文件解析、子电路构建、异常检测(5种优先级)、信号传播、结果排序共计五类职责,代码膨胀至约500行。异常检测逻辑与核心业务逻辑高度耦合,导致优先级错误在繁杂的代码中被掩盖,难以调试。
组合模式未完全抽象:虽然有了 SubCircuit 和 Gate,但两者没有实现统一的 Component 接口。主电路在处理“门”和“子电路”时依然靠 if (isSubPin) 手动分流,未享受到多态带来的代码简化红利,也未能利用递归组合处理更深层的嵌套。
(5)SourceMonitor分析




第三次作业重构后,代码结构发生了根本性变化:单文件 Main 类被拆分为 9 个独立类,按职责划分为 model、parser、validator、engine、printer 五个包。Main 类从 500+ 行缩减至 22 行,真正成为纯入口类。最大圈复杂度从重构前的 69(高危)降至 14,降幅达 79.7%,其中 ConnectionValidator 的圈复杂度仅为 2,异常检测逻辑清晰可控,从根本上杜绝了优先级判断错误的风险。但 CircuitParser.java(14)和 Gate.java(12)的圈复杂度仍然偏高,解析逻辑和门计算逻辑仍有拆分空间。总语句数约 480 条,注释比例从 0.2% 回升至 9.1%,平均深度从 4.05 优化至约 2.3,代码可读性明显改善。
三、踩坑心得
3.1坑点一:引脚解析的边界情况(第一次作业)
问题描述:
在解析引脚信息时,最初我用简单的split("-")分割字符串,但对于A(2)1-1这样的格式,split会返回["A(2)1", "1"],工作正常。但对于A-1这样的外部输入,split也会返回["A", "1"],导致将外部输入误判为元件引脚。
解决方案:
增加判断逻辑——先检查字符串是否包含元件名的特征(如(、)或字母+数字组合),再决定如何解析。
3.2坑点二:三态门控制端和输入端混淆(第二次作业)
问题描述:三态门的引脚顺序为:0号控制端、1号输入端、2号输出端。最初我将0号误认为输入端,1号误认为控制端,导致三态门逻辑完全错误。
解决方案:严格按照题目要求建立引脚映射
3.3坑点三:译码器输出格式(第二次作业)
问题描述:译码器要求输出输出为0的引脚编号,而非输出电平值。最初我按照普通门的格式输出M(2)1-6:0,但题目要求的是M(2)1:0。
解决方案:为译码器单独处理输出格式,记录decoderZeroIdx字段,在输出时打印元件名加冒号加编号。
3.4坑点四:数据分配器的输出格式(第二次作业)
问题描述:数据分配器要求按引脚编号从小到大的顺序输出所有输出引脚的信号,无效状态引脚输出"-"。例如F(3)1:-1------表示8个输出引脚中,只有W1输出1,其余为无效状态。
解决方案:构建一个长度为输出引脚数的字符数组,初始填充'-',然后将有效输出引脚置为'0'或'1',最后一次性输出整个字符串。
3.5坑点五:子电路内部元件与主电路元件编号冲突(第三次作业)
问题描述:子电路内部的元件编号与主电路中的编号可能相同,例如主电路中有一个N1,子电路中也有一个N1。如果使用全局gateMap存储,会导致冲突。
解决方案:子电路使用独立的gates映射表,输出时加上子电路编号前缀(如C1-N1-0),实现命名空间隔离。
四、改进建议
4.1采用真正的组合模式重构架构
目前的代码虽然有SubCircuit类,但没有实现统一的抽象接口,主电路和子电路仍然是分离处理的。建议按照题目提示,采用标准的组合模式重构
4.2将异常检测独立为专门的模块
目前异常检测逻辑散落在checkConn()和checkConflict()中,与核心业务逻辑耦合紧密。应将异常检测独立为专门的模块
4.3使用工厂模式管理元件创建
目前元件的创建分散在parseGate()和getOrCreateGate()中,新增元件类型时需要修改多处代码。应引入工厂模式
4.4增加单元测试覆盖
本次作业的调试主要依靠手动构造测试用例和对照题目样例。建议建立系统的单元测试框架:
为每种门电路编写独立的测试用例
为每种异常情况编写测试用例
使用自动化对拍工具对比输出结果
五、总结
5.1学到了什么
(1)迭代开发的重要性
三次作业从基础门电路到复杂组合逻辑,再到子电路与异常处理,是一次完整的增量式迭代开发训练。每次作业都在前一次的基础上增加新功能,而非推翻重来。这让我深刻体会到:好的架构设计应当具备良好的可扩展性,能够在不破坏现有功能的前提下容纳新需求。
(2)设计模式的实际应用
第三次作业中组合模式的应用让我理解了如何用设计模式解决实际问题。将子电路视为复合元件、与基础门电路统一处理,这种抽象层次的设计思维是面向对象编程的核心。
(3)异常处理的优先级设计
五种异常情况按优先级输出的要求,让我认识到异常处理不只是“能不能检测到”,更是“检测到之后如何处理” 。优先级设计体现了工程实践中“先处理最严重的问题”这一原则。
(4)代码质量与可维护性
三次作业代码量的增长(200→350→500行)让我深刻体会到:功能增加的同时,代码复杂度也在上升。如果不在设计层面做出合理的抽象和分离,代码很快就会变得难以维护。
5.2哪些地方需要进一步学习
(1)设计模式的深入学习
虽然第三次作业使用了组合模式,但对于工厂模式、策略模式、观察者模式等其他设计模式的应用还不够熟练。这些模式在更复杂的软件系统中都有广泛的应用场景。
(2)图论算法
电路本质上是一个有向图,信号传播是图的遍历问题。目前我的实现采用简单的迭代扫描,对于复杂电路效率较低。需要深入学习拓扑排序、广度优先搜索等图论算法,并应用到实际问题中。
(3)单元测试与自动化测试
本次作业的测试主要依赖手动构造用例和对照样例输出,效率低且覆盖不全。需要学习JUnit等单元测试框架,建立系统的测试体系。
(4)代码重构能力
第三次作业因为前两次的代码结构不够灵活,我选择了部分重构。但重构的时机、方法和风险控制都是需要进一步学习的技能。
5.3结语
从第一次作业的100分到第二次的100分,再到第三次的81分,这组数据清晰地反映了迭代开发中架构设计的重要性。前两次作业虽然满分,但代码的扩展性不足,导致第三次作业需要打补丁式地添加新功能,最终在异常处理的细节上出现了失误。
如果重新来过,我会在第一次作业时就采用组合模式设计架构,将门电路和子电路统一抽象;在第二次作业时就引入工厂模式管理元件创建;在第三次作业时将异常检测独立为专门的模块。这样的设计虽然前期投入更多,但后期的维护和扩展会轻松许多。
这三次作业不仅让我掌握了数字电路模拟的技术细节,更让我理解了软件工程中“设计先行”的重要性。写好代码不只是实现功能,更是为未来的变化留出空间。
浙公网安备 33010602011771号