数字电路模拟程序的迭代设计与学习总结---三次作业复盘

数字电路模拟程序的迭代设计与学习总结---三次作业复盘

一、前言

这篇博客是对作业集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 行,代码规模不断增加。如果没有类的拆分,后面很难继续维护。虽然目前代码还有不少不足,比如多态使用不充分、部分解析逻辑偏复杂、本地测试不够系统,但相比刚开始,我对对象、职责、流程这些概念的理解更清楚了。

后续我还需要继续学习继承、多态、接口、异常处理和设计模式,尤其是组合模式和事件驱动模拟这类思想。对我来说,这一阶段最大的收获不是某一个语法点,而是明白了复杂题目不能一上来就写代码,应该先分析数据结构和业务流程。结构想清楚了,代码才不会越写越乱。