作业4-6 数字电路模拟程序 总结性Blog
一、前言
本阶段共完成了三次迭代式编程作业,均围绕“数字电路模拟程序”这一核心主题展开,从最基础的逻辑门模拟,逐步扩展到多引脚组合元件、控制引脚,最终引入子电路机制与完善的异常处理体系。
三次作业概览
作业 | 知识点 | 题量 | 难度 |
|---|---|---|---|
作业4(电路模拟1) | 基础逻辑门(与/或/非/异或/同或)、引脚传播、拓扑排序 | 1题(含6个测试点) | ⭐⭐⭐ |
作业5(电路模拟2) | 新增三态门/译码器/数据选择器/数据分配器、控制引脚、多输出 | 1题(含10个测试点) | ⭐⭐⭐⭐ |
作业6(电路模拟3) | 子电路(组合模式)、异常检测(5类)、优先级处理 | 1题(含8个测试点) | ⭐⭐⭐⭐⭐ |
三次作业呈现明显的递进式迭代特征:代码量从最初的约 456 行膨胀至最终近 700 行,类数量从 7 个增加到 15+ 个,元件类型从 5 种扩展到 9 种,同时新增了子电路和异常处理两大模块。这让我深刻体会到了增量开发与软件复用在工程实践中的重要性。
二、设计与分析
2.1 作业4(数字电路模拟程序-1)—— 基础难度
2.1.1 类图设计

2.1.2 SourceMonitor 质量分析(基于实际报表数据)

指标 | 数值 |
|---|---|
总文件行数 | 456 |
可执行语句数 | 290 |
语句密度(Executable/Lines) | 63.6% |
分支语句占比 | 20.0% |
方法调用语句数 | 33 |
方法调用占比(Call/Statements) | 11.4% |
注释行占比 | 21.3% |
类和接口数 | 7 |
平均嵌套深度 | 1.68 |
最大嵌套深度 | 6~7(由柱状图推测) |
深度分布特征:
- 大部分代码块位于深度 0~2,结构相对扁平;
- 但存在深度达 6~7 的深层嵌套,主要出现在 CircuitParser.createElement 和 CircuitCalculator.getTopologicalOrder 等复杂方法中。
质量评价:
- 注释率 21.3% 接近行业推荐的 20%~30%,说明代码有一定文档化,但关键逻辑(如引脚传播、拓扑排序)仍缺少必要的解释性注释,对后续维护造成障碍。
- 分支语句占比 20.0% 表明代码中包含较多 if/switch 判断,逻辑分支丰富,但这也意味着测试覆盖需要更加全面。
- 最大嵌套深度达 6~7,过深的嵌套会降低代码可读性,并且容易引入逻辑错误,应当通过提取子方法进行扁平化重构。
- 语句密度 63.6% 表示非执行语句(空行、括号、注释等)占比较高,代码风格相对松散,虽有助于阅读,但也可能意味着冗余结构较多。
优化建议:
- 针对深度超过 4 的代码块(如 createElement 中的类型分支),将其抽取为独立的方法,降低嵌套层级;
- 为关键算法(如拓扑排序、信号传播)补充行内注释,说明设计意图;
- 考虑将 createElement 中的大 switch 重构为工厂模式,减少分支数量,提高可扩展性。
2.1.3核心设计分析
1. 引脚传播机制
Pin.propagateSignal() 是实现信号流动的核心。每个输出引脚维护一个 connectedPins 列表,当信号有效时,会遍历该列表将信号值赋给所有连接的输入引脚。这种观察者模式的变体使得信号传播变得直观:
public void propagateSignal() {
if (isOutput && signal != null) {
for (Pin targetPin : connectedPins) {
targetPin.setSignal(this.signal);
}
}
}
2. 拓扑排序计算顺序
CircuitCalculator.getTopologicalOrder() 使用 BFS 构建依赖图,解决了组合逻辑电路的计算顺序问题:
- 每个元件是一个节点
- 若元件A的输出连接到元件B的输入,则存在 A→B 的依赖边
- 入度为0的元件(无依赖)先计算

3. 存在的问题
- 依赖关系不够健壮:仅通过 connectedPins 反向查找依赖,当存在复杂网络时可能遗漏
- 固定10次迭代:若电路链路过长(超过10级),可能计算不完整
- 缺乏异常处理:输入格式错误时程序直接崩溃
- 2.2 作业5(数字电路模拟程序-2)—— 高级难度
- 2.2.1类图设计(重构后)

2.2.2 SourceMonitor 质量分析(基于实际报表数据)

指标 | 数值 |
|---|---|
总文件行数 | 930 |
可执行语句数 | 477 |
语句密度(Executable/Lines) | 51.3% |
分支语句占比 | 21.2% |
方法调用语句数 | 61 |
方法调用占比(Call/Statements) | 12.8% |
注释行占比 | 21.0% |
类数量 | 9 |
平均方法数/类 | 28.0 |
平均圈复杂度 | 2.00 |
最大圈复杂度 | 8 |
平均嵌套深度 | 1.31 |
最大嵌套深度 | 6 |
深度分布(Block Histogram):
嵌套深度 | 语句数量 |
|---|---|
0 | 142 |
1 | 174 |
2 | 94 |
3 | 32 |
4 | 17 |
5 | 11 |
6 | 7 |
7+ | 0 |
从深度分布可以看出,86% 的语句位于深度 0~2,整体控制流较为扁平。深度 3~6 的语句合计仅占约 14%,且最大深度为 6,说明代码在结构控制方面表现良好,没有出现作业4中深度 6~7 的极深嵌套问题。
质量评价:
- 文件规模(930 行/477 语句):相比作业4(456 行/290 语句)增长了约一倍,与新增的 4 种元件类型(三态门、译码器、数据选择器、数据分配器)以及配套的引脚管理、输出格式化逻辑相匹配。规模增长合理,但已接近单文件的可维护性上限。
- 模块化程度(9 个类):每个具体门类型独立成类(AndGate、OrGate 等),体现了良好的单一职责意识。但 CircuitCalculator 类同时承担了拓扑排序、信号传播和结果输出三项职责,有进一步拆分的空间。
- 方法分布(平均 28.0 方法/类):该指标偏高,说明部分类(尤其是 Main 和 CircuitCalculator)包含了过多方法。结合平均方法长度仅 1.86 个语句来看,存在大量“一次性”的辅助方法,虽然提高了可读性,但也增加了类的复杂度和方法调用的开销。
- 圈复杂度(最大 8,平均 2.00):最大圈复杂度 8 低于警戒线 10,平均复杂度 2.00 表明大部分方法逻辑清晰、分支简单。相比作业4的 12 点最大圈复杂度有了明显改善,说明抽象类 + 子类策略有效降低了单个方法的决策密度。
- 注释率 21.0%:与作业4(21.3%)基本持平,维持在可接受水平。但考虑到作业5新增了译码器控制逻辑、数据选择器选择算法等较复杂的业务规则,建议在关键算法处补充设计意图说明。
- 分支语句占比 21.2%:略高于作业4(20.0%),主要来源于新增元件类型各自的 calculateOutputLogic 中的条件判断。该比例仍处于合理范围。
优化建议:
- 补充关键注释:为译码器的控制条件(S1=1, S2+S3=0)和数据选择器的选择算法添加说明性注释,帮助后续维护者理解设计意图。
- 合并过于零碎的方法:识别那些仅被调用一次且逻辑高度相关的辅助方法,按功能相关性进行合并,降低方法总数,提高内聚性。
- 重构过大的类:将 CircuitCalculator 拆分为 TopologicalSorter、SignalPropagator 和 ResultFormatter 三个独立类,遵循单一职责原则。
- 增强异常处理:当前对于非法输入(如不存在的引脚编号、格式错误的元件名)仍采用 throw new IllegalArgumentException 直接终止,建议增加更细粒度的错误恢复机制。
核心设计改进
1. 抽象类 + 策略模式
将 CircuitElement 提升为抽象类,每种元件作为独立子类实现 initPins() 和 calculateOutputLogic()。这完美遵循了开闭原则——新增元件类型无需修改已有代码。
以三态门为例:
class TriStateGate extends CircuitElement {
@Override
protected void calculateOutputLogic() {
boolean control = controlPins.get(0).getSignal();
if (control) {
outputPins.get(2).setSignal(inputPins.get(1).getSignal());
} else {
outputPins.get(2).setSignal(null); // 高阻态
}
}
}
2. 引脚类型分离
引入 Pin.PinType 枚举(INPUT / OUTPUT / CONTROL),使引脚的角色更加清晰。特别是对译码器这类多引脚元件,引脚编号分配变得可预测:
// 译码器 M(3)1 引脚布局
// 控制引脚: 0,1,2
// 输入引脚: 3,4,5
// 输出引脚: 6~13
3. 输出格式多样化
不同元件有不同的输出要求:
- 基本门:A(2)1-0:1
- 译码器:M(3)1:3(输出0的引脚编号)
- 数据分配器:F(3)1:-1------(无效状态用"-"表示)
为此,CircuitCalculator.printElementResult() 使用了多态分发,根据元件类型调用不同的格式化方法。
设计缺陷
1. 引脚编号硬编码风险
各子类中引脚编号是硬编码的,例如 Decoder.initPins() 中:
controlPins.put(0, new Pin(...));
controlPins.put(1, new Pin(...));
controlPins.put(2, new Pin(...))
若需求变更(如引脚顺序调整),需要修改所有相关子类,违反了迪米特法则。
2. 数据选择器前置检查逻辑偏弱
Mux.areAllPinsReady() 只检查了将要被选择的那个输入引脚是否有信号,但题目要求是“所有控制引脚有效即可”,两者存在偏差。
2.3 作业6(数字电路模拟程序-3)—— 超高难度
本次作业在已有基础上新增了子电路和异常检测两大功能,是三次作业中改动最大、最复杂的一次
设计思路:组合模式
根据题目设计建议,采用组合模式将子电路和元件统一视为“电路部件”:
2.3.1题目提示的类图:

2.3.2设计的类图如下:

类图设计说明
1. 核心结构(Scope 体系)
- Scope 接口:定义电路或子电路的输入输出签名,用于统一处理连接校验时的角色识别。
- SubCircuit:子电路定义,存储本地名称、输入/输出信号名、原始连接语句。
- MainCircuit:主电路,固定包含一个 OUT 输出,输入来自 INPUT: 行。
2. 连接表示
- ConnectionLine:原始连接行 [...],记录文本和所属 Scope。
- SignalPath:经过展开后的单源-多目标路径,用于信号传播模拟。
3. 门实例管理
- GateInstance:表示一个具体的逻辑门(如 A(2)1),包含限定名、本地名、前缀、类型、输入引脚数、输出引脚和输入引脚列表。
- GateEvaluator:函数式接口,实现各门类型的真值表计算。
- EvaluatorFactory:工厂类,注册并提供对应的评估器。
4. 辅助解析类
- GatePinRef / SubmodulePinRef:分别用于解析门引脚和子电路引脚引用,存储解析后的结构化信息。
- PinRole 枚举:标识一个 token 在连接中是源(SOURCE)还是目标(DESTINATION)。
- ValidationResult:封装校验结果及错误信息。
5. 静态工具类
- Parser:提供引脚解析、门识别等静态方法。
- Validator:实现连接合法性校验(包括五类异常检测)。
- Discovery:递归展开子电路引用,建立前缀映射。
- Helper:提供排序比较等辅助方法。
6. 主控类 Main
- 聚合所有子电路、主电路、输入映射、连接行列表。
- main 方法驱动整个模拟流程:解析 → 校验 → 展开 → 传播 → 输出。
2.3.3 SourceMonitor 质量分析(基于实际报表数据)

核心实现分析
1. 子电路展开机制
Discovery.scan() 递归遍历所有子电路引用,构建扁平化的命名空间:
public static void scan(String prefix, List<String> rawConns,
Map<String, SubCircuit> submodules,
Map<String, String> prefixMap) {
for (String conn : rawConns) {
// 提取所有 Cx-xxx 格式的引用
// 为每个子电路实例分配唯一前缀(如 "C1-")
// 递归展开子电路内部连接
}
}
这样,主电路中的 C1-A 在展开后可能变成 C1-N1-1 这样的具体引脚路径。
2. 五类异常检测(按优先级)
优先级 | 异常类型 | 检测条件 |
|---|---|---|
1 | 包含多个输入 | sources > 1 |
2 | 无输入 | sources == 0 |
3 | 无输出 | destinations == 0 |
4 | 输入输出顺序错误 | roles.get(0) != SOURCE |
5 | 输入信号冲突 | 同一目标引脚被多个源连接 |
Validator.checkConnection() 按顺序检查,返回第一个匹配的异常。
3. 信号冲突检测
使用 targetToOriginMap 记录每个目标引脚的唯一源:
if (targetToOriginMap.containsKey(qualifiedDst)) {
if (!prevSrc.equals(qualifiedSrc)) {
return "ERROR: " + qualifiedDst + " input signal conflict";
}
}
存在的问题
1. 主类过于臃肿
Main 类包含了:
- 输入解析
- 子电路管理
- 异常校验
- 信号传播
- 门实例管理
- 排序与输出
这导致代码难以理解和维护。应该将不同职责拆分到独立的类中。
2. 异常优先级处理不完整
题目要求“多条输入都包含异常,只处理排在最前面的异常信息”,但当前实现是逐条处理连接信息,一旦遇到异常就立即返回,没有实现“全局扫描→收集异常→排序→输出最高优先级”的逻辑。
3. 子电路命名空间冲突风险
使用字符串拼接构建前缀(如 "C1-N1-0"),当子电路嵌套层级加深时,字符串解析变得复杂且容易出错。
三、采坑心得
坑点1:多源连接同一输入引脚检测失效
问题场景:
INPUT: A-1 B-1
[A A(2)1-1]
[B A(2)1-1] // 同一引脚被连接两次
原因分析:
在 CircuitParser.parseConnection() 中,使用 allPins.containsKey(pinIds[i]) 判断引脚是否已存在。但 allPins 在第一次 getOrCreatePin() 时就已经存入了该引脚,第二次连接时会直接触发 IllegalArgumentException。
修复方案:
应当将“引脚已存在但来自不同源”与“引脚已存在但来自相同源”区分开。增加一个 sourceMap 记录每个输入引脚的来源:
Map<String, String> inputSourceMap = new HashMap<>();
// 在解析连接时记录
if (inputSourceMap.containsKey(targetPinId)) {
if (!inputSourceMap.get(targetPinId).equals(sourcePinId)) {
// 冲突!
}
}
坑点2:三态门输出无效时信号传播链断裂
问题场景:
text
INPUT: C-0 D-1
[C S1-0] // 控制端为0
[D S1-1] // 输入端为1
[S1-2 OUT] // 输出端应该无效(高阻态)
预期行为:三态门输出无效,后续连接的元件无法计算,输出结果中不包含该路径上的任何元件。
原因分析:
在 TriStateGate.calculateOutputLogic() 中,当控制端为低电平时,我将输出引脚信号设为 null。但 Pin.propagateSignal() 中:
java
public void propagateSignal() {
if (signal != null) { // 信号为null时不传播
for (Pin targetPin : connectedPins) {
targetPin.setSignal(this.signal);
}
}
}
当 signal == null 时,不会将null传播给连接的引脚,因此下游元件的输入引脚仍然保持 null,不会被标记为“已接收无效信号”,导致下游元件始终处于等待状态。
反思:应该将 null 也作为一种“有效状态”传播,或者引入 SignalState 枚举(HIGH / LOW / INVALID / UNKNOWN)来更精确地表示信号状态。
坑点3:数据选择器前置检查过度严格
问题场景:
text
INPUT: S0-1 S1-0 D0-1 D1-1 D2-1 D3-1
[S0 Z(2)1-0]
[S1 Z(2)1-1]
// D0~D3 部分未连接
预期:只要控制引脚有效,就能选择一路输出。未被选择的输入引脚不需要信号。
错误实现:
在 Mux.areAllPinsReady() 中检查了所有输入引脚是否都有信号:
java
for (Pin pin : inputPins.values()) {
if (pin.getSignal() == null) return false;
}
这导致只要有一个输入引脚未连接,整个数据选择器就无法工作,与题目要求不符。
修正方案:
java
@Override
protected boolean areAllPinsReady() {
// 只检查控制引脚
for (Pin pin : controlPins.values()) {
if (pin.getSignal() == null) return false;
}
// 只检查被选中的输入引脚
int selectIndex = getSelectIndex();
List<Pin> sortedInputs = getSortedInputs();
return sortedInputs.get(selectIndex).getSignal() != null;
}
坑点4:译码器输出编号映射错误
问题场景:
对于3-8线译码器,输入 000 应该让 Y0 输出0,Y1~Y7 输出1。
错误实现:
我按照引脚编号顺序(6~13)直接映射,但未考虑输出引脚编号与输出位置的对应关系。在 printDecoderOutput 中:
java
for (int i = 0; i < outputPins.size(); i++) {
if (!outputPins.get(i).getSignal()) {
System.out.printf("%s:%d\n", name, i);
}
}
这里的 i 是列表索引,而非引脚编号。当输出引脚编号不连续或不是从0开始时,会产生错误映射。
正确做法:应该使用引脚编号与输出位置的关系进行映射:
java
int outputIndex = code; // 输入编码值
// 输出引脚编号与输出位置对应:引脚编号 = baseOffset + outputIndex
坑点5:子电路嵌套时前缀处理混乱
问题场景:
text
C1: INPUT: A OUT: B [A N1-1] [N1-0 B] endc
C2: INPUT: A OUT: B [A C1-A] [C1-B B] endc // C2嵌套C1
INPUT: X-1 [X C2-A] end
错误现象:C2-C1-N1-0 的前缀解析为 C2-C1-,但子电路查找时只识别 C1,导致无法找到对应定义。
原因:前缀字符串 "C2-C1-" 在 activePrefixes 中查找时,key 是 "C1-",无法匹配。
修正:改用树形结构存储子电路实例关系,而非扁平化的字符串前缀。
坑点6:异常优先级处理未达标
题目要求:如果一条输入出现了多种异常,按优先级输出最高的异常。
我的实现是逐个连接行校验,遇到异常立即输出并终止:
java
for (ConnectionLine connLine : originalConnections) {
ValidationResult outcome = Validator.checkConnection(...);
if (!outcome.isOk()) {
System.out.println(outcome.getErrorMsg());
return; // ❌ 直接返回,无法处理同一条连接有多个异常的情况
}
}
改进方案:
- 先遍历所有连接行,收集所有异常信息
- 对每个连接行,根据优先级选择最高优先级的异常
- 如果多个连接行都有异常,选择最先出现的行
java
List<ValidationResult> allErrors = new ArrayList<>();
for (ConnectionLine connLine : originalConnections) {
ValidationResult result = Validator.checkConnection(connLine, submodules);
if (!result.isOk()) {
allErrors.add(result);
}
}
if (!allErrors.isEmpty()) {
// 按优先级排序后输出第一个
System.out.println(allErrors.get(0).getErrorMsg());
}
四、改进建议
建议1:引入拓扑排序算法
当前使用固定10次迭代的近似方法,存在计算不完整的风险。建议引入Kahn算法进行真正的拓扑排序:
java
public List<CircuitElement> topologicalSort() {
// 1. 构建邻接表
// 2. 计算入度
// 3. BFS/DFS 输出排序结果
// 4. 检测环(若排序结果数量 < 元件数量,则存在环)
}
建议2:使用异常类体系替代字符串错误返回
当前大量使用 IllegalArgumentException 和字符串拼接错误信息,不利于错误分类和处理。建议:
java
class CircuitException extends Exception { ... }
class ConnectionException extends CircuitException {
private final ConnectionLine line;
private final ErrorType type;
}
class ConflictException extends CircuitException { ... }
建议3:拆分Main类
Main 类目前承担了过多职责,建议拆分为:
text
├── CircuitSimulator // 主控类
├── SubcircuitManager // 子电路管理
├── ConnectionValidator // 连接校验
├── GateInstanceFactory // 门实例工厂
├── SignalPropagator // 信号传播引擎
├── ResultFormatter // 结果格式化
└── Main // 仅包含入口方法
建议4:引入访问者模式处理输出格式化
不同元件的输出格式差异较大,使用 switch 分支会随着元件类型增加而膨胀。可使用访问者模式:
java
interface OutputVisitor {
void visit(AndGate gate);
void visit(Decoder gate);
void visit(Demux gate);
// ...
}
建议5:优化子电路命名空间管理
用树形结构替代字符串拼接:
java
class NamespaceNode {
String name; // "C1"
NamespaceNode parent; // 父节点
Map<String, NamespaceNode> children;
SubCircuit definition; // 对应的子电路定义
Map<String, Pin> pinMap; // 引脚映射
}
建议6:增加单元测试
针对异常检测、信号传播、门逻辑计算等核心模块编写单元测试(JUnit),确保每次迭代不破坏已有功能:
java
@Test
public void testDecoderOutput() {
// 测试3-8译码器输入000应输出Y0=0
}
@Test
public void testTriStateDisabled() {
// 测试三态门控制端为0时应输出无效
}
五、总结
学到的知识与能力
1. 面向对象设计原则的实践
- 开闭原则:通过抽象类 CircuitElement,新增门类型时无需修改已有代码
- 单一职责原则(虽然后期有所违背,但早期设计有体现):CircuitParser 只负责解析,CircuitCalculator 只负责计算
- 组合模式:在子电路设计中,将电路视为“叶子”和“组合”的统一结构
2. 数据结构的应用
- 图论:拓扑排序解决电路依赖计算顺序
- 哈希映射:高效管理引脚与元件实例
- 队列:BFS 实现拓扑排序
3. 复杂业务建模能力
- 从5种元件扩展到9种,从单一输出到多输出/无效输出
- 引脚类型从2种扩展到3种(输入/输出/控制)
- 从单层电路扩展到多层嵌套子电路
4. 异常处理思维
- 5类异常检测与优先级排序
- 输入验证前置 vs 运行时检查
5. 代码演进能力
- 三次迭代代码量从456行→930行→700行(优化后)
- 在新增功能的同时保持核心逻辑稳定
需要进一步学习的方向
1. 设计模式深化
- 访问者模式:用于解决不同元件输出格式差异化问题
- 工厂模式:元件实例化逻辑目前散落各处,可用工厂统一管理
- 状态模式:引脚信号状态(HIGH/LOW/INVALID/UNKNOWN)用状态模式更优雅
2. 软件工程实践
- 单元测试:TDD 开发方法
- 重构技巧:如何安全地拆分巨型类
- 代码审查:自我审查与互查能力
3. 算法优化
- 拓扑排序的正确实现与环检测
- 信号传播的稳定性(避免无限传播)
- 复杂电路的仿真步长控制
4. 异常处理框架
- Java 异常体系(Checked vs Unchecked)
- 自定义异常层次设计
- 全局异常处理器
5. 业务理解
- 数字电路基础知识(组合逻辑 vs 时序逻辑)
- 硬件描述语言(Verilog/VHDL)的基本思想
- 仿真器的核心概念(事件驱动仿真)
写在最后
这三次作业让我深刻体会到:“能用”和“好用”之间隔着十万八千里。
如果说有什么最想分享的,那就是:先写对,再写好,最后再写快。不要为了“设计模式”而设计模式,也不要为了“代码简洁”而牺牲可读性。在工程的世界里,平衡才是最高的艺术。
浙公网安备 33010602011771号