数字电路模拟程序的迭代设计与学习总结---三次作业复盘
一、前言
这篇博客是对作业集4到作业集6的阶段性总结。三次作业的核心都围绕"数字电路模拟程序"展开,对应的题目分别是 NCHUD-数字电路模拟程序-1、NCHUD-数字电路模拟程序-2、NCHUD-数字电路模拟程序-4。虽然每次作业集从数量上看基本都是一道综合题,但实际写起来并不轻松,因为它考查的不是某一个单独语法点,而是把 Java 类设计、对象状态维护、字符串解析、集合使用、流程控制、异常判断和输出格式统一放进一个模拟类问题中。
从我自己的学习过程来看,这几次作业最大的难点不是"会不会写 if、for、ArrayList",而是怎么把题目中的复杂规则拆成合适的类和方法。刚开始写程序-1时,我更多关注的是能不能让样例跑通;到了程序-2,题目增加了更多复杂元件,我开始发现原来的写法扩展起来比较吃力;到了程序-4,加入子电路和异常检测后,如果不拆分类职责,代码基本很难继续维护。
本次整理中,我读取并核对了三份当前源码的数据。数字电路模拟程序-1 的代码为 280 行,主要类有 Gate、Connection、Main;数字电路模拟程序-2 的代码为 442 行,主要类有 Gate、Connection、Circuit、Main;数字电路模拟程序-4 的代码为 611 行,主要类增加到十多个,包括 Gate、RawConnection、Connection、CircuitDefinition、CircuitHelper、PinInfo、InputReader、ErrorChecker、CircuitBuilder、Simulator、OutputPrinter、Main 等。
| 题目 | 当前源码行数 | 主要类数量 | 主要任务 |
|---|---|---|---|
| 数字电路模拟程序-1 | 280 行 | 3 个类 | 基础逻辑门、连接传播、排序输出 |
| 数字电路模拟程序-2 | 442 行 | 4 个类 | 增加复杂元件,封装电路管理 |
| 数字电路模拟程序-4 | 611 行 | 约 12 个类 | 子电路、异常检测、构建与模拟分离 |
其中,程序-1 的最后一次提交结果为 95/100,最后一个多层级联电路测试点 case40 出现 Non-zero Exit Code,得分为 0/5。这个数据对我来说很有提醒意义:程序不是完全不能跑,而是在复杂场景下还有稳定性问题。程序-2和程序-4部分,我这次主要依据已读取到的题面和右侧当前源码进行分析,没有核对到的评测分数不在文中随便写。
源码数据与类结构图
本次分析主要依据三份已读取源码和题面内容,未核对到的数据不单独编写。整体变化如下:
| 题目 | 源码行数 | 主要类数量 | 重点变化 |
|---|---|---|---|
| 程序-1 | 280 行 | 3 个 | 基础门电路、连接传播、排序输出 |
| 程序-2 | 442 行 | 4 个 | 增加复杂元件,开始封装 Circuit |
| 程序-4 | 611 行 | 约 12 个 | 加入子电路、异常检测和分层处理 |
text
代码规模:280行 -> 442行 -> 611行
类数量: 3个 -> 4个 -> 约12个
程序-1类结构较集中,Main 负责读取、创建对象和输出,Gate 保存门电路状态,Connection 保存连接关系。
text
程序-1类结构
Main
├─ Gate:编号、类型、输入值、输出值、计算逻辑
└─ Connection:起点、终点、信号传递关系
程序-2增加复杂元件后,把一部分电路管理交给 Circuit,代码比程序-1清楚一些,但 Gate 内部承担的类型判断仍然偏多。
text
程序-2类结构
Main
└─ Circuit
├─ Gate:基础门、三态门、译码器、选择器等
└─ Connection:元件之间的连线
程序-4的结构变化最大,输入读取、错误检查、构建、模拟、输出被拆成多个类,维护性比前两题更好。
text
程序-4类结构
InputReader -> CircuitDefinition -> ErrorChecker
└-> CircuitBuilder -> Simulator -> OutputPrinter
CircuitBuilder 使用 Gate、Connection、RawConnection、PinInfo 等对象完成构建
程序-1最后一次提交为 95/100,失败点是 case40 多层级联电路,状态为 Non-zero Exit Code,该点 0/5;运行数据为 21632 KB、130 ms / 800 ms。这个结果说明基础逻辑基本通过,但多层传播仍有稳定性问题。
二、设计与分析
1. 数字电路模拟程序-1:先把基础逻辑门跑起来
程序-1是整个系列的基础版本。题目要求模拟五种基础逻辑门:与门 A、或门 O、非门 N、异或门 X、同或门 Y。输入信息主要分成两类:一类是 INPUT 开头的初始输入信号,另一类是中括号形式的连接信息。程序需要把初始信号按照连接关系传递到元件引脚上,当某个元件的输入引脚都有效后,再计算它的输出,并继续传播到下一级元件。
这一版我的代码主要设计了三个类。Gate 类用于保存元件信息,包括元件类型、名称、编号、输入引脚数量、输入数组、是否完成计算、输出值等。Connection 类用于保存连接关系,也就是信号从哪里来、要传到哪个引脚。Main 类承担了主要流程,包括输入解析、查找元件、创建元件、发送信号、计算输出和排序打印。
程序-1的大致流程可以概括为:
读取 INPUT 初始信号
读取连接信息
根据连接信息创建 Gate 对象
把输入信号写入目标元件引脚
循环扫描所有 Gate,判断是否可以计算
计算输出后继续沿连接关系传播
按 A、O、N、X、Y 顺序排序输出
从设计上看,这一版代码更偏过程式。虽然我定义了 Gate 和 Connection,但是 Gate 更像一个数据容器,真正的逻辑基本还是放在 Main 里。比如 findGate、sendValue、getOrCreate、sortById、printList 等方法都在 Main 中。这样写的好处是简单直接,写的时候不用考虑太多结构;坏处是后期功能一多,Main 类会越来越重。
程序-1最终提交是 95/100,说明大部分基础测试是能通过的,比如单个门、多输入门、简单级联、部分复杂连接等。但是 case40 的多层级联电路出现 Non-zero Exit Code,说明程序在深层传播时还有漏洞。我分析原因可能与信号传播方式有关。当前逻辑是循环扫描所有元件,判断哪些元件已经准备好。如果电路层次比较深,或者某些输出需要多次传播,程序就可能出现状态更新不及时、漏传播、提前计算等问题。
这道题让我认识到,模拟类题目不能只靠"能跑样例"判断是否稳定。基础样例通过,只能说明规则的大方向对了;复杂测试点才会暴露设计上的问题。程序-1如果后续要改进,我认为应该把信号传播改成队列式流程。初始输入先进入队列,每次取出一个信号,找到它连接到的目标引脚,写入后判断目标元件是否已经可以计算。如果可以计算,就把输出继续加入队列。这样比反复扫描所有元件更清晰,也更适合处理多层级联。
2. 数字电路模拟程序-2:复杂元件带来扩展压力
程序-2是在程序-1基础上的扩展版本。除了原来的五种基础逻辑门,题目还增加了三态门、译码器、数据选择器、数据分配器。这个变化让题目难度明显提高,因为每一种新增元件的引脚规则都不同,不再只是简单的 0 和 1 计算。
例如,三态门有输入引脚和控制引脚,当控制端有效时输出等于输入,当控制端无效时输出为无效状态。译码器需要根据多个输入引脚的编码决定哪一路输出有效,还要考虑控制引脚是否满足条件。数据选择器需要根据控制端选择某一路输入作为输出,数据分配器则是把一路输入分配到多路输出中的某一路。也就是说,程序-2已经不只是基础逻辑门的组合,而是开始接近更复杂的数字电路元件模拟。
这一版代码为 442 行,主要类有 Gate、Connection、Circuit、Main。和程序-1相比,最大的变化是增加了 Circuit 类。Circuit 类负责管理整个电路中的输入、元件、连接关系和模拟流程。Main 类不再承担全部细节,而是负责读入数据并调用 Circuit 中的方法。
这一版结构大致可以表示为:
Main
-> Circuit
-> Gate
-> Connection
Circuit 中包含输入解析、连接解析、元件创建、信号发送、模拟运行和结果输出等方法。相比程序-1所有逻辑都放在 Main 里,这样的结构已经清楚了不少。至少从阅读代码的角度看,可以知道电路整体管理逻辑在 Circuit 中,而不是到处散落。
但是程序-2也暴露出另一个问题:虽然代码拆出了 Circuit 类,但不同元件的计算逻辑仍然主要依赖类型字符进行判断。比如根据 A、O、N、X、Y、S、M、Z、F 等类型进入不同计算分支。这种写法能完成当前任务,但扩展性一般。如果以后继续增加元件,compute 相关逻辑会越来越长。
从面向对象角度看,更好的方式应该是使用继承和多态。可以定义一个抽象的 Gate 父类,再让 AndGate、OrGate、NotGate、TriStateGate、Decoder、Mux、Demux 等子类分别实现自己的 compute 方法。这样 Circuit 只负责调度,不需要知道每一种门内部如何计算。当然,以我当前学习阶段来说,程序-2能先把 Circuit 拆出来已经是一种进步,但多态思想还没有真正落实。
程序-2让我体会到,代码设计不能只看当前题目能不能过,还要看下一步是否容易扩展。如果一个新规则加入后,需要大范围修改旧代码,就说明原来的职责划分还不够好。
3. 数字电路模拟程序-4:子电路和异常检测让结构变复杂
程序-4是这三道题中综合性最强的一题。它不是继续增加更多基础元件,而是在程序-1基础上增加了子电路和异常输入检测两个功能。这个变化让题目从"计算电路输出"进一步变成了"管理复杂电路结构"。
子电路的定义格式类似 C1:、INPUT、OUT、若干连接信息、endc。主电路可以通过 C1-A、C1-B、C1-C 这样的形式引用子电路的输入和输出。输出时,子电路内部元件还要带上子电路编号,例如 C1-A(2)1-0:0。这个要求说明子电路不是普通字符串,而是一个包含输入、输出、内部元件和内部连接的组合结构。
程序-4 的代码规模为 611 行,类的数量也明显增加。主要类和职责如下:
| 类名 | 主要职责 |
|---|---|
| Gate | 表示基础逻辑元件 |
| RawConnection | 保存原始连接文本 |
| Connection | 保存处理后的连接关系 |
| CircuitDefinition | 保存主电路或子电路定义 |
| CircuitHelper | 处理元件名、引脚类型、格式判断等辅助逻辑 |
| PinInfo | 保存解析后的引脚信息 |
| InputReader | 读取子电路和主电路输入 |
| ErrorChecker | 负责异常输入检测 |
| CircuitBuilder | 构建实际用于模拟的电路结构 |
| Simulator | 负责信号传播和计算 |
| OutputPrinter | 按题目要求输出结果 |
| Main | 程序入口 |
从结构上看,这一版已经明显比前两版更模块化。InputReader 负责读取,ErrorChecker 负责检查错误,CircuitBuilder 负责把主电路和子电路构造成可模拟的结构,Simulator 负责运行模拟,OutputPrinter 负责输出结果。这样一来,Main 类只需要串联整体流程。
程序-4的大致流程可以整理为:
读取所有子电路定义
读取主电路定义
对连接信息进行异常检测
根据主电路和子电路构建实际元件与连接
发送初始输入信号
运行模拟器进行信号传播
按题目要求输出结果
这一版最难的地方,我认为有两个。第一个是子电路命名空间问题。主电路里可能有 N1,子电路里也可能有 N1,如果不做区分,它们在集合中就会冲突。因此代码需要给子电路内部元件加上子电路编号前缀,保证每个元件在全局模拟时都是唯一的。第二个是异常检测。题目要求判断一个连接信息中是否包含多个输入、是否没有输入、是否没有输出、输入输出顺序是否错误、输入引脚是否冲突等。这些错误有时还会同时出现,必须按题目要求的优先级输出。
程序-4让我比较明显地感受到,面向对象设计不是为了"看起来高级",而是为了让复杂问题可拆、可查、可改。如果把子电路展开、异常检测、信号模拟全写在 Main 里,代码会非常难维护。拆成多个类之后,虽然总行数增加到了 611 行,但每一部分的责任更清楚,调试时也更容易定位。
三、踩坑心得
1. 字符串解析不能随手写
三道题里最频繁出现的问题就是字符串解析。像 A(2)1-1、X1-0、C1-A、A A(2)1-1 这些格式看起来都不长,但含义差别很大。刚开始我习惯直接使用 split 和 substring,样例简单时能跑,格式一复杂就容易出错。
比如在程序-4里,C1-A 可能表示子电路输入,C1-N1-0 又可能表示子电路内部元件输出。如果没有统一的解析规则,很容易把两种东西混在一起。后面我才意识到,解析逻辑应该集中处理,先判断是不是元件引脚,再判断是输入引脚还是输出引脚,再判断是否属于子电路。这样代码会多一些,但比到处临时拆字符串更可靠。
2. 程序-1的 95 分说明基础能跑不代表结构稳定
程序-1拿到 95/100 时,我一开始觉得只差 5 分,问题应该不大。但后来回头看,这个失败点其实暴露的是结构问题。大部分测试点能过,说明基础规则处理得差不多;但多层级联电路失败,说明传播流程在复杂场景下不够稳定。
这个问题不是简单多加几个判断就能彻底解决的。它更像是设计上的问题:信号应该如何传播,元件什么时候计算,输出什么时候继续传递,这些都应该有明确流程。如果只是不断扫描所有元件,复杂情况下就可能漏掉某些状态变化。
3. 类拆分不是为了凑数量
程序-1只有三个类,写起来快,但 Main 类太重。程序-4有十二个左右的类,看起来复杂,但每个类职责更明确。以前我对"单一职责"的理解比较浅,觉得类拆多了只是形式。做完这几道题后,我发现类拆分真正的意义是降低维护难度。
当输入读取、错误检测、构建、模拟、输出都混在一起时,出错后很难判断问题在哪里。拆开以后,至少能先判断问题大概出在读取阶段、检查阶段还是模拟阶段。
4. 子电路需要命名空间意识
程序-4中,子电路内部的元件名可能和主电路重复。如果直接用原始名称保存,后面查找和输出都会混乱。因此需要给子电路内部元件加上前缀,例如 C1-N1。这个问题让我第一次比较具体地理解了"命名空间"的作用。
5. 异常检测要考虑优先级
异常检测不是发现错误就随便输出,而是要符合题目规定。有些连接信息可能同时满足多种错误情况,比如既存在多个输入,又没有合法输出。如果判断顺序不对,输出的错误类型就会和题目要求不一致。这个地方也说明我的测试习惯还不够好,应该提前整理更多异常样例,而不是只靠提交后的结果判断。
四、改进建议
1. 程序-1改用队列式信号传播
程序-1最需要改进的是信号传播方式。可以把初始输入和元件输出都看成待传播信号,放入队列中统一处理。每次从队列中取出一个信号,发送到所有目标引脚。如果目标元件输入完整,就计算输出并继续入队。这样比循环扫描所有元件更直观,也更适合多层级联。
2. 程序-2引入继承和多态
程序-2中元件类型已经很多,如果继续使用类型字符判断,代码会越来越长。后续可以定义抽象 Gate 类,再让不同元件继承它,各自实现 compute 方法。这样 Circuit 只负责调度,具体怎么算交给具体元件类处理。
3. 程序-4优化错误类型设计
程序-4的 ErrorChecker 还可以继续优化。可以把错误类型设计成统一枚举或常量,例如 MORE_THAN_ONE_INPUT、NONE_INPUT、NONE_OUTPUT、SEQUENCE_ERROR、INPUT_CONFLICT。这样错误判断和错误输出可以分离,后续增加规则时也更好维护。
4. 建立本地测试样例库
这几道题让我意识到本地测试很重要。后续应该自己整理测试样例,包括单个基础门、多输入门、多层级联、多分支连接、子电路调用、输入冲突、输入输出顺序错误等。每次修改代码后先在本地跑一遍,再提交 PTA,效率会比盲目提交高很多。
五、总结
这三次作业让我对 Java 面向对象有了更实际的认识。程序-1让我完成了基础逻辑门模拟,也暴露出 Main 类过重和复杂级联不稳定的问题。程序-2让我开始把电路整体封装到 Circuit 类中,逐渐意识到模块化的重要性。程序-4进一步加入子电路和异常检测,让我真正感受到复杂程序必须先分析对象、关系和流程,再开始写代码。
从 280 行到 442 行,再到 611 行,代码规模不断增加。如果没有类的拆分,后面很难继续维护。虽然目前代码还有不少不足,比如多态使用不充分、部分解析逻辑偏复杂、本地测试不够系统,但相比刚开始,我对对象、职责、流程这些概念的理解更清楚了。
后续我还需要继续学习继承、多态、接口、异常处理和设计模式,尤其是组合模式和事件驱动模拟这类思想。对我来说,这一阶段最大的收获不是某一个语法点,而是明白了复杂题目不能一上来就写代码,应该先分析数据结构和业务流程。结构想清楚了,代码才不会越写越乱。