作业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% 表示非执行语句(空行、括号、注释等)占比较高,代码风格相对松散,虽有助于阅读,但也可能意味着冗余结构较多。

优化建议

  1. 针对深度超过 4 的代码块(如 createElement 中的类型分支),将其抽取为独立的方法,降低嵌套层级;
  2. 为关键算法(如拓扑排序、信号传播)补充行内注释,说明设计意图;
  3. 考虑将 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 中的条件判断。该比例仍处于合理范围。

优化建议

  1. 补充关键注释:为译码器的控制条件(S1=1, S2+S3=0)和数据选择器的选择算法添加说明性注释,帮助后续维护者理解设计意图。
  2. 合并过于零碎的方法:识别那些仅被调用一次且逻辑高度相关的辅助方法,按功能相关性进行合并,降低方法总数,提高内聚性。
  3. 重构过大的类:将 CircuitCalculator 拆分为 TopologicalSorter、SignalPropagator 和 ResultFormatter 三个独立类,遵循单一职责原则。
  4. 增强异常处理:当前对于非法输入(如不存在的引脚编号、格式错误的元件名)仍采用 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; // ❌ 直接返回,无法处理同一条连接有多个异常的情况

}

}

改进方案

  1. 先遍历所有连接行,收集所有异常信息
  2. 对每个连接行,根据优先级选择最高优先级的异常
  3. 如果多个连接行都有异常,选择最先出现的行

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)的基本思想
  • 仿真器的核心概念(事件驱动仿真)

写在最后

这三次作业让我深刻体会到:“能用”和“好用”之间隔着十万八千里。

如果说有什么最想分享的,那就是:先写对,再写好,最后再写快。不要为了“设计模式”而设计模式,也不要为了“代码简洁”而牺牲可读性。在工程的世界里,平衡才是最高的艺术。

posted @ 2026-06-23 19:15  让烟丝燃一会  阅读(9)  评论(0)    收藏  举报