一、前言
本博客围绕NCHU数字电路模拟程序三次迭代作业展开,记录从基础门电路模拟到完整组合逻辑电路仿真系统的演进过程。三次作业层层递进,逐步构建出一个接近真实数字电路工作场景的模拟程序。
1.1 知识点覆盖
| 作业 | 核心知识点 |
|---|---|
| 作业四 | 抽象类与继承、多态、队列传播、门电路逻辑(与或非异或同或)、引脚编号映射 |
| 作业五 | Pin对象传播模型、Component多态设计、迭代收敛算法、三态门/译码器/数据选择器/数据分配器、特殊输出格式化 |
| 作业六 | 子电路解析与递归模拟、依赖拓扑排序、优先级异常处理、工厂模式、排序比较器、防御性编程 |
1.2 题量与难度
| 指标 | V1 | V2 | V4 |
|---|---|---|---|
| 源码行数 | 434 | 681 | 888 |
| 类数量 | 13 | 16 | 14 |
| 平均方法数/类 | 3.08 | 3.44 | 5.43 |
| 平均语句数/方法 | 4.78 | 5.53 | 4.96 |
| 平均圈复杂度 | 2.23 | 2.87 | 2.55 |
| 最大圈复杂度 | 15 | 13 | 23 |
| 注释率 | 7.1% | 8.2% | 7.9% |
1.3 难度感受
V1作为入门阶段,重点在于理解门电路的抽象继承关系和队列传播机制,五个基本门类型逻辑清晰,整体结构直观。
V2难度明显提升,引入Pin对象传播模型后,需要同时管理元件的引脚列表、信号连接关系和25轮迭代收敛,类间协作从简单的继承层次变为Pin-Component网状交互。
V4复杂度最高,涉及子电路递归模拟、依赖拓扑排序和五种异常优先级处理,但通过将解析、模拟、输出三阶段分离,反而让主流程保持清晰。这也印证了合理的架构分层是应对复杂业务的关键手段。
二、设计与分析
2.1 作业四解读
以下是作业四通过 SourceMonitor 生成的代码质量分析报表:

作业四关键指标
- 类数:13 个(含 Gate 体系、CircuitBuilder、CircuitSimulator、GateFactory、PinParser 等),相较基础门电路版本大幅扩展,体现了从“单文件实现”到“模块化分层”的转变。
- 最大圈复杂度:15,出现在
GateFactory.createGate()方法中,由 9 种元件类型的 switch 分支和引脚解析逻辑导致。复杂度偏高,但属于工厂类特有的集中创建逻辑。 - 平均圈复杂度:2.23,整体表现良好,大部分门逻辑和模拟方法保持简洁。
- 最大嵌套深度:4,出现在
CircuitBuilder.build()和CircuitSimulator.simulate()中,由循环+条件+队列操作组合导致,控制在合理范围内。 - 注释率:7.1%,核心逻辑和复杂元件已添加必要注释,但整体仍有提升空间。
- 语句数:288,方法调用 61 次,反映出模拟逻辑的交互频度较高。
以下是作业四的类图:

类图核心关系
- Gate:作为所有门电路的抽象基类,持有编号、输入引脚数组和有效计数,定义输出计算、类型获取、引脚名获取等抽象契约,不参与具体逻辑实现。
- CircuitBuilder:持有输入行数组、外部输入映射、连接关系映射,是解析构建阶段的协调者,负责将原始输入行转化为门对象和连接关系。
- CircuitSimulator:持有外部输入映射、连接关系映射、全局门映射、有效门队列,是模拟执行阶段的驱动者,负责信号传播和输出计算。
- GateFactory:纯工厂类,只提供静态方法创建门对象并管理全局门映射,不持有实例状态,不依赖其他业务类。
- PinParser:纯工具类,只提供静态方法解析引脚字符串,不持有任何成员变量,不依赖其他业务类。
- GateComparator:纯比较器类,只实现排序逻辑,不持有任何成员变量。
作业四设计心得
优点:
- 职责划分遵循了单一职责原则。Gate 只管门逻辑,GateFactory 只管创建,CircuitBuilder 只管解析构建,CircuitSimulator 只管模拟传播,PinParser 只管字符串解析,GateComparator 只管排序比较。各类职责边界清晰,修改模拟算法时不影响解析逻辑,反之亦然。
- 模拟传播采用队列驱动 + 依赖拓扑设计,每个门只在其最后一个输入被设置后才进入队列,避免盲目轮询。
- 多态设计使新增门类型时无需修改模拟器核心逻辑,只需新增 Gate 子类和工厂分支。
缺点: - 注释率约 7.1%,核心逻辑虽有基础说明,但复杂方法(如
createGate)仍需更详细文档。 GateFactory.createGate()圈复杂度达 15,由 9 种元件的 switch 分支和引脚解析逻辑导致,后续若新增元件类型,该方法将持续膨胀,可考虑拆分为多工厂或注册表模式。
2.2 作业五解读
以下是作业五通过 SourceMonitor 生成的代码质量分析报表:

作业五关键指标
- 类数:16 个(含 Circuit、CircuitParser、ComponentFactory、Pin、Component 体系、Gate 子类、三态门、译码器、数据选择器、数据分配器、Main),相比 V1 的 13 个类,新增了 Pin 对象模型、Circuit 容器、以及 4 个新元件的独立类。
- 最大圈复杂度:13,出现在
ComponentFactory.createComponent()方法中,由 9 种元件的 switch 分支和括号内数字解析逻辑导致。 - 平均圈复杂度:2.87,较 V1 的 2.23 有所上升,主要源自 Pin 对象传播模型的引入(每个元件需要管理多个引脚对象)以及新增元件的特殊计算逻辑(译码器控制条件判断、数据选择器/分配器的编码选择)。
- 注释率:8.2%,较 V1 的 7.1% 小幅提升,核心类(如
Decoder、DataSelector、DataDistributor)已添加功能说明,但内部算法细节仍需补充。
指标特点分析:
V2 最大的架构变化是用 Pin 对象传播模型 替代了 V1 的 boolean 数组模型。每个 Pin 持有信号值和输出连接列表,通过 propagate() 方法将信号推送给下游引脚。主循环固定迭代 25 次,每次执行“输入引脚传播 → 元件计算输出 → 输出引脚传播”,使得信号能够在电路中逐级流动。这一设计使引脚成为独立的信号载体,但也带来了额外的对象管理开销。
以下是作业五的类图:

类图核心关系
- Component:作为所有元件的抽象基类,持有元件名称、输入引脚列表、输出引脚列表,定义引脚管理方法和抽象计算接口
calculateOutput(),是元件体系的顶层契约。 - Gate:继承 Component,作为五门元件的标记抽象类,不新增字段,仅用于类型标识。AndGate、OrGate、NotGate、XorGate、XnorGate 继承 Gate 并实现各自逻辑。
- ThreeStateGate:继承 Component,持有 0 号控制引脚、1 号数据引脚、2 号输出引脚。
calculateOutput()中判断控制引脚:1 则输出等于数据,0 则输出置 null(高阻)。 - Decoder:继承 Component,引脚按控制→输入→输出顺序排列。
calculateOutput()先检查三个控制引脚是否满足 S1=1 且 S2+S3=0,满足则根据输入编码将对应输出置 0 其余置 1,否则所有输出置 null。 - DataSelector:继承 Component,引脚按控制→数据输入→输出顺序排列。
calculateOutput()根据控制引脚组合值从多路数据输入中选择一路送到输出。 - DataDistributor:继承 Component,引脚按控制→数据输入→输出顺序排列。
calculateOutput()根据控制引脚组合值将唯一一路输入信号送到对应输出,其他输出置 null。 - Pin:信号载体类,持有信号值(Integer,可为 null)和输出连接列表,提供
setSignal()、connectTo()、propagate()方法,是信号传播的基本单元。 - Circuit:容器类,持有元件映射(
components)和外部输入引脚映射(inputPins),是整个电路的资源管理中心。 - CircuitParser:解析类,持有 Circuit 引用,负责将输入行解析为 Pin 对象、Component 对象和连接关系。
作业五设计心得
优点:
- Pin 对象传播模型:将信号封装为独立的 Pin 对象,每个引脚持有输出连接列表,通过
propagate()方法主动推送信号,形成了天然的信号流图。相比 boolean 数组模型,Pin 模型更接近真实电路的工作方式。 - 迭代收敛算法:主循环固定执行 25 轮“输入引脚传播 → 元件计算输出 → 输出引脚传播”,确保信号能够在任意长度的组合逻辑链中完整传播。25 次迭代足以覆盖绝大多数电路深度,且实现简单、无需拓扑排序。
- Component 多态设计:所有元件统一继承 Component,模拟器核心只依赖 Component 抽象方法,新增元件类型时只需新增子类并修改工厂分支,核心传播逻辑完全不受影响。
- 引脚编号分组设计:译码器、数据选择器、数据分配器的引脚按“控制→输入→输出”连续编号,与题目要求完全对应,使得引脚创建和信号读取逻辑直观一致。
- 特殊输出格式化:Decoder 输出有效引脚编号列表、DataDistributor 用
-占位符表示无效状态,体现了不同元件类型的输出特征差异。
缺点: - 工厂类圈复杂度偏高:
ComponentFactory.createComponent()圈复杂度达 13,由 9 种元件的 switch 分支和括号内数字解析逻辑导致,后续新增元件将持续膨胀。 - Decoder 计算逻辑集中:
Decoder.calculateOutput()圈复杂度达 11,将控制条件检查、编码计算、输出设置三者交织在同一方法中,可拆分为独立的私有方法提高可读性。 - Pin 反向引用:Pin 持有
component引用,形成了双向关联(Component → Pin,Pin → Component),增加了对象图的耦合度,析构时需注意引用清理。
2.3 作业六解读
以下是作业六通过 SourceMonitor 生成的代码质量分析报表:

作业六关键指标解读
- 类数:14 个(含 Gate 体系、SubCircuit、CircuitBuilder、CircuitSimulator、OutputFormatter 等),相比 V1 的 13 个类,新增了子电路容器和输出格式化类,同时合并了部分冗余结构,整体模块粒度更合理。
- 最大圈复杂度:23,出现在
OutputFormatter.printGates()方法中。原因是该方法同时负责子电路门与主电路门的收集、排序、去重和格式化输出,嵌套了多层循环和集合操作。若后续迭代需要优化,可将收集和输出逻辑拆分为两个独立方法,或引入策略模式按类型分别处理。 - 平均圈复杂度:2.55,较 V1 的 2.23 略有上升,主要源自异常处理分支和子电路依赖排序逻辑的引入,但单个方法复杂度仍控制在可接受范围。
- 最大嵌套深度:6,出现在
CircuitBuilder.parseMainConnection()中,由异常优先级判断、引脚类型识别、连接关系存储三者交织导致。深度 6 在可维护性阈值内,但接近警戒线。 - 注释率:7.9%,较 V1 的 7.1% 小幅提升,核心方法(如
parseMainConnection、processSubCircuits)已添加必要说明,但仍有提升空间。 - 语句数:524,方法调用 170 次,反映出模拟引擎与解析器之间的高频交互,以及子电路递归模拟带来的额外调用开销。
指标变化分析:
V4 在 V1 的基础上新增了子电路递归解析、五种优先级异常处理、依赖拓扑排序三大模块。虽然业务逻辑显著复杂化(需要同时管理主电路门和子电路门,处理子电路间的信号依赖链),但通过将解析(CircuitBuilder)、模拟(CircuitSimulator)、输出(OutputFormatter)三阶段分离,核心模拟引擎的圈复杂度(simulate() 仅为 2)保持极低,复杂性被隔离在工厂类和输出格式化类中。
OutputFormatter.printGates() 圈复杂度达到 23,是本次最高值,主要因为该方法承担了“子电路门收集 → 主电路门收集 → 统一排序 → 子电路前缀输出 → 主电路裸名输出”的完整流水线。CircuitBuilder.parseMainConnection() 嵌套深度达到 6,由异常优先级判断(5 种)、引脚类型识别、连接关系存储三者交织导致,深度在可维护性阈值内,但已接近警戒线。Main.main() 语句数保持线性串联结构,未出现过度膨胀,验证了分层设计的有效性。
以下是作业六的类图:

类图核心关系
- Gate:作为所有门电路的抽象基类,持有编号、输入引脚数组和有效计数,定义输出计算、类型获取、引脚名获取等抽象契约,不参与具体逻辑实现。
- SubCircuit:作为子电路的容器类,持有输入引脚列表、输出引脚列表、内部 gateMap 和 connectMap,是子电路数据的载体,不包含任何模拟逻辑。
- CircuitBuilder:持有输入行数组、外部输入映射、连接关系映射、子电路映射,是解析构建阶段的协调者。负责识别
C1: ... endc子电路块,将主电路和子电路的输入行转化为门对象和连接关系。 - CircuitSimulator:持有外部输入映射、连接关系映射、全局门映射、有效门队列、子电路映射、子电路输入暂存区,是模拟执行阶段的驱动者。负责队列驱动的主电路信号传播,以及按依赖顺序递归模拟子电路。
- GateFactory:纯工厂类,只提供静态方法创建门对象并管理全局门映射,不持有实例状态。
- PinParser:纯工具类,只提供静态方法解析引脚字符串,不持有任何成员变量。
- GateComparator:纯比较器类,只实现按类型(A→O→N→X→Y)和编号升序排序的逻辑。
作业六设计心得
优点:
- 分层架构清晰:将解析(CircuitBuilder)、模拟(CircuitSimulator)、输出(OutputFormatter)三阶段完全分离,主流程
Main.main()保持线性串联结构,未因复杂度增长而膨胀。 - 队列驱动传播:每个门只在其最后一个输入被设置后才进入有效队列,避免了盲目轮询,模拟效率高。
- 递归子电路模拟:通过
while (changed)循环 +processed集合按依赖顺序处理子电路,支持多级串联和复杂依赖链。 - 异常优先级机制:五种异常按题目规定的优先级顺序依次检测,一旦触发立即停止当前电路解析,符合题目要求。
- 工厂模式隔离创建逻辑:
GateFactory.createGate()集中管理 9 种元件的实例化,新增元件类型时只需修改工厂类。 - 多态设计:模拟器核心只依赖
Gate抽象类,新增门类型无需修改CircuitSimulator代码。
缺点: - 工厂方法圈复杂度偏高:
GateFactory.createGate()圈复杂度达 15,由 9 种元件的 switch 分支和引脚解析逻辑导致,后续若新增元件类型将持续膨胀,可考虑拆分为多工厂或注册表模式。 - 输出格式化类复杂度集中:
OutputFormatter.printGates()圈复杂度达 23,同时承担收集、排序、去重、格式化四条职责,后续可拆分为收集器 + 格式化器的协作模式。
三、踩坑心得
1. V1 同一个门被创建了多个对象
问题表现

V1 中 GateFactory.creatGate() 每次被调用都会 new 一个门对象。一个门的不同引脚会多次出现在输入中,例如 X1-1、X1-2、X1-0 分别对应异或门 X1 的输入引脚1、输入引脚2、输出引脚0。每次调用 creatGate 都会创建一个新的 XorGate 对象。
由于 HashSet 的 equals 和 hashCode 基于 id 和类型判断相等,重复添加的对象不会进入 gates 集合,但 gateMap 中的映射会被后创建的对象覆盖。最终,setInputPin 操作的是最后一次创建的对象(即 gateMap 中的那个),而 gates 里保留的是第一次创建的对象,从未被设置过输入引脚,count 始终为 0,isEffective() 永远返回 false,导致所有门都不输出。
这个坑点在于,HashSet 虽然阻止了重复添加,但旧对象仍然存活并被错误使用,而 gateMap 的覆盖行为让调试时难以察觉操作的对象和 gates 里的对象不是同一个。直到用 System.identityHashCode() 打印对象地址,才发现两个地方的 hashCode 相同但 identityHashCode 不同。这个坑让我加深了对同一个对象的理解。
2. V4 子电路依赖顺序导致中间大改
问题表现

V4 新增子电路功能后,最初的设计是在 CircuitSimulator 中用一个 subInputs Map 收集所有子电路的输入值,然后一次性遍历所有子电路调用 simulate()。对于独立的子电路(如 C1 和 C2 互不依赖),这个方案能够正常工作。
但当子电路之间存在依赖链时——例如 C1 的输出连接到 C2 的输入——问题就暴露了。subInputs 在收集阶段同时包含 C1 和 C2 的输入,但 C2 的输入依赖于 C1 的输出,而 C1 的输出只有在 C1 被模拟之后才能产生。按原方案一次性遍历 subInputs,如果 C2 排在 C1 前面先被模拟,它的输入还不完整,导致 C2 无法计算输出,整条依赖链断裂。
更隐蔽的是,subInputs 中的 Map 是 pendingInputs 的直接引用,子电路模拟过程中 CircuitSimulator 对 valueMap 的修改会污染 pendingInputs,导致后续轮次的数据混乱。
踩完坑后深觉不能把问题想太简单。
3. V2布尔数组转二进制混乱
问题表现

译码器、数据分配器、数据选择器的二进制处理反复横跳,用例十的处理和题目描述有出入,加上沿用V1架构整体全改前过的一半测试点受这点影响的测试点不多(从后往前和从前往后都能过),导致一直确认不了正确的顺序,后面全改对大部分测试点才确认三个元件的顺序。本来不应该耗费太多时间的点,却因为急躁找错,数据分配器和数据选择器的转法一致但写的时候很长一段时间都写成了相反更是延后了进度。这个坑启示我找不到错时不应该一直盯着有出入的地方看,而是要思考整体架构问题,特别是一半测试点都过不了的情况。
四、改进建议
4.1 架构层面
- 引入独立的输出格式化类:V1 中
OutputFormatter已承担了收集、排序、输出的职责,但门输出逻辑与主流程仍有耦合。后续可将子电路门和主电路门的格式化策略拆分为独立的MainGateFormatter和SubGateFormatter,进一步降低printGates()的圈复杂度(当前 23)。 - 统一引脚解析职责:V1 中
PinParser负责引脚字符串解析,V2 中CircuitParser.resolvePin()又重复实现了类似逻辑,V4 中仍依赖PinParser。可考虑将解析逻辑完全收敛到PinParser中,其他类只调用静态方法,避免三版本中解析逻辑的碎片化。
4.2 代码层面
- 工厂方法拆分:
GateFactory.createGate()(V1)和ComponentFactory.createComponent()(V2)圈复杂度分别为 15 和 13,均由多类型 switch 分支导致。后续可改用注册表模式,将各门类型的创建函数注册到 Map 中,通过类型标识直接查找创建逻辑,消除 switch 分支。 - 圈复杂度治理:
OutputFormatter.printGates()圈复杂度为 23,可拆分为:collectGates():收集子电路门和主电路门sortGates():按类型和编号排序formatGate():根据门来源决定输出格式
将 23 的圈复杂度拆解到多个 1~3 的方法中。
4.3 可维护性优化
- 注释率提升:V1 注释率 7.1%,V2 8.2%,V4 7.9%,三版本注释率均在 8% 上下浮动。虽然核心类和方法已添加必要说明,但复杂逻辑(如异常判断顺序、子电路依赖传播、25 次迭代收敛依据)缺乏行内注释。后续应将注释率提升至 15% 以上,尤其对
Decoder.calculateOutput()、CircuitSimulator.processSubCircuits()等复杂方法补充详细说明。 - 双版本继承优化:V2 中
Component与 V4 中Gate存在功能重叠但未复用,后续如有融合需求,可将Gate抽象类的count/isEffective()机制与Component的 Pin 传播模型结合,形成统一的第三代仿真引擎。
五、总结
5.1 收获与成长
写代码前先理清数据流向,是整个三轮迭代最实在的收获。V1 的时候拿到题目就直接开写,类该干什么、数据该从哪到哪,全是边写边想。到 V4 加子电路的时候,发现如果一开始没想清楚信号从主电路流进子电路、子电路输出再流回主电路的路径,后面根本没法收场。这逼着我在动笔之前先把整条链画清楚,后面写起来反而顺了很多。调试能力的进步更明显。三轮迭代下来,遇到 bug 的第一反应从“翻代码找错误”变成了“推数据流向找断点”。V1 的重复对象问题也好,V4 的子电路依赖链断裂也好,本质上都是数据没走到该去的地方。学会了顺着连接关系一层层排查,而不是漫无目的地改代码碰运气。
5.2 不足与待提升
注释率三版本都卡在 8% 上下,核心算法的设计意图和边界条件缺乏说明,自己回头看都要想半天。V4 的 28 分最后没拿到,卡在异常优先级的用例上——isInputPin() 的判断顺序和题目要求的检测优先级可能有偏差,但改了好几轮仍然有几个 case 没过。前面改子电路依赖顺序已经耗了太多精力,实在没有心力继续死磕这一块。整体设计能力还不足以应对这种多条件交织、优先级嵌套的错误类型,硬撑下去大概率也是反复试错、效率极低。有些分拿不到,不是态度问题,是当前能力边界确实在这。能意识到这点,比盲目死磕更有价值。
5.3 课程与作业改进建议
V1 到 V4 中间隔了一个完全重写的 V2,加上 V4 本身也在中间经历了一次架构大改(从一次性遍历子电路改为循环依赖扫描),导致三版本的代码风格差异较大。建议之后的系列作业可保持核心引擎部分的接口稳定,允许新增东西但不要搞的每次都有推倒重来的可能。特别,V2 的 Pin 传播模型和 V1 的队列驱动模型各有优劣,但 V1 能 AC 的模型到了 V2 却一半都过不了。
