作业集4-6总结博客:数字电路仿真系统的迭代

一、前言

经过作业集4、5、6三轮迭代开发,我在"数字电路仿真系统"这一题目上从最基本的逻辑门模拟,走到了包含三态门、译码器、多路选择器、多路分配器在内的复合器件仿真,再到最终引入子电路概念、实现连接关系合法性校验的完整仿真框架。回顾这段历程,既有对面向对象设计思想的深化理解,也有不少踩坑教训。

1.1 三次作业的知识覆盖范围

作业集4 聚焦基础逻辑门仿真,知识点包括:

(1)Java 抽象类(abstract class Gate)和多态的应用

(2)HashMap 管理门实体与引脚值的映射

(3)字符串解析:从名称字符串(如 A(3)1、N2)动态推断门类型、输入端口数和编号

(4)迭代传播:通过 do-while 循环反复遍历连接关系,直至所有可计算的门都输出结果 题目难度较低,只需处理 AND / OR / NOT / XOR / XNOR 五种门。

 

作业集5 在作业集4的基础上大幅扩展,新增知识点:

(1)三态门(TriState)的高阻抗状态建模,用常量 INVALID = -1 表示无效值

(2)译码器(Decoder / M 类型)的使能引脚逻辑:三个控制引脚需满足"1、0、0"才激活

(3)多路选择器(Mux / Z 类型)和多路分配器(Demux / F 类型)的控制线 + 数据线分离设计 (4)用 Component 抽象基类统一管理所有器件,使扩展更容易 (5)LinkedHashMap 保序特性,确保解析顺序不被打乱 难度中等,核心挑战在于多类型器件的引脚编号约定和无效值的传播处理。

 

作业集6 引入了子电路概念,是三次作业中最复杂的:

(1)子电路(C1、C2 等)的定义、封装与在主电路中的引用

(2)作用域隔离:子电路内的引脚需要用 C{id}: 前缀进行命名空间隔离

(3)连接关系的合法性校验:每条连接线必须恰好有一个信号源,且信号源必须排在首位

(4)冲突检测:同一输入引脚不能被两条不同的连接线驱动

(5)正则表达式解析门名称引脚

(6)主电路与子电路的统一求值图(evaluation graph) 难度较高,代码量是前两次的三倍以上,主要挑战来自作用域管理和错误检测逻辑。

二、设计与分析

2.1作业集4:抽象类继承层次 作业集4的核心设计是 Gate 抽象类 + 五个具体子类的继承体系。

代码规模:类结构示意: 复杂度分析:

Gate 类承担了公共字段(type、name、inputCount、inputs[]、output、id)和公共方法(allInputsSet()、getOutputPinName()),以及 abstract void compute() 强制子类实现自己的逻辑运算。 动态门创建(getOrCreateGate) 是该题最有意思的设计。由于输入只有连接关系字符串,门的名称直接携带了类型和参数信息(如 A(3)1 表示3输入AND门编号1),getOrCreateGate 通过解析名称字符串按需创建门对象,并缓存到 HashMap<String, Gate> gates 中,避免重复创建。

信号传播算法采用定点迭代:

1. 遍历所有连接,将已知引脚值传播到目标引脚

2. 检查所有门,若输入已全部就绪则计算输出并写入输出引脚

3. 重复直至一轮迭代无任何更新(updated == false) 这种算法能正确处理任意深度的串联电路,时间复杂度为 O(N × K),N 为迭代轮次,K 为连接数与门数之和。 输出排序按门类型优先级(A→O→N→X→Y)、相同类型按编号升序排列,通过自定义 Comparator 实现。

 

 

2.2 作业集5:复合器件与无效值传播 作业集5最重要的设计决策是引入 Component 抽象基类,将所有器件(包括基础逻辑门和新增的复合器件)统一管理。 代码规模: 类结构示意: 复杂度分析:

Component 基类提供了 setPinValue / getPinValue 的封装,以及 extractNumber 工具方法,各子类只需关注自身的逻辑。 三态门设计是新增器件中最简单的:控制引脚 -0、输入引脚 -1、输出引脚 -2。当控制引脚为1时,输入直通输出;为0时,输出设为 INVALID。关键在于必须用 INVALID(-1)而非 null 来表示高阻状态,因为 null 代表"尚未计算",语义完全不同。 译码器(Decoder)引脚约定: (1)引脚 -0、-1、-2:三个使能控制引脚(需满足1/0/0)

(2)引脚 -3 到 -(2+inputCount):地址输入引脚

(3)引脚 -(3+inputCount) 到末尾:2^inputCount 个输出引脚 被选中的输出引脚输出0,其余输出1(低电平有效),这一约定与74HC138等实际器件一致。

多路选择器(Mux)引脚约定: (1)引脚 -0 到 -(ctrlCount-1):控制线(低位在前)

(2)引脚 -ctrlCount 到 -(ctrlCount + dataCount - 1):数据输入

(3)引脚 -(ctrlCount + dataCount):唯一输出 控制线的二进制值决定选通哪路数据,设计中使用位运算 selected |= (1 << i) 计算选通索引,简洁高效。 INVALID 传播策略:INVALID 值不参与正向传播(computeAll 中明确跳过 outVal == INVALID 的情况),但会被设置到相关引脚以表示无效状态。这保证了:若器件使能不满足,其输出不会错误地传播给下游器件。 printResults 的输出策略差异明显:不同器件有不同的输出格式——普通门输出 name-0:val,译码器输出 name:index(输出哪路被选中),Demux 输出所有通道值的拼接字符串。这体现了多态在 printOutput 上的应用。

 

 

2.3 作业集6:子电路与错误检测 作业集6的设计挑战体现在三个层面:作用域隔离、连接合法性验证、统一求值图构建。 子电路数据模型: 每个子电路包含:输入端口名列表(inputNames)、输出端口名列表(outputNames)、内部门列表(gates)、连接关系列表(connections)。子电路在主电路中通过 C{id}-{portName} 格式引用其端口。 代码规模: 类结构示意 复杂度分析:

作用域隔离机制: 子电路内的所有引脚在求值图中以 C{id}:{pinName} 格式表示,主电路引脚不加前缀。scopedPin(subId, pin) 负责转换。这样,不同子电路中的同名引脚不会互相干扰。

连接合法性验证(validateOneConn): 每条连接必须满足:

1. 恰好有且仅有一个信号源(gate输出引脚或输入信号)

2. 至少有一个目标(gate输入引脚或子电路输入端口)

3. 信号源必须排在第一位,不能出现在后面 验证函数通过 classifier 函数(函数式接口 Function<String, String>)对每个引脚分类为 "src" 或 "dst",逻辑清晰,且通过 lambda 传参实现了主电路和子电路校验逻辑的代码复用。

冲突检测: 用 Map<String, String> destSources 记录每个目标引脚的驱动源,若同一目标引脚出现两次则报 input signal conflict 错误。 统一求值图构建(buildEvaluationGraph): 将子电路内部连接和主电路连接统一建成一张有向图(Map<String, List> graph),求值时在此图上迭代传播,子电路的接口引脚(C{id}:portName)作为连接桥梁。 输出排序复杂化: 相同类型和编号的门,来自子电路的排在来自主电路的前面,子电路按编号升序排。为此使用 Map<Gate, Integer> gateSubId 记录每个门所属子电路ID(主电路为 null)。

三、采坑心得

3.1 作业集4:引脚值覆盖导致计算错误 问题描述: 最初的实现中,信号传播时若目标引脚已存在值,会直接跳过而不更新。这在大多数情况下正确,但如果初始化时有误将某引脚设为了错误值,就永远无法纠正。

现象: 测试用例中,某些级联电路的中间节点值出现错误,但单独测试每个门又是对的。 根因: if (pins.containsKey(dest)) continue; —— 这行代码阻止了任何覆盖,而门的输出引脚 -0 在被门计算之前有可能已经通过连接关系被赋值(如果用户在连接线里错写了来源),导致门的 compute() 永远不执行。 解决方案: 明确区分"已由门计算赋值"和"由连接传播赋值",对于门输出引脚(以 -0 结尾)要特别注意不能在门未计算前提前锁定其值。

 

 

3.2 作业集4:门的 ID 解析依赖字符串约定 AND/OR门的名称格式为 A(inputCount)id,NOT/XOR/XNOR门的格式为 Xid,解析时需用括号位置确定分割点。最初的代码在括号缺失时会抛出 StringIndexOutOfBoundsException,导致整个程序崩溃。 教训: 所有解析代码都应包裹在 try-catch 中,并在解析失败时返回 null 而非让异常向上传播。

 

 

3.3 作业集5:INVALID 语义混淆 一开始用 null 表示"高阻/无效输出",但 null 同时也表示"尚未计算",导致逻辑无法区分这两种状态——特别是在判断"是否所有输入就绪"时,会把真正的高阻态信号误认为"还没收到值"而反复尝试计算。 解决方案: 引入常量 INVALID = -1,明确区分三种状态:null(尚未计算),0/1(有效值),-1(无效/高阻)。computeAll 中对 INVALID 的传播做特殊处理:INVALID 不向下游传播,但会记录在当前引脚上。

 

3.4 作业集5:Decoder 引脚编号偏移计算 译码器的引脚编号是固定控制引脚(3个)+ 可变地址引脚 + 可变输出引脚的顺序拼接,第一个输出引脚的索引是 CTRL_COUNT + inputCount(即 3 + n)。初始实现中把这个值直接写成了硬编码 4,在 inputCount=1 时正确,但 inputCount=2 时第一个输出引脚应是第 5 个,导致写错了位置。 教训: 任何依赖"偏移量"的引脚访问都必须用常量或计算表达式表示,绝对不能硬编码具体数字。

 

3.5 作业集6:子电路引脚作用域隔离遗漏 问题: 两个不同子电路(C1 和 C2)各自内部有一个名为 N1 的 NOT 门。初始实现没有加作用域前缀,导致两个子电路的 N1 共享同一个引脚值存储,C1 的输入会污染 C2 的计算。 现象: 在测试 "C1 输入为0,C2 输入为1" 的用例时,两个子电路的 NOT 门输出值相同,而不是相反。 解决方案: 所有引脚在内部存储时强制加 C{id}: 前缀,仅在输出时还原为显示格式(C{id}-{pinName})。

 

3.6 作业集6:连接顺序校验的边界情况 问题: 连接 [N1-0 A(2)1-1 N1-0] —— 信号源 N1-0 出现两次,其中一次在第0位(作为源),一次在第2位(重复出现,本身还是源)。最初的校验逻辑只计数 srcCount,发现 srcCount==2 就报错,但正确的报错信息要携带完整连接行,而非只说 pin 名称。 另一边界情况:[A(2)1-1 A(2)1-2 N1-0] —— 前两个引脚都是目标(dst),第三个才是源,应报"input and output sequence error"。初版代码对此判断条件写反,误判为正常。 教训: 对于校验逻辑,务必构造正向和反向测试用例各至少3个,覆盖"全都是src"、"全都是dst"、"src在中间"、"src重复"四种异常场景。

四、改进建议

 

4.1 作业集4:引入接口代替抽象类 当前设计用 abstract class Gate 实现多态,但 Java 的单继承限制使得未来无法让某个门同时继承 Gate 并混入其他行为(如 Describable、Serializable 的自定义扩展)。可以将公共行为拆分为接口: interface Computable { void compute(); } interface Pinnable { boolean allInputsSet(); String getOutputPinName(); } 抽象类保留公共字段,接口定义行为约定,这样后续扩展更灵活。

 

4.2 作业集5:模式改进 createComponent(String name) 中的 switch 语句随着器件种类增加会越来越长,违反开闭原则。可以用工厂注册表替代: 维护一个 Map<Character, Supplier> 注册表,在类加载时注册每种器件的构造器。新增器件只需在注册表中添加一行,无需修改 createComponent。

 

4.3 作业集5:INVALID 状态建议用枚举 当前用 int INVALID = -1 表示无效状态,但这要求所有使用引脚值的地方都记得做 == INVALID 判断,容易遗漏。可以引入 OptionalInt 或自定义包装类型: enum Signal { LOW, HIGH, INVALID } 这样编译器会强制在使用前处理 INVALID 分支,将运行时错误转化为编译期错误。

 

4.4 作业集6:错误处理应支持多错误收集 当前实现遇到第一个错误就立即打印并返回(fail-fast 模式)。在实际调试场景中,用户更希望一次性看到所有错误,而非每次修一个才能发现下一个。建议将 String error 改为 List errors,收集所有错误后统一输出。

五、总结

5.1 三次作业综合回顾 维度 作业集4 作业集5 作业集6 代码行数 ~200行 ~480行 ~680行 器件种类 5种逻辑门 9种器件 5种逻辑门+子电路 核心挑战 字符串解析、迭代传播 无效值语义、引脚约定 作用域隔离、合法性校验 OOP特性 继承、多态、抽象类 抽象类、工厂方法 组合、函数式接口 错误处理 无 INVALID 详细错误信息

 

 

5.2 学到了什么

(1)抽象类和接口的选择不是随意的,要考虑扩展方向:子类增加用抽象类,行为组合用接口

(2)命名空间隔离是多实例管理的基础,应在设计之初就规划好,不要等到出了 bug 再补

(3)引脚编号约定是"隐式接口",必须有注释明确每个引脚的语义,否则极易在多人协作或后期维护中出错

(4)测试用例的设计要覆盖边界:0输入/最大输入、使能全满足/部分满足/全不满足、串联一层/串联多层

(5)正则表达式虽然强大,但应尽量只用于初步解析,具体语义提取(如括号内的数字)用普通字符串方法更可读

(6)面对复杂题目不要一开始就写代码,先在纸上(或脑中)画出数据流:信号从哪里来、经过哪些门、输出到哪里,把这条链路理清楚,代码基本就出来了

 

5.3 需要进一步学习的方向

1. 图算法深入学习: 本题本质是信号在有向图上的传播,掌握拓扑排序、强连通分量将大幅提升解题能力

2. 正式的OOP设计模式: 工厂模式、策略模式、观察者模式在本题中都有潜在应用场景,系统学习会让代码更易扩展

3. 软件复杂度度量: 尝试用 SourceMonitor、SonarQube 等工具分析自己的代码,了解圈复杂度、认知复杂度等指标,养成量化评估代码质量的习惯 这三次作业让我从一个"能写出来就行"的状态,逐步走向"想清楚了再写,写完了还要想怎么改进"的状态。数字电路仿真这个题目设计得很好,它不仅是一道编程题,更是一个微缩的"软件系统"——有输入解析、有核心计算、有错误处理、有结果输出。希望在后续的作业中,能把这种系统性思维保持下去,持续精进。

posted @ 2026-06-21 12:13  戴鑫泰  阅读(6)  评论(0)    收藏  举报