数字电路模拟程序

一、前言:
数字电路是一种处理离散信号的电子电路。与处理连续变化信号(如声音、温度)的模拟电路不同,数字电路只识别和运算两种基本状态:高电平(通常表示为“1”) 和 低电平(通常表示为“0”)。这正好与二进制数制系统相对应,使得数字电路成为所有计算机和数字系统的物理实现基础。这是一次关于数字电路模拟的大作业,经历了两次迭代,第一次大作业,电路中包含与门、或门、非门、异或门、同或门、第二次大作业增加了三态门、译码器、数据选择器、数据分配器九种元件。
二、设计与分析:
第一次大作业:
电路中包含与门、或门、非门、异或门、同或门五种元件。元件特征如下:
与门:
包含两个或多个输入引脚和一个输出引脚。所有输入引脚必须都是高电平,输出引脚才是高电平,只要有一个输入引脚为低电平,输出引脚输出低电平。
或门:
包含两个或多个输入引脚和一个输出引脚。所有输入引脚必须都是低电平,输出引脚才是低电平,只要有一个输入引脚为高电平,输出引脚输出高电平。
非门:
包含一个输入引脚和一个输出引脚。输出引脚的电平与输入引脚的电平相反,如输入为低电平,输出则为高电平。
异或门:
包含两个输入引脚和一个输出引脚。当两个输入引脚电平不一致时输出引脚输出高电平,否则输出低电平。
同或门:
包含两个输入引脚和一个输出引脚。当两个输入引脚电平一致时输出引脚输出高电平,否则输出低电平。
2、程序输入
1)元件信息:
用A、O、N、X、Y 分别用作与门、或门、非门、异或门、同或门五种元件的元件标识符。
电路中的每个与门、或门用“标识符(输入引脚数)+编号”作为其元件名。
例如:A(8)1表示一个8输入引脚的与门,O(4)2代表一个4输入引脚的或门。
电路中的每个非门、异或门、同或门用“标识符+编号”作为其元件名。
例如:X8表示一个异或门,Y4代表一个同或门,N1代表一个非门。
约束条件:
不同元件的编号可以相同,如X4、Y4。
同一电路中同种元件的编号不可重复,可以不连续
2)引脚信息:
引脚信息由“元件名-引脚号”构成,。
例如:A(8)1-2代表与门A(8)1的2号引脚。
3)电路的输入信息:
电路的输入格式:
INPUT:英文空格+输入1+”-”+输入信号1+英文空格+输入2+....+输入n+”-”+输入信号n
例如:
“INPUT: A-0 B-1 C-0”代表整个电路包括3个输入:A、B、C 分别输入0,1,0信号。
4)连接信息
引脚的连接信息格式:
[+输出引脚+英文空格+输入引脚1+。。。。+英文空格+输入引脚+]
例如:
[A A(8)1-1 A(8)1-3 X5-2]
代表信号从引脚A发送给与门A(8)1的1、3两个引脚,以及异或门X5的2号引脚。
[Y8-0 N1-1 O(4)2-3 Y2-1]
代表信号从引脚Y8-0发送给非门N1的1号引脚、或门O(4)2的3号引脚、同或门Y2的1号引脚。
约束条件:
一个输出引脚可以连接多个输入引脚,即将输出引脚的信号传给每一个输入引脚。但一个输入引脚不能连接多个输出引脚。
输出引脚不能短接在一起。
5)输入结束信息
所有输入以end为结束标志,end之后出现的内容忽略不计
3、程序输出
按照与门、或门、非门、异或门、同或门的顺序依次输出所有元件的输出引脚电平。同类元件按编号从小到大的顺序排序。
如果某个元件的引脚没有接有效输入,元件输出无法计算,程序输出结果忽略该元件
4、测试输入默认满足以下条件:
1)每个元件的输入引脚连续编号。假设元件有n个输入引脚,则其编号取值范围为[1,n],且引脚号不重复。
2)本题涉及的五种元件都只有一个输出引脚,输出引脚号默认为0。
image
最复杂方法:AndGate.calculate()(复杂度 4)
最大复杂度(Maximum Complexity):4
平均复杂度(Average Complexity):1.50
最深代码块深度:6 层
结构与注释:
类 / 接口数量:4 个
平均每个类的方法数:7.25
平均每个方法的语句数:5.72
注释占比(Percent Lines with Comments):仅 8.4%(注释较少,可能影响可读性)
三、方法复杂度详情
列出了类中 “最复杂的方法”,每个方法的指标是:[复杂度, 语句数, 最大深度, 调用次数]例如:
AndGate.calculate():复杂度 4、语句数 5、深度 6、调用 2 次
Main.calculate():复杂度 2、语句数 4、深度 3、调用 3 次
类图:
image
代码逻辑分析:

输入解析层 解析输入信号、电路连接关系 parseInputSignals/parseConnection/parsePinInfo 把用户输入转化为程序可识别的对象(信号映射、连接实例、逻辑门实例)
核心运算层 信号传播、逻辑门运算 simulateCircuit/LogicGate及其子类 模拟电路信号传播,按逻辑门规则计算输出
结果输出层 结果收集、排序、格式化输出 outputResults/indexOf

工厂方法(createGate):动态创建实例
代码缺陷:
鲁棒性缺陷(最易导致程序异常)

  1. 无异常处理机制,容错性极差
    代码仅通过简单的 null 判断跳过无效场景,但未处理核心异常,易导致静默失败或崩溃:
    类型转换异常:parsePinInfo 返回的引脚号字符串(如 "abc")调用 Integer.parseInt 时,会直接抛出 NumberFormatException 终止程序;
    空指针异常:createGate 返回 null 后,gates.get(gateName) 拿到 null,后续调用 gate.setInput 会触发 NullPointerException;
    输入值异常:parseInputSignals 若拆分后信号值非 "0"/"1"(如 "2"),会错误存储为 false,无任何提示;
    循环依赖未处理:若电路存在循环(如门 A 输出连门 B 输入,门 B 输出连门 A 输入),BFS 队列可能陷入无限循环(虽因 “信号值不变则不加入队列” 缓解,但未显式检测)。
  2. 输入校验缺失
    未校验 INPUT: 行的格式合法性(如 INPUT: IN1-1 IN2,拆分后仅 1 个元素),直接取 signal[1] 会触发数组越界;
    未校验逻辑门输入数量合理性(如与门输入数设为 0),导致 canCalculate 永远返回 true,但 calculate 遍历空集合,输出错误的 true;
    重复设置同一引脚输入值时,无任何提示(如多次给门的 1 号引脚设不同值),覆盖后可能导致计算结果错误。
    通过正则匹配逻辑门名称(如 A (2) 1、N3),动态创建对应类型的逻辑门实例,避免大量 if-else 的硬编码耦合,同时适配不同格式的逻辑门命名规则,提升扩展性。

第二次大作业:
三态门:
三态门的作用类似于电路中的开关。包含一个输入引脚、一个输入控制引脚、一个输出引脚。当控制引脚为高电平时,三态门输入输出之间导通,输出电平等于输入电平;当控制引脚为低电平时,三态门输入输出之间呈现高阻态(类似开关断开),输出为无效状态。
译码器:
译码器的作用是讲输入的编码转换为一路有效信号。一个译码器包含两个或多个输入引脚(如图中的A2\A1\A0)、三个控制引脚(如图中的S3\S2\S1)、4个或多个输出引脚(如图中的Y7~Y0)。根据输入输出的数量有2-4线译码器、3-8线译码器等。
当控制引脚当S1 =1,S2 +S3 =0时,译码器正常工作,输出引脚只有一个输出信号0,其余输出为1;哪个引脚输出0由输入引脚的编码决定,例如:图中的3-8线译码器三个输入引脚信号的编码与输出引脚的编码对应,A2\A1\A0输入000时,Y0输出0,其余输出1;A2\A1\A0输入001时,Y1输出0,其余输出1;依次类推。
控制引脚不满足S1 =1,S2 +S3 =0时,译码器处于无效状态,所有输出为无效值。
数据选择器:
数据选择器的作用是从多路输入信号中选择一个,并将其信号直接送往唯一的输出端,选择哪一路输入信号由控制端决定。如图所示控制端有两个则输入端有4个,S1\S0是两个控制端,D3D0是输入端,S1\S0的4种信号组合00、01、10、11分别选择D3D0其中一路输入。如S1S0=00,则Y=D0;S1S0=01,则Y=D1;S1S0=10,则Y=D2;S1S0=11,则Y=D3
根据输入引脚数量的不同有二选一数据选择器(1个控制端)、四选一数据选择器(2个控制端)、八选一数据选择器(3个控制端)等
数据分配器:
数据分配器的作用与数据选择器正好相反,是将唯一的一路输入信号输出到多路输出引脚的其中之一,选择哪一路输出引脚输出由控制端决定。如图所示控制端有两个AB,输出端有4个W0\W1\W2\W3,D是输入端,AB的4种信号组合00、01、10、11分别选择W3~W0其中一路输出,其他三路输出为无效状态。如AB=00,则W0=D;AB=01,则W1=D;AB=10,则W2=D;AB=11,则W3=D。
根据输出引脚数量的不同有二路数据分配器(1个控制端)、四路数据分配器(2个控制端)、八路数据分配器(3个控制端)等
2、程序输入
1)元件信息:
用A、O、N、X、Y、S 、M、Z、F分别用作
与门、或门、非门、异或门、同或门、
三态门、译码器、数据选择器、数据分配器九种元件的元件标识符。
电路中的每个与门、或门用“标识符(输入引脚数)+编号”作为其元件名。
例如:A(8)1表示一个8输入引脚的与门,O(4)2代表一个4输入引脚的或门。
电路中的每个非门、异或门、同或门用“标识符+编号”作为其元件名。
例如:X8表示一个异或门,Y4代表一个同或门,N1代表一个非门。
电路中的数据选择器、数据分配器用“标识符(控制引脚数)+编号”作为其元件名。
例如:Z(2)2代表一个四选一数据选择器,F(3)2代表一个8路数据分配器。
译码器用“标识符(输入引脚数)+编号”作为其元件名。
例如:M(3)1表示一个3-8线译码器。
约束条件:
不同元件的编号可以相同,如X4、Y4。
同一电路中同种元件的编号不可重复,可以不连续
2)引脚信息:
引脚信息由“元件名-引脚号”构成。
例如:A(8)1-2代表与门A(8)1的2号引脚。
含控制引脚的元件如本次添加的所有元件,按控制-输入-输出的顺序排序,
每种类型的引脚按编号从小到大的顺序排序,
例如3-8线译码器M(3)1包含3个输入引脚、3个控制引脚、8个输出引脚,
M(3)1-0/1/2对应控制引脚S1/S2/S3,
M(3)1-3/4/5对应输入引脚A0/A1/A2,
M(3)1-6/7/8/9/10/11/12/13对应输出引脚Y0~Y7。
又如三态门的三个引脚,0号引脚为控制端、1号引脚为输入端、2号引脚为输出端。
3)电路的输入信息:
电路的输入格式:
INPUT:英文空格+输入1+”-”+输入信号1+英文空格+输入2+....+输入n+”-”+输入信号n
例如:
“INPUT: A-0 B-1 C-0”
代表整个电路包括3个输入:A、B、C 分别输入0,1,0信号。
4)连接信息
引脚的连接信息格式:
[输出引脚+英文空格+输入引脚1+。。。。+英文空格+输入引脚]
例如:
[A A(8)1-1 A(8)1-3 X5-2]
代表信号从引脚A发送给与门A(8)1的1、3两个引脚,以及异或门X5的2号引脚。
[Y8-0 N1-1 O(4)2-3 Y2-1]
代表信号从引脚Y8-0发送给非门N1的1号引脚、或门O(4)2的3号引脚、同或门Y2的1号引脚。
约束条件:
一个输出引脚可以连接多个输入引脚,即将输出引脚的信号传给每一个输入引脚。但一个输入引脚不能连接多个输出引脚。
输出引脚不能短接在一起。
5)输入结束信息
所有输入以end为结束标志,end之后出现的内容忽略不计
3、程序输出
按照与门、或门、非门、异或门、同或门、三态门、译码器、数据选择器、数据分配器的顺序依次输出所有元件的输出引脚电平。
同类元件按编号从小到大的顺序排序。
#如果某个元件的引脚没有接有效输入、输入输出之间断开(如三态门)或控制引脚输入无效,元件输出无效,程序输出忽略该元件。
#译码器不输出引脚电平,输出其输出为0的引脚的编号。如“M(3)1:3”代表译码器M3的输出引脚Y3输出0,其他引脚输出1。
#数据分配器按引脚编号从小到大的顺序输出所有输出引脚的信号,无效状态引脚输出“-”。
如“F(2)1:--0-”代表分配器F1的输出引脚W2输出0信号,其他三个引脚为无效状态。
4、测试输入默认满足以下条件:
1)每个元件的输入引脚连续编号。假设元件有n个输入引脚,则其编号取值范围为[1,n],且引脚号不重复。
2)本题涉及的五种元件都只有一个输出引脚,输出引脚号默认为0。

image
核心质量指标
复杂度相关(风险点):
最复杂方法:getOutputPinNumber().calculate()(复杂度 13)
最大复杂度(Maximum Complexity):13(复杂度≥10 属于高风险,维护难度大)
平均复杂度(Average Complexity):2.30(整体复杂度比 TEST4 高)
最深代码块深度:7 层(嵌套层级较深,逻辑易混乱)
结构与注释:
类 / 接口数量:6 个
平均每个类的方法数:13.00(类的职责较重)
平均每个方法的语句数:5.83
注释占比(Percent Lines with Comments):仅 11.1%(注释仍然偏少,可读性不足)
方法复杂度详情
列出了类中 “最复杂的方法”,每个方法的指标是:[复杂度, 语句数, 最大深度, 调用次数]其中风险较高的方法:
AndGate.calculate():复杂度 5、语句数 5、深度 5、调用 2 次
getControlCount().calculate():复杂度 7、语句数 9、深度 5、调用 2 次
此图片反映了,代码结构设计不合理类的职责过度集中平均每个类的方法数达 13 个(TEST4 仅 7.25 个),说明部分类承担了过多职责(如既负责逻辑运算、又负责引脚管理、还负责信号校验),违反 “单一职责原则”:
类的体积过大(835 行代码),新人接手需通读全部逻辑,学习成本高;不同功能的代码耦合在一个类中,修改 “引脚规则” 可能影响 “运算逻辑”。方法粒度设计粗糙虽然平均每个方法的语句数(5.83)与 TEST4(5.72)接近,但高复杂度方法(如复杂度 13 的calculate)未做拆分,仍将大量逻辑堆砌在单个方法中,缺乏 “小方法、单一职责” 的设计思路。
类图
image
踩坑心得:
初代把解析、模拟、输出全写在Main里,新增译码器 / 选择器后,解析逻辑、模拟逻辑、输出逻辑都要改Main,代码从 380 行膨胀到 835 行,且修改一处可能影响全流程;
例如:新增数据分配器的输出格式时,既要改模拟逻辑(处理多输出),又要改输出逻辑(拼接输出字符串),耦合严重。
早期就要做 “职责拆分”,哪怕功能简单:
按 “解析→模拟→输出→工厂” 拆分为独立类(如改进版的CircuitParser/CircuitSimulator/ResultOutput/GateFactory),降低耦合;
每个类仅负责单一环节,新增器件时仅需修改对应类(如新增译码器只需改GateFactory和CircuitSimulator)。
初代仅处理 “单输出→单输入” 的传播,新增数据分配器(单输入→多输出)后,模拟逻辑要单独处理分配器的多输出,遍历outputs逐个加入队列,逻辑与基础器件不一致,且易遗漏 “输出值为 null” 的场景;
基类统一 “输出收集” 逻辑:
基类新增List getOutputPins()方法,子类返回自身的输出引脚列表(基础门返回[name-0],分配器返回[name-0, name-1, ...]);
模拟逻辑遍历gate.getOutputPins(),统一处理输出传播,无需区分器件类型。
改进建议:
重构基类:抽象Pin类,拆分引脚类型,用State枚举统一状态,消除多Map和硬编码判断;
消除instanceof:基类定义setPinValue/getOutputString等抽象方法,子类实现专属逻辑;
简化复杂度:拆分高复杂度方法(如CircuitSimulator中器件专属逻辑),每个方法不超过 20 行,复杂度控制在 10 以内。

posted @ 2025-12-14 15:24  pys21  阅读(11)  评论(0)    收藏  举报