南昌航空大学-25201411-纪俊杰-第二次博客作业
一、前言
本博客对 Java OOP 课程作业集4、5、6 进行综合总结。三次作业以数字电路模拟器为主线,从基础逻辑门仿真出发,逐步扩展到多类型复合器件,再引入子电路定义与复用机制,难度呈阶梯式递增。
三次作业概览

知识点分布: 作业集4 侧重算法(BFS)与字符串处理;作业集5 侧重面向对象设计(抽象类、多态);作业集6 侧重工程健壮性(错误检测优先级、层次化电路)。三次作业合计涉及 9 种器件类型、5 类错误检测,累计代码约 600 行,题量适中,难度递进合理。
二、设计与分析
2.1 架构演化路径

每一次迭代都是对上一版本的扩展而非推倒重来(理想情况下),这正是面向对象设计的核心价值所在。
2.2 作业集4:过程式驱动的 BFS 仿真
作业集4 用一个静态内部类 Gate 承载所有逻辑门,通过正则匹配门名自动识别门类型和输入数:
A(3)1 → 3输入AND门,编号1
N2 → NOT门,编号2
X1 → XOR门,编号1
信号传播采用 BFS 队列:从外部输入出发,将值逐级推送到下游门的输入管脚;当一个门的所有输入管脚就绪时,立即计算并将输出值入队继续传播。

优点: 逻辑直观,代码量少,适合纯逻辑门场景。
缺点: 门类型以整型常量区分,扩展新器件需修改 evaluate() 的 switch,违反开闭原则。
输出排序: 按 AND → OR → NOT → XOR → XNOR 类型顺序,同类按编号升序排列。
类图:

报告内容:

2.3 作业集5:继承体系 + 迭代传播
作业集5 引入抽象类 CircuitComponent,定义三个核心抽象方法:

各器件类型独立成类,关键设计如下:

传播逻辑改为迭代式推进,比BFS更能处理多输出组件:

类图:

报告内容:

2.4 作业集6:子电路复用 + 错误检测
子电路格式:

展开策略: expandSubCircuit(instId, def) 为子电路内部每个门添加"实例ID-"前缀,生成全局唯一门名(如 C1-N1),并建立端口映射表,将 C1-A、C1-Y 等外部端口引用替换为实际门管脚。
5类错误检测(按优先级从高到低):

优先级判断要特别注意:优先级1~4 在单条连线遍历时判断,优先级5 需要收集完所有连线的 inputPinSources 后才能判断,不能混在同一循环里。
类图:

报告内容:

三、采坑心得
坑1:作业集4 — 外部信号与门输出管脚标识符混淆
现象: 部分外部信号无法传播到下游门。
原因: 门输出用 gateName + "-0" 作为 key,而外部信号名没有 "-",在 BFS 取值时没有区分两种来源。
修复: 在 simulate() 里对 sourcePin 是否含 "-" 做分支判断,外部信号直接从 externalInputs 取值,门输出从已计算结果取值。
坑2:作业集4 — 同一管脚被多条路径到达时值被覆盖
现象: 多扇入信号的门输出偶尔不稳定。
原因: BFS 同一管脚可能被多条路径推入,后到的值直接覆盖先到的值。
修复: 加入 containsKey 检查,保证"先到先得",避免重复写入:

坑3:作业集5 — Decoder 地址位累加方向错误
现象: 2-to-4 译码器地址 10 被当成 01,输出全错。
原因: 题目规定地址从 pin3 开始,pin3 是最低有效位(LSB)。初版代码从 i=0 到 bits-1 正向累加,实际应从 bits-1 到 0 倒序:

坑4:作业集5 — -1 语义歧义:高阻 vs 未就绪
现象: 三态门输出高阻时,下游门误以为"输入未就绪"而跳过计算。
原因: -1 同时表示"高阻/未知"和"计算未完成",两者混用导致传播逻辑混乱。
修复: 规定三态门输出 -1 时不写入 pinVals(不触发下游传播),下游门若检测到该输入是"已连线但无值",则视为输入缺失,输出标记为 -。
坑5:作业集6 — 子电路内部连线未加入 expandedConns
现象: 子电路内部门之间信号无法传播,内部门输出均为 -。
原因: expandSubCircuit() 中构建了门对象和 portMap,但忘记把内部连线字符串添加到 expandedConns 列表,导致传播阶段根本看不到这些连线。
修复: 拆分为两步:先遍历创建门对象,再遍历登记内部连线(带前缀的展开版本)到 expandedConns。
坑6:作业集6 — 错误检测优先级逻辑的顺序Bug
现象:"多源驱动"错误有时被"输入冲突"错误覆盖,输出错误类型。
原因: 优先级5(输入冲突)需要扫描全部连线后才能判断,初版把它混在单条连线循环里,导致有时先判断了优先级5,压过了优先级1。
正确做法: 先完成优先级1~4 的单条连线检查,同时收集 inputPinSources;全部连线扫描完毕后,再统一检查 inputPinSources 是否有冲突。
测试用例示例
作业集4 测试:

预期输出:

分析:A=1 → N1 输出 NOT(1)=0;B=0 → A(2)1 输入1=0,AND 门直接输出 0。
作业集5 Decoder 测试:

预期输出(地址 01=1,输出 Y1 有效):M(2)1:1
四、改进建议
4.1 用枚举代替 -1 魔数
当前三个版本均以整型 -1 混合表示"未就绪"和"高阻输出",建议引入枚举:

分别对应 0、1、高阻、未计算。让类型系统约束状态转换,彻底消除整型魔数带来的语义歧义。
4.2 工厂方法集中化,消除重复解析逻辑
作业集5、6 中门名解析散落在多处,存在大量重复的 startsWith / switch 判断。建议:

新增器件类型只需在 registry 里注册一行,无需修改现有代码。
4.3 连线数据结构改用 Pin 对象
目前连线用字符串 "compName-pinNum" 作为 key,每次使用都要 split/lastIndexOf 解析,效率低且容易出错。建议:

访问时直接用字段,语义清晰,性能更好。
4.4 子电路支持递归嵌套
当前子电路只能包含基本门,不能引用其他子电路。若在 expandSubCircuit() 里加一层"若内部组件是子电路则递归展开"的逻辑,即可支持层次化设计,更接近真实 EDA 工具的使用方式。
4.5 为错误检测补充单元测试
作业集6 的5种错误场景目前全靠手工构造输入验证。建议用 JUnit 5 参数化测试为 detectErrors() 方法编写覆盖用例:

回归测试保护后续修改时不破坏已有检测逻辑。
五、总结
这阶段学到的
抽象类 + 多态让器件扩展只需增加新类,无需改旧代码,开闭原则的价值在作业集4→5的重构中得到了真实验证。
正则表达式批量解析格式化门名,远比手工 split 更健壮,一行 Pattern 解决一类命名规律。
BFS 与迭代传播各有适用场景:BFS 适合纯逻辑门的单输出传播,迭代推进更适合多输出、存在高阻状态的复杂器件。
pinValues 键值对比数组更适合稀疏管脚场景(如译码器输出 16 个管脚但只有 1 个有效)。
错误检测优先级需要明确分层,不能把不同层次的检查混在同一遍历里,先收集再判断是稳健的做法。
子电路展平的核心思路:为内部组件加命名空间前缀,复用原有传播逻辑,不需要单独维护一套子电路仿真引擎。
还需进一步研究
如何检测组合逻辑中的环路(避免无限传播循环)
时序电路(触发器、锁存器)的仿真扩展方向
使用 record 或 sealed class 替代继承层次的现代 Java 写法
如何可视化电路连接关系(图形化调试辅助)
JUnit 5 参数化测试的工程实践
核心感悟
设计比编码更重要。
作业集4 的过程式代码功能上完全正确,但当作业集5 需要引入三态门、译码器等复杂组件时,不得不几乎从头重构。而作业集5 良好的继承体系让作业集6 的主要精力可以专注在子电路和错误检测上,而不是纠缠门的计算逻辑。
调试过程中"拿数据说话"同样关键——每次输出不符预期,先手工模拟一遍信号传播,把每个门的预期输入/输出列出来,再对比程序实际运行情况,效率远高于盲目加 System.out.println。这个习惯值得长期坚持。
浙公网安备 33010602011771号