数字电路模拟程序三次迭代作业总结
一、前言
本阶段三次作业围绕“NCHUD-数字电路模拟程序”这一主题展开迭代开发,对应的题目分别是数字电路模拟程序-1、数字电路模拟程序-2和数字电路模拟程序-4。虽然每次作业集都只有一道主编程题,但每一道题的代码量和设计难度都相当可观。
知识点覆盖层面:
- 第一次作业:基础Java容器(HashMap、ArrayList)、字符串解析、迭代式信号传播、基础面向对象封装
- 第二次作业:多类型器件扩展、控制引脚概念、多输出处理、差异化输出格式
- 第三次作业:组合设计模式、子电路嵌套、五级异常输入检测、分层架构设计
| 维度 | 第一次 | 第二次 | 第三次 |
|---|---|---|---|
| 源码行数 | 约280行 | 约442行 | 约611行 |
| 主要类数量 | 3个 | 4个 | 约12个 |
| 元件种类 | 5种 | 9种 | 9种(+子电路) |
| 难度评级 | 中等 | 较难 | 困难 |
从数据可以看出,三次作业的代码规模从280行增长到611行,类数量从3个扩展到约12个,复杂度呈指数级上升。第一次作业侧重“算逻辑”,第二次侧重“建电路”,第三次则上升为“管理电路系统”。
二、设计与分析
2.1 第一次作业:基础逻辑门模拟
题目要求
第一次作业要求模拟五种基础逻辑门:与门(A)、或门(O)、非门(N)、异或门(X)、同或门(Y)。与门和或门支持多输入(以A(8)1表示8输入与门),非门、异或门、同或门为固定输入数量。程序需要解析INPUT行和连接信息行,通过事件队列传播信号,最终按指定顺序输出所有有有效输出的元件。
源码架构分析
第一次作业采用了过程式为主、类封装为辅的设计思路。核心类结构如下:
Main(主控类)
├── Gate(抽象基类)
│ ├── AndGate(与门)
│ ├── OrGate(或门)
│ ├── NotGate(非门)
│ ├── XorGate(异或门)
│ └── XnorGate(同或门)
├── Target(目标引脚封装)
└── Event(事件封装)
关键设计特点:
-
Gate抽象基类:定义了name、idNum、typeChar、inputCount、inputVals[]、outputVal等核心属性,以及updateInput()、computeOutput()、allInputsReady()等核心方法。每个具体门类只需实现computeOutput()抽象方法,体现了模板方法模式的思想。
-
事件队列驱动:使用Queue
作为信号传播引擎。初始时将外部输入信号入队,每次从队列取出事件,查找该信号驱动的所有目标引脚,调用对应Gate的updateInput()方法。若门电路输出发生变化,则生成新事件入队。这种设计天然支持多级电路的级联传播。 -
连接关系管理:使用Map<String, List
> outToTargets维护从输出引脚到目标引脚列表的映射,Map<String, Gate> gates维护元件名到元件实例的映射。
类图示意:

设计心得:
第一次作业的设计虽然简单,但已经建立了良好的扩展基础。Gate的继承体系使得新增门类型只需添加子类,符合开闭原则。事件队列驱动的方式也有效地解决了信号传播的时序问题。不过,所有逻辑门类型判断和创建都集中在createGate()方法中,使用正则表达式匹配元件名,这种方式在元件种类较少时尚可接受,但在后续迭代中会成为瓶颈。
2.2 第二次作业:复杂组合电路元件扩展
题目要求
第二次作业在第一次的基础上新增了四种元件:三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F)。最大的变化是引入了控制引脚的概念,引脚被分为控制、输入、输出三类。不同元件的引脚编号规则完全不同:
- 三态门:0号引脚为控制端、1号为输入端、2号为输出端
- 译码器:控制引脚在前、输入引脚居中、输出引脚在后
- 数据选择器:控制引脚在前、数据输入引脚居中、输出引脚最后
- 数据分配器:控制引脚在前、输入引脚居中、输出引脚最后
输出格式也多样化:普通逻辑门输出“引脚:电平”,译码器输出为0的引脚编号,数据分配器输出含高阻标记“-”的字符串。
源码架构分析
第二次作业在第一次的基础上做了重要调整:
-
Gate从抽象类变为普通类:所有门类型不再继承Gate,而是各自独立实现。这实际上是一种倒退——放弃了继承带来的多态优势。
-
输出方式多样化:每个Gate子类都有自己的formatOutput()方法,处理不同的输出格式需求。例如DecoderGate输出M(3)1:3格式,DemuxGate输出F(2)1:--0-格式。
-
多输出引脚支持:Gate的currentOutputs从单个Integer变为Map<Integer, Integer>,支持一个元件有多个输出引脚(如译码器有8个输出引脚)。
-
引脚编号灵活性:不同元件的引脚编号规则差异通过各子类的构造函数和computeOutputs()方法独立处理。
新增元件逻辑要点:
-
三态门:控制引脚为1时输出等于输入,控制引脚为0时输出为null(高阻态)
-
译码器:需检查三个控制引脚S1=1、S2=0、S3=0,满足条件后根据输入编码决定哪个输出引脚为0
-
数据选择器:根据控制引脚的编码选择对应的数据输入
-
数据分配器:根据控制引脚的编码决定将输入信号送往哪个输出引脚
设计缺陷分析:
第二次作业虽然新增了元件种类,但在架构设计上存在明显问题。所有逻辑门不再共享统一的抽象父类行为,每种元件独立实现自己的computeOutputs()和formatOutput(),导致代码中存在大量重复逻辑。例如,AndGate和OrGate的computeOutputs()方法中都有检查所有输入是否就绪的循环,formatOutput()方法也高度相似。这种“平铺式”设计在元件种类较少时尚可工作,但已经暴露出扩展性不足的问题。
2.3 第三次作业:子电路与异常检测
题目要求
第三次作业新增两大核心功能:
-
子电路(SubCircuit) :可以将一部分电路封装为子电路,在主电路中被引用。子电路有自己的输入、输出和连接信息,以C+编号:开始,以endc结束。子电路中的元件编号与主电路可以相同,输出时带上子电路编号前缀。
-
异常输入检测:需要检测五种异常并按优先级输出:
-
优先级1:连接中包含多个输入
-
优先级2:连接中没有输入
-
优先级3:连接中没有输出
-
优先级4:输入输出顺序错误
-
优先级5:输入引脚信号冲突
题目强制要求使用组合设计模式实现子电路与基础门的统一管理。
源码架构分析
第三次作业进行了架构层面的重构,代码结构发生了根本性变化:
DigitalCircuitSystem(系统入口)
├── Parser(解析器)
│ ├── 解析子电路定义(C1: ... endc)
│ ├── 解析主电路(INPUT: ... end)
│ └── 存储 Circuit / SubCircuit
├── Validator(验证器)
│ ├── validateConnection():五级异常检测
│ ├── identifyRole():引脚角色识别
│ └── checkSignalConflicts():信号冲突检测
├── Flattener(展开器)——核心
│ ├── buildFlatCircuit():递归展开子电路
│ ├── ExpansionTask:展开任务队列
│ └── collectGates():收集所有门实例
├── Simulator(模拟器)
│ ├── 信号传播循环
│ └── GateLogic.compute():逻辑计算
└── 数据类
├── Circuit / SubCircuit
├── FlatCircuit / GateInstance
└── PinRole(枚举)
组合模式的应用:
第三次作业虽然没有显式地使用Component抽象类,但通过Flattener将子电路“展开”为扁平的门电路列表,实现了组合模式的核心思想——将子电路视为一个复合元件,内部包含多个基础元件和连接关系。GateInstance作为统一的元件表示,FlatCircuit作为展开后的扁平电路。
展开算法的核心逻辑:
-
首先解析所有子电路定义,存储为SubCircuit对象
-
解析主电路,识别其中的子电路引用(如C2-A)
-
使用队列(Deque
)进行广度优先展开 -
对每个子电路引用,将其内部连接复制并添加前缀(如C2-)
-
如果子电路内部又引用了其他子电路,继续入队展开
-
最终得到所有基础门实例的列表和扁平化的连接关系
异常检测的优先级处理:
Validator类中的validateConnection()方法按照题目规定的优先级顺序依次检查:
// 优先级1:包含多个输入
if (inputCount > 1) return "ERROR: ... include more than one input";
// 优先级2:没有输入
if (inputCount == 0) return "ERROR: ... include none input";
// 优先级3:没有输出
if (outputCount == 0) return "ERROR: ... include none output";
// 优先级4:输入输出顺序错误
if (roles.get(0) == SYSTEM_OUTPUT && inputCount > 0)
return "ERROR: ... input and output sequence error";
引脚角色识别(identifyRole())是异常检测中最复杂的部分。一个引脚在不同上下文中可能扮演不同角色:
普通门引脚:根据引脚号判断(0为输出,非0为输入)
子电路端口:根据该端口在子电路定义中是INPUT还是OUT决定
主电路顶层引脚:根据上下文判断
设计亮点:
-
职责分离:Parser、Validator、Flattener、Simulator各司其职,符合单一职责原则
-
展开-模拟两阶段:先展开所有子电路得到扁平电路,再统一模拟,避免了递归模拟的复杂性
-
异常优先级硬编码:虽然不够优雅,但清晰表达了题目要求
三、采坑心得
3.1 第一次作业的坑
坑1:Scanner的换行符陷阱
在解析输入时,如果混用nextInt()和nextLine(),会遇到换行符残留的问题。例如:
java
int n = sc.nextInt();
String s = sc.nextLine(); // 这里会读到空字符串
修复方式是在nextInt()后额外调用一次nextLine()消耗换行符。
坑2:字符串比较使用==
Java中字符串比较应使用equals()而非==。在门类型判断中如果写成if (type == "A"),在某些情况下会得到错误结果。
坑3:排序方向错误
题目要求按“与门、或门、非门、异或门、同或门”的顺序输出,同类按编号从小到大。如果比较器的返回值写反,会导致输出顺序与预期相反。
坑4:信号传播的终止条件
最初实现信号传播时使用while (true)循环,依靠队列为空作为终止条件。但在某些电路拓扑中,如果存在循环依赖(虽然题目未明确禁止),可能导致无限循环。需要增加迭代次数上限或变更检测机制。
3.2 第二次作业的坑
坑1:引脚编号规则混淆
第二次作业中不同元件的引脚编号规则完全不同。三态门的0号是控制引脚、1号是输入、2号是输出;译码器的0-2号是控制、3-5号是输入、6-13号是输出。在实现时,很容易把引脚号搞混。例如,把三态门的控制引脚当成输入引脚来处理,导致控制逻辑完全失效。
坑2:数据分配器的输出顺序
数据分配器要求“按引脚编号从小到大的顺序输出所有输出引脚的信号”,无效状态输出“-”。最初实现时把选择顺序理解反了(如AB=00时选择了W3而非W0),导致隐藏测试点全部错误。
坑3:无效状态不能当作0处理
三态门控制引脚为0时输出为高阻态(无效),数据分配器未被选中的输出引脚也为无效状态。最初实现时把无效状态当作0处理,这改变了电路的逻辑含义。例如,一个三态门输出为高阻态时,后续门电路应该认为该输入未就绪而非收到了0信号。
坑4:译码器输出格式混淆
译码器的输出格式是M(3)1:3(输出为0的引脚编号),而不是普通的M(3)1-0:1格式。最初按照普通门的格式输出,导致格式错误。
坑5:控制信号条件判断错误
译码器正常工作的条件是S1=1, S2=0, S3=0。如果把这个条件写反(如S1=0, S2=1, S3=1),会导致译码器永远无法正常工作。
3.3 第三次作业的坑
坑1:子电路引脚前缀处理
子电路展开时,需要给所有内部引脚添加子电路编号前缀(如C2-)。但子电路内部的子电路引用需要添加双重前缀(如C2-C3-A)。在实现展开算法时,很容易出现前缀重复添加或遗漏的情况。
坑2:引脚角色识别的上下文依赖
identifyRole()方法需要根据上下文判断引脚角色。同一个引脚C1-A在子电路定义中是输入端口,在主电路的连接中却可能是输出端口。如果角色识别错误,会导致异常检测误报。
坑3:异常优先级判断顺序
题目规定了五种异常的优先级顺序。如果判断顺序与优先级顺序不一致,会输出错误的异常信息。例如,[A(2)1-1 A(2)1-0]应该输出“顺序错误”,但如果先判断“包含多个输入”就会输出错误信息。
坑4:子电路展开的递归深度
在嵌套子电路场景中,展开算法需要处理任意深度的嵌套。如果使用递归实现,需要小心栈溢出;如果使用队列实现,需要确保不会因为循环引用而无限展开。
坑5:信号冲突检测的范围
信号冲突检测需要覆盖主电路和所有子电路。最初实现时只检查了主电路,忽略了子电路内部的冲突。修复方案是在展开后的扁平电路上进行统一检测。
四、改进建议
4.1 架构层面的改进
建议1:统一元件抽象接口
第二次作业放弃了继承体系,各Gate子类各自为政。建议恢复统一的抽象基类或接口,定义computeOutputs()、formatOutput()、getInputPins()等标准方法。这样新增元件时只需实现接口,无需修改已有代码。
建议2:采用真正的组合模式
第三次作业虽然使用了组合模式的思想(通过Flattener展开子电路),但没有显式实现Component抽象类。建议按照题目设计建议,定义Component接口,Gate和SubCircuit都实现该接口。SubCircuit内部维护List

建议3:异常检测模块化
第三次作业的异常检测逻辑集中在Validator类中,且优先级判断使用硬编码的if-else链。建议将每种异常检测封装为独立的Check类,通过责任链模式组织优先级顺序,提高可维护性和可扩展性。
4.2 代码质量改进
建议4:减少方法圈复杂度
解析和验证相关的方法圈复杂度较高。建议将大方法拆分为多个小方法,每个方法只做一件事。例如,createGate()方法中使用了5个正则表达式匹配,可以拆分为createAndGate()、createOrGate()等独立方法。
建议5:增加注释和文档
三次作业的注释行占比普遍偏低。建议增加类注释、方法注释和关键逻辑的行内注释,提高代码可读性。
建议6:使用枚举替代字符串常量
门类型使用字符串常量(如"A"、"O")在多个地方硬编码。建议使用枚举类型GateType,提高类型安全性,避免拼写错误。
4.3 功能层面的改进
建议7:支持更多异常检测类型
题目仅要求检测五种异常,实际电路中还可能存在更多异常情况,如元件未定义、引脚号超出范围、循环依赖等。可以逐步扩展异常检测的范围。
建议8:增加调试模式
在复杂电路模拟中,信号传播过程难以追踪。建议增加调试模式,输出每一步的信号传播日志,便于定位问题。
五、总结
5.1 学到了什么
1. 迭代开发的经验教训
三次作业是一个典型的迭代开发项目,从280行代码到611行代码,从3个类到12个类。我深刻体会到了架构设计的前置重要性。第一次和第二次作业侧重于快速实现功能,代码设计缺乏长远扩展性;第三次作业需要支持子电路和异常检测时,才发现原有架构难以支撑。这让我认识到,在迭代项目中,“先编码、后设计”是不可取的。
2. 组合模式的实际应用
第三次作业要求使用组合模式,这是第一次在实际项目中应用设计模式。通过将子电路视为复合元件,与基础门统一管理,实现了层次化电路的建模。这让我理解了设计模式不是纸上谈兵,而是解决实际问题的有效工具。
3. 复杂字符串解析能力
三次作业都需要解析自定义格式的输入,从简单的INPUT:A-0 B-1到复杂的子电路定义C2: INPUT:A B OUT:C,解析难度逐步提升。通过这三次作业,我对正则表达式、字符串分割、递归解析等技术有了更深入的掌握。
4. 信号传播算法的理解
事件队列驱动的信号传播方式,本质上是对数字电路时序行为的模拟。通过实现这个算法,我对组合逻辑电路的信号传播有了更直观的理解。
5.2 需要进一步学习的内容
1. 设计模式的深入学习
虽然第三次作业应用了组合模式,但对其他设计模式的理解还不够深入。需要进一步学习工厂模式、责任链模式、观察者模式等在类似场景中的应用。
2. 时序电路模拟
本系列作业仅涉及组合逻辑电路。后续迭代将增加D触发器、JK触发器时序电路元件,需要学习时序电路的模拟方法,包括时钟信号、状态保持、建立/保持时间等概念。
3. 异常处理的系统化设计
第三次作业的异常处理采用硬编码方式,虽然完成了功能,但代码可维护性较差。需要学习如何设计可扩展的异常处理框架。
4. UML建模与设计先行
通过这次作业,我认识到在设计阶段使用UML类图、时序图等工具的重要性。后续开发中应养成“先画图、后编码”的习惯。
5.3 结语
三次数字电路模拟程序作业,从基础逻辑门到复杂组合元件,再到子电路与异常检测,完整地模拟了一个软件项目的迭代开发过程。虽然过程中遇到了各种问题——架构设计缺陷、引脚编号混淆、异常优先级判断错误等——但正是这些问题让我对面向对象设计、设计模式应用和系统架构有了更深刻的理解。正如一位同学所说,这套作业“完整复刻了数字电路从基础元件到模块化电路的工程实现逻辑”。
未来的学习和开发中,我将更加注重架构设计的前置规划和设计模式的合理应用,避免“先编码、后设计”的开发误区。
浙公网安备 33010602011771号