数字电路模拟程序系列作业总结
一、前言
本阶段三次作业围绕“数字电路模拟程序”展开,从基础逻辑门电路模拟到引入多输入输出组合元件,再到子电路与异常处理机制,逐步深入。三次作业的核心目标都是构建一个能够解析电路描述、计算信号传播并输出结果的模拟系统。
知识点覆盖:
第一次作业:面向对象基础设计、字符串解析、集合框架使用、简单逻辑门(与门、或门、非门、异或门、同或门)的模拟
第二次作业:多态与继承、复杂引脚类型(控制引脚、输入引脚、输出引脚)的管理、组合元件(三态门、译码器、数据选择器、数据分配器)的模拟
第四次作业(注:三次作业指1、2、4,跳过3):子电路设计模式(组合模式)、异常处理机制、输入合法性验证
题量与难度: 每次作业均为单一编程题,题量适中但难度递增。第一次作业约200-300行代码即可完成,第二次作业扩增至600-800行,第四次作业则在代码量上进一步增长至1000行以上,且逻辑复杂度显著提升。
核心挑战: 在电路元件种类不断扩展的情况下,如何保持代码的可扩展性和可维护性;在引入子电路后,如何处理层次化结构和信号传递;在异常检测要求下,如何权衡性能与正确性。
二、设计与分析
2.1 第一次作业:基础逻辑门电路模拟
2.1.1 代码结构分析
第一次作业中,我采用了较为直接的面向对象设计。核心类包括:
Gate:抽象基类,定义所有逻辑门共有的属性(名称、输入引脚列表、输出引脚)和行为(计算输出)
AndGate、OrGate、NotGate、XorGate、XnorGate:继承自Gate的具体实现类
Pin:引脚类,包含所属元件、引脚编号、引脚类型和信号值
Circuit:电路类,负责管理所有元件和连接关系
Main:主类,负责解析输入并驱动电路模拟
类图如下:

2.1.2 设计心得与反思
优点:
采用继承结构,每种逻辑门独立实现compute()方法,逻辑清晰
引脚与元件分离,便于管理连接关系
不足:
输入解析部分使用了大量if-else判断,代码耦合度较高
电路模拟采用简单的拓扑排序,但对输入顺序不合理的场景处理不够健壮
没有考虑信号传播的时序问题,所有元件在输入信号全部就绪后才计算
SourceMonitor分析:
tMetrics Details For File 'MainPta4.java'
Parameter Value
========= =====
Project Directory D:\CodeBase\java代码学习\pta2.1
Project Name
Checkpoint Name Baseline
File Name MainPta4.java
Lines 489
Statements 301
Percent Branch Statements 23.9
Method Call Statements 106
Percent Lines with Comments 2.2
Classes and Interfaces 12
Methods per Class 4.17
Average Statements per Method 5.78
Line Number of Most Complex Method 167
Name of Most Complex Method AddPinToGate.addPin()
Maximum Complexity 11
Line Number of Deepest Block 57
Maximum Block Depth 5
Average Block Depth 1.81
Average Complexity 2.19
Most Complex Methods in 11 Class(es): Complexity, Statements, Max Depth, Calls
AddPinToGate.addPin() 11, 18, 4, 2
AddPinToGate.AddPinToGate() 1, 2, 2, 0
AND_gate.addPin() 1, 1, 2, 1
AND_gate.AND() 3, 4, 4, 1
AND_gate.AND_gate() 1, 5, 2, 5
AND_gate.getOutPin() 1, 1, 2, 0
AND_gate.IsPinNull() 1, 1, 2, 1
AND_gate.setOutPin() 1, 1, 2, 1
CreateGate.create_gate() 7, 12, 3, 0
Gate.Gate() 1, 1, 2, 0
Gate.getName() 1, 1, 2, 0
JudgeGate.judge_gate() 9, 12, 3, 7
JudgeGate.judge_out() 7, 12, 3, 0
NOT_gate.getInput() 1, 1, 2, 0
NOT_gate.getOutPin() 1, 1, 2, 0
NOT_gate.IsPinNull() 2, 3, 3, 0
NOT_gate.NOT() 3, 4, 3, 1
NOT_gate.NOT_gate() 1, 3, 2, 1
NOT_gate.setInput() 1, 1, 2, 0
NOT_gate.setOutPin() 1, 1, 2, 1
OR_gate.addPin() 1, 1, 2, 1
OR_gate.getOutPin() 1, 1, 2, 0
OR_gate.IsPinNull() 1, 1, 2, 1
OR_gate.OR() 3, 4, 4, 1
OR_gate.OR_gate() 1, 5, 2, 5
OR_gate.setOutPin() 1, 1, 2, 1
Pin.getName() 1, 1, 2, 0
Pin.getValue() 1, 1, 2, 0
Pin.Pin() 1, 2, 2, 0
ReturnOutValue.System.out.println() 7, 12, 3, 5
XNOR_gate.getInput1() 1, 1, 2, 0
XNOR_gate.getInput2() 1, 1, 2, 0
XNOR_gate.getOutPin() 1, 1, 2, 0
XNOR_gate.IsPinNull() 3, 3, 3, 0
XNOR_gate.setInput1() 1, 1, 2, 0
XNOR_gate.setInput2() 1, 1, 2, 0
XNOR_gate.setOutPin() 1, 1, 2, 1
XNOR_gate.XNOR() 6, 4, 3, 4
XNOR_gate.XNOR_gate() 1, 4, 2, 1
XOR_gate.getInput1() 1, 1, 2, 0
XOR_gate.getInput2() 1, 1, 2, 0
XOR_gate.getOutPin() 1, 1, 2, 0
XOR_gate.IsPinNull() 3, 3, 3, 0
XOR_gate.setInput1() 1, 1, 2, 0
XOR_gate.setInput2() 1, 1, 2, 0
XOR_gate.setOutPin() 1, 1, 2, 1
XOR_gate.XOR() 6, 4, 3, 4
XOR_gate.XOR_gate() 1, 4, 2, 1
Block Depth Statements
0 22
1 97
2 114
3 52
4 15
5 1
6 0
7 0
8 0
9+ 0
2.2 第二次作业:引入多输入输出组合元件
2.2.1 代码结构分析
第二次作业在第一次基础上增加了三态门、译码器、数据选择器和数据分配器,元件种类从5种扩展至9种,引脚类型从单纯的输入/输出扩展至控制引脚。我的设计进行了如下调整:
引入PinType枚举:CONTROL、INPUT、OUTPUT三种类型
Gate类增加pinTypeMap,按类型管理引脚
为复杂元件(译码器、数据选择器、数据分配器)设计专门的内部逻辑
类图:

2.2.2 设计心得与反思
改进点:
使用PinType枚举清晰区分引脚功能,避免混淆
对译码器、数据选择器等复杂元件实现了专用计算逻辑,方便调试
采用Map结构管理引脚,查找效率O(1)
不足:
在增加新元件时仍然需要修改GateFactory,违反了开闭原则
引脚编号映射逻辑较为混乱,例如译码器的引脚02为控制端、35为输入端、6~13为输出端,代码中硬编码了大量if (pinNum <= 2)之类的判断
信号传播采用BFS方式,但未对环形电路进行检测,存在死循环风险
SourceMonitor分析:
Metrics Details For File 'Main.java'
Parameter Value
========= =====
Project Directory D:\CodeBase\java代码学习\pta2.2
Project Name
Checkpoint Name Baseline
File Name Main.java
Lines 757
Statements 312
Percent Branch Statements 29.2
Method Call Statements 99
Percent Lines with Comments 24.2
Classes and Interfaces 15
Methods per Class 8.60
Average Statements per Method 2.29
Line Number of Most Complex Method 11
Name of Most Complex Method Main.main()
Maximum Complexity 13
Line Number of Deepest Block 660
Maximum Block Depth 7
Average Block Depth 3.17
Average Complexity 7.00
Most Complex Methods in 2 Class(es): Complexity, Statements, Max Depth, Calls
main().validComponents.sort(() 4, 9, 4, 8
Main.handleComponentOutputRouting() 5, 6, 5, 7
Main.handleInputSignalRouting() 6, 10, 6, 8
Main.main() 13, 12, 4, 9
Block Depth Statements
0 6
1 24
2 93
3 115
4 74
5 54
6 10
7 3
8 0
9+ 0
2.3 第三次作业:子电路与异常处理
2.3.1 代码结构分析
第四次作业在第一次作业基础上增加了两大功能:子电路和异常检测。虽然没有第二次的9种元件,但子电路的引入极大地增加了设计难度。
核心设计思路: 采用组合模式(Composite Pattern)
text
┌──────────────────┐
│ Component(接口) │
├──────────────────┤
│ + compute() │
│ + getOutput() │
└──────────────────┘
↑
┌────────┴────────┐
│ │
┌──────────┐ ┌──────────┐
│ Gate │ │ SubCircuit│
│(Leaf) │ │(Composite)│
└──────────┘ └──────────┘
实现细节:
Component接口定义统一的计算和输出获取方法
Gate类实现Component接口,为单个元件
SubCircuit类实现Component接口,内部维护一个List
引脚解析设计:
子电路的输入引脚使用INPUT:语句定义,输出引脚使用OUT:语句定义,连接信息中可以使用子电路引脚名(如C1-A)作为输出/输入。
异常检测类设计:
text
┌──────────────────────┐
│ ConnectionValidator │
├──────────────────────┤
│ + validate(List
└──────────────────────┘
完整类图:

2.3.2 设计心得与反思
组合模式的优势:
子电路和普通元件在接口层面完全一致,主电路无需区分两者
支持递归嵌套,子电路内可以包含其他子电路
计算过程自然形成递归,代码简洁
异常处理的设计决策:
五种异常按照优先级排序,优先输出优先级高的异常。我采用了一个验证管道设计:
首先检查连接信息是否符合基本格式
然后按优先级顺序逐项验证
一旦发现违规立即停止验证并输出
SourceMonitor分析:
Metrics Details For File 'Main.java'
Parameter Value
========= =====
Project Directory D:\CodeBase\java代码学习\pta2.4
Project Name
Checkpoint Name Baseline
File Name Main.java
Lines 1,028
Statements 825
Percent Branch Statements 27.8
Method Call Statements 389
Percent Lines with Comments 1.7
Classes and Interfaces 15
Methods per Class 7.27
Average Statements per Method 6.21
Line Number of Most Complex Method 653
Name of Most Complex Method Main.process()
Maximum Complexity 18
Line Number of Deepest Block 528
Maximum Block Depth 6
Average Block Depth 2.17
Average Complexity 2.57
Most Complex Methods in 13 Class(es): Complexity, Statements, Max Depth, Calls
AndGate.AndGate() 1, 1, 2, 1
AndGate.calculateOutput() 4, 6, 3, 3
Circuit.addConnection() 1, 1, 2, 1
Circuit.addGate() 2, 5, 3, 5
Circuit.addPinToMap() 1, 2, 2, 2
Circuit.addSubcircuit() 1, 1, 2, 1
Circuit.applyInputSignals() 6, 8, 5, 6
Circuit.buildDependencyGraph() 8, 14, 6, 9
Circuit.Circuit() 1, 9, 2, 0
Circuit.computeIndegrees() 4, 7, 4, 5
Circuit.detectSignalConflicts() 5, 10, 4, 8
Circuit.extractNumber() 5, 9, 3, 9
Circuit.getAllOutputs() 1, 1, 2, 1
Circuit.getConnections() 1, 1, 2, 0
Circuit.getGateMap() 1, 1, 2, 0
Circuit.getInputValue() 1, 1, 2, 1
Circuit.getOutputs() 8, 17, 6, 13
Circuit.getPin() 1, 1, 2, 1
Circuit.getPinMap() 1, 1, 2, 0
Circuit.getSubcircuitById() 1, 1, 2, 1
Circuit.getSubcircuits() 1, 1, 2, 0
Circuit.getTypeOrder() 7, 13, 4, 0
Circuit.putPin() 1, 1, 2, 1
Circuit.resolveSourceValue() 4, 6, 2, 5
Circuit.setInput() 1, 1, 2, 1
Circuit.simulate() 12, 23, 5, 20
Connection.Connection() 1, 3, 2, 1
Connection.countSignalSinks() 4, 7, 3, 2
Connection.countSignalSources() 4, 7, 3, 2
Connection.getInputPins() 3, 6, 3, 3
Connection.getInputPinStr() 1, 1, 2, 0
Connection.getOutputPin() 1, 1, 2, 1
Connection.getOutputPinStr() 1, 1, 2, 0
Connection.getRawconnection() 1, 1, 2, 0
Connection.isSignalSink() 8, 19, 5, 10
Connection.isSignalSource() 8, 19, 5, 10
Connection.parse() 5, 8, 3, 4
Connection.validate() 7, 14, 3, 4
Gate.addInputPin() 1, 1, 2, 1
Gate.addOutputPin() 1, 1, 2, 1
Gate.Gate() 3, 8, 3, 2
Gate.getAllPins() 1, 4, 2, 3
Gate.getInputMap() 1, 1, 2, 0
Gate.getInputPin() 1, 1, 2, 1
Gate.getName() 1, 1, 2, 0
Gate.getOutputMap() 1, 1, 2, 0
Gate.getOutputPin() 1, 1, 2, 1
Gate.getType() 1, 1, 2, 0
Gate.hasInputValue() 3, 4, 3, 2
GateFactory.createExpandedGate() 7, 15, 4, 2
GateFactory.createGate() 7, 16, 4, 3
GateFactory.extractInputPinCount() 3, 9, 4, 5
GateFactory.extractNumber() 3, 6, 3, 5
getOutputs().sortedGates.sort(() 2, 6, 3, 6
Main.main() 1, 1, 2, 0
Main.Main() 1, 1, 2, 0
Main.process() 18, 37, 5, 28
Main.run() 3, 8, 3, 7
NotGate.calculateOutput() 4, 7, 2, 3
NotGate.NotGate() 1, 1, 2, 1
OrGate.calculateOutput() 4, 6, 3, 3
OrGate.OrGate() 1, 1, 2, 1
Pin.connectTo() 1, 1, 2, 1
Pin.getConnectedTo() 1, 1, 2, 0
Pin.getFullName() 3, 4, 3, 0
Pin.getOwner() 1, 1, 2, 0
Pin.getPinNum() 1, 1, 2, 0
Pin.getType() 1, 1, 2, 0
Pin.getValue() 1, 1, 2, 0
Pin.hasValue() 1, 1, 2, 0
Pin.Pin() 1, 5, 2, 0
Pin.propagateValue() 5, 9, 4, 4
Pin.setValue() 1, 1, 2, 0
Subcircuit.addConnection() 1, 1, 2, 1
Subcircuit.addGate() 1, 2, 2, 2
Subcircuit.addInput() 1, 3, 2, 2
Subcircuit.addOutput() 1, 3, 2, 2
Subcircuit.getConnections() 1, 1, 2, 0
Subcircuit.getGateMap() 1, 1, 2, 0
Subcircuit.getGates() 1, 1, 2, 0
Subcircuit.getId() 1, 1, 2, 0
Subcircuit.getInputNames() 1, 1, 2, 0
Subcircuit.getInputPin() 1, 1, 2, 1
Subcircuit.getInputPinMap() 1, 1, 2, 0
Subcircuit.getOutputNames() 1, 1, 2, 0
Subcircuit.getOutputPin() 1, 1, 2, 1
Subcircuit.getOutputPinMap() 1, 1, 2, 0
Subcircuit.Subcircuit() 1, 8, 2, 0
XnorGate.calculateOutput() 4, 8, 2, 5
XnorGate.XnorGate() 1, 1, 2, 1
XorGate.calculateOutput() 4, 8, 2, 5
XorGate.XorGate() 1, 1, 2, 1
Block Depth Statements
0 33
1 214
2 313
3 146
4 89
5 27
6 3
7 0
8 0
9+ 0
三、采坑心得
做这三次作业的过程中,踩过的坑其实比代码本身更值得记下来。有些问题当时折腾了好几个小时,回头一看其实挺简单的,但正是这些踩坑的过程让我对题目理解得更深。下面说说几个印象比较深的。
第一次作业:正则和排序的麻烦
最开始解析输入的时候,我对正则表达式掌握得不太好,上来就写了Pattern.compile("([A-Z])\((\d+)\)(\d+)"),想着匹配A(2)1这种格式。结果跑起来才发现,非门、异或门它们没有括号啊,人家叫X1、Y4,根本不匹配这个正则。当时图省事,又加了几个if去单独判断,搞得代码特别乱,后面要加新的门类型又得改一堆地方。后来干脆调整思路,先把有没有括号区分开,再统一解析,逻辑一下就顺了。
还有个排序的问题,题目要求输出顺序是有规定的,先按门的类型排,再按编号从小到大。我一开始没太在意,直接把门按解析顺序存进列表,结果跑测试的时候输出顺序全乱套了,跟样例对不上。后来自己写了个比较器,把五种门排了个优先级顺序,再按编号排序,问题才解决。
第二次作业:引脚映射乱得一塌糊涂
第二次作业加了很多新元件,像三态门、译码器这些,引脚编号的含义完全不一样。三态门的0号是控制端,1号是输入,2号才是输出;译码器更复杂,0到2是控制,3到5是输入,6到13全是输出。我在代码里到处写if (pinNum <= 2)这种硬判断,刚开始觉得挺管用,结果越到后面越乱。想加一个新元件的时候,得翻遍全代码去改那些判断,改完还容易漏掉什么地方。后来才想到用枚举加映射的方式,把每种元件的引脚和角色绑定起来,查的时候直接拿角色,不用再管具体是几号了。
三态门的高阻态也坑了我一下。题目说控制端为0的时候输出无效,我一开始用null表示,结果信号传下去的时候,有的门读到null直接崩了。后来改成枚举类型,定义LOW、HIGH、INVALID三种状态,所有门在计算之前先检查输入有没有无效的,有就输出无效,这样就不会崩了。
第三次作业:递归栈溢出和异常优先级
子电路的部分,一开始我是用递归去解析的,因为子电路可以嵌套,想着递归最自然。但实际测试的时候发现,嵌套到六七层就已经开始报栈溢出了。这种递归结构在数据量一大根本扛不住,后来我改成用栈来迭代处理,遇到C:就压栈,遇到endc就出栈放到父电路里,跑起来又快又稳。
异常检测部分也踩了个坑。题目里异常有好几种,优先级有先后。我刚开始是把所有异常都收集起来,最后再按优先级排序输出,结果跟题目的要求不一样——题目说碰到高优先级异常就要立刻停,不用管后面的。改完之后用测试样例跑了一下,这回对了。
输入引脚冲突这个检测也折腾了一阵子。要判断有没有两个不同的输出连到同一个输入上,我一开始没想好怎么存这个关系。后来把所有连接信息扫一遍,维护一个从输入引脚到输出引脚的映射,如果发现同一个输入被赋值了两次,就报冲突。逻辑不难,但写的时候容易漏掉一些边界情况。
总的来说,这些坑大部分都是因为对题目细节理解不到位,或者设计的时候没有提前想清楚数据结构。每次踩完坑把代码改好,后面就会顺畅很多。
四、改进建议
4.1 总体设计改进
建议1:采用工厂模式统一元件创建
目前创建不同元件时,无论是new AndGate()还是new DecoderGate()都在主类或解析器中显式调用。建议引入GateFactory:
public class GateFactory {
public static Gate createGate(String typeStr, String name, int pinCount) {
switch (typeStr) {
case "A": return new AndGate(name, pinCount);
case "O": return new OrGate(name, pinCount);
case "N": return new NotGate(name);
......
default: throw new IllegalArgumentException("Unknown gate type: " + typeStr);
}
}
}
这样新增元件时只需修改工厂类,主类保持稳定。
建议2:引入引脚管理器封装引脚操作
多次作业中引脚编号和角色的管理是反复出问题的点。觉得可以设计一个统一的PinManager:
public class PinManager {
private Map<Integer, PinRole> roleMap;
private Map<Integer, Pin> pins;
private int inputStart, inputCount;
private int controlStart, controlCount;
private int outputStart, outputCount;
public SignalState getInput(int index) { }
public void setOutput(SignalState state) { }
}
这个类封装了引脚编号到具体角色的映射,对外暴露高层次的get/set方法。
建议3:采用观察者模式处理信号传播
目前的信号传播是在计算时主动读取输入引脚的值,属于“拉”模式。可以改为“推”模式:当某个引脚信号变化时,主动通知所有连接的输入引脚。
java
public interface SignalObserver {
void onSignalUpdate(Pin source, SignalState state);
}
这样更适合处理复杂的反馈电路(后续迭代需求)。
4.2 针对各次作业的细节改进
第一次作业改进:
将输入解析逻辑拆分为独立的InputParser类,减少主类复杂度
增加拓扑排序确保元件计算顺序正确
第二次作业改进:
为复杂元件(译码器、数据选择器等)引入DecodeLogic和SelectLogic辅助类,分离计算逻辑与元件管理
使用枚举定义引脚角色而非硬编码编号
第四次作业改进:
异常检测引入策略模式,每种异常类型对应一个独立的ValidationStrategy实现类
子电路的信号传递可以考虑缓存机制,避免重复计算
五、总结
三次作业做下来,说句实话,过程挺折腾的,但回过头来看,收获也确实实实在在。最开始写第一次作业的时候,脑子里对面向对象其实没什么概念,就知道把功能写出来就行,能跑通测试就万事大吉。到了第二次,元件一下子多了起来,硬写已经撑不住了,被迫开始想怎么把代码组织得清楚一点。第三次再来个组合模式和异常检测,彻底把我逼着去思考设计层面的东西。一步一步走过来,感受最深的有几个方面。
对设计模式的理解变深了。 第一次作业基本就是继承那点东西,父类写个抽象方法,子类各自实现。第二次加了工厂模式,一开始觉得多此一举,后来发现新增元件的时候只改工厂一处确实省事。第三次用组合模式处理子电路,才真正体会到接口统一有多重要——主电路根本不用管操作的是普通门还是子电路,直接调同一个接口就好,特别省心。
数据结构的设计越来越重要。 一开始就是简单的列表存一存,后来引脚多了,有控制的有输入的有输出的,编号还乱,就意识到光靠硬编码不行。慢慢学会了用枚举、映射这些方式来管理,代码确实好维护多了。
异常处理教会我"先想坏情况"。 第四次作业的异常检测跟以前那种"输入一定合法"的假设完全不同,逼着我去想各种边界和非法情况。刚开始按自己的思路写,结果跟题目要求的优先级对不上,改了之后才明白,设计验证逻辑的时候得先弄清楚哪条规则说了算。
还有一个很大的收获,是学会了"不急着写代码"。 以前拿到题目第一反应就是开始写,写到一半发现结构不对又从头改。后面越来越体会到,先把类的职责分清楚、把数据怎么存想好,后面写起来反而快很多。
当然也有做得不够的地方。设计模式还只是会用几个基础的,像策略模式、观察者模式这些,还没真正在作业里用起来。代码注释率偏低,一些方法写得还是太长,拆分的粒度不够细。以后要多注意这些。
三次作业,每一版都比上一版写得顺手一些,虽然中间各种折腾,但每次改完、跑通测试的那一刻,还是有点成就感的。编程这回事,急不来,写了、错了、改了,慢慢就进步了。
5.3 个人成长与感悟
三次作业虽然只用了短短几周时间,但收获远超预期。从最初只会写if-else和简单类,到现在能够设计出具有一定架构水平的程序,这条路虽然艰辛但充满成就感。
特别感谢每次作业的互测环节,通过阅读和分析他人的代码,我学到了许多不同的设计思路和技巧。同时,被别人“找bug”的过程也让我深刻认识到,代码不仅要“运行正确”,更要“容易理解和维护”。
面向对象编程不仅是一种编程技术,更是一种思维方式的转变——从关注“如何做”到关注“做什么”,从“面向过程”到“面向对象”。期待在后续的课程学习中继续精进,成为一名合格的软件工程师。

浙公网安备 33010602011771号