一、前言
本阶段的三次作业是一个完整的迭代开发过程,主题围绕数字电路模拟展开。三次作业的知识点、题量和难度呈现明显的阶梯式递进:
| 作业 | 核心知识点 | 题量 | 重点考察 |
|---|---|---|---|
| 第一次 | 基本门电路的模拟 | 中等 | 类的设计、接口实现 |
| 第二次 | 引入三态门、解码器、多路复用器等复杂组件 | 较大 | 工厂模式、组件扩展 |
| 第三次 | 子电路定义与嵌套、连接验证、信号传播 | 大 | 递归解析、图论遍历、错误处理 |
三次作业环环相扣,从最简单的门电路模拟开始,逐步引入更复杂的组件类型,最后实现了完整的子电路嵌套系统。这个过程不仅锻炼了Java编程能力,更重要的是培养了面向对象的设计思维。
二、设计与分析
2.1 第一次作业:基本门电路模拟
设计思路:
第一次作业要求模拟五种基本门电路(AND、OR、NOT、XOR、XNOR)。我采用了接口+实现类的设计模式每个门电路类都实现了setPin()和getOutput()方法,通过引脚编号管理输入值。值得一提的是,我引入了缓存机制(cachedOutput和computed字段),只有在输入发生变化时才重新计算输出,这在后续复杂电路的模拟中能显著提升效率。
SourceMonitor分析:
| 类 | OCavg | WMC |
|---|---|---|
| AndGate | 2.0 | 8 |
| OrGate | 2.0 | 8 |
| NotGate | 1.8 | 7 |
| XorGate | 2.2 | 9 |
| XnorGate | 2.2 | 9 |
| Main | 4.5 | 36 |
可以看到,各个门电路类的圈复杂度控制得很好,平均复杂度在2.0左右。但Main类的simulate()方法复杂度较高(v(G)=12),因为它承担了信号传播的核心逻辑。
UML类图:

测试用例:

心得:
第一次作业让我深刻理解了接口的作用——它的统一使得后续扩展新的组件类型变得非常容易。同时,缓存机制的设计也让我认识到,好的设计不仅要考虑功能正确性,还要考虑性能。
2.2 第二次作业:引入复杂组件
设计思路:
第二次作业新增了三态门(TriState)、解码器(Decoder)、多路复用器(Mux)和解复用器(Demux)。这些组件的输入输出逻辑比基本门电路复杂得多:
-
TriState:有控制引脚C和数据引脚IN,当C=1时输出IN,否则高阻态
-
Decoder:有3个控制引脚和n个地址引脚,输出选中的通道号
-
Mux:有k个控制引脚和2^k个数据引脚,根据控制信号选择一路输出
-
Demux:有k个控制引脚和1个数据输入,将数据分发到2^k个输出通道
为了统一管理这些组件的创建,我引入了工厂模式
UML类图:

测试用例:

心得:
工厂模式的使用让Main类与具体组件类解耦,新增组件类型时只需修改工厂类,符合开闭原则。但同时我也发现,Decoder和Mux的setPin()方法逻辑复杂(需要区分控制引脚和数据引脚),这提示我应该在设计引脚映射时更加规范化。
2.3 第三次作业:子电路与系统集成
设计思路:
第三次作业是前两次的集成,要求支持子电路定义和嵌套。这是最复杂的一次,涉及:
-
子电路解析:解析
Cid: INPUT:... OUT:... [连接] endc格式 -
连接验证:检查每个连接是否有且仅有一个输入(驱动源),至少一个输出
-
信号冲突检测:确保每个接收端只被一个信号驱动
-
子电路展开:将子电路内部的连接展开到主电路中
-
信号传播:基于图遍历的BFS算法
UML类图:

测试用例:

SourceMonitor分析:
| 类 | OCavg | WMC | 说明 |
|---|---|---|---|
| CircuitSimulator | 4.8 | 58 | 主控类,复杂度合理 |
| SubCircuit | 2.5 | 10 | 子电路类,清晰简洁 |
| Connection | 1.0 | 2 | 纯数据类 |
| CircuitComponent | 3.2 | 16 | 组件类,包含解析和计算 |
| 方法 | ev(G) | iv(G) | v(G) | 说明 |
|---|---|---|---|---|
| parseSubcircuits() | 5 | 8 | 10 | 子电路解析,分支较多 |
| parseMainCircuit() | 4 | 7 | 9 | 主电路解析 |
| CircuitComponent.parse() | 8 | 10 | 12 | 组件名称解析,复杂度最高 |
| propagateSignal() | 3 | 6 | 7 | 信号传播,逻辑清晰 |
| checkSingleConnError() | 4 | 5 | 6 | 连接验证 |
心得:
第三次作业让我深刻体会到复杂系统的挑战。通过重构,我将功能拆分到四个类中,使得代码更加清晰。但CircuitSimulator类仍然承担了过多职责(58个方法),未来可以进一步拆分。同时,CircuitComponent.parse()方法的圈复杂度高达12,说明组件名称的解析逻辑过于复杂,可以考虑使用策略模式或状态模式来简化。
三、采坑心得
3.1 坑一:引脚编号的误解
问题描述:
在第一次作业中,我最初认为组件的输出引脚编号是1,但实际上是0。这导致信号传播时找不到输出。
错误代码:
// 错误:输出引脚编号为1
cachedOutput.add(name + "-1:" + out);
正确代码:
// 正确:输出引脚编号为0
cachedOutput.add(name + "-0:" + out);
教训: 仔细阅读题目要求,特别是引脚编号规范。
3.2 坑二:子电路展开时的命名冲突
问题描述:
在第三次作业中,展开子电路时,我最初直接使用了子电路内部的引脚名,导致与主电路中的同名引脚冲突。
错误做法:
// 直接使用子电路内部引脚名
newPins.add(pin); // 可能与其他子电路的引脚同名
正确做法:
// 添加子电路ID前缀
newPins.add("C" + cid + "-" + pin);
3.3 坑三:信号传播的死循环
问题描述:
在实现信号传播时,我最初没有检测循环依赖,导致在某些电路中陷入死循环。
错误代码:
while (!queue.isEmpty()) {String u = queue.poll();for (String v : adj.get(u)) {if (!signal.containsKey(v)) {signal.put(v, uVal);queue.add(v); // 可能无限循环}}
}
正确做法:
while (!queue.isEmpty()) {String u = queue.poll();for (String v : adj.get(u)) {if (!signal.containsKey(v)) {signal.put(v, uVal);// 只有新赋值的节点才加入队列if (pinToComp.containsKey(v)) {// 检查组件输入是否全部就绪// ...}queue.add(v);}}
}
这种情况下,A(2)1的两个输入都来自a,不会形成循环。但如果设计不当,可能会出现A->B->A的循环。
教训: 图遍历时必须有访问标记,防止重复处理。同时,对于组合逻辑电路,不应该有循环依赖。
3.4坑四:连接验证的边界条件
问题描述:
在验证连接时,我最初没有考虑[pin]这种只有一个引脚的情况。
错误:
[pin] // 只有一个引脚,既无输入也无输出
正确验证:
if (pins.size() < 2) {// 至少需要两个引脚:一个驱动源,一个接收端System.out.println("ERROR: " + connStr + " invalid connection");return true;
}
教训:需要全面考虑各种异常情况。
四、改进建议
4.1 增强错误处理
当前代码中大量使用了try-catch吞掉异常,这不利于调试:
try {// 可能出错的代码
} catch (Exception e) {// 什么都不做,静默失败
}
建议改为明确抛出有意义的异常:
try {// 可能出错的代码
} catch (NumberFormatException e) {throw new CircuitParseException("Invalid number format in: " + name, e);
}
4.2 引入设计模式
-
访问者模式:用于遍历电路结构,执行不同的操作(如验证、展开、传播)
-
观察者模式:用于信号变化时的通知,替代当前的轮询方式
-
建造者模式:用于构建复杂的子电路对象
-
策略模式:用于简化组件名称的解析逻辑
五、总结
通过这三次作业,我完成了从"面向过程"到"面向对象"的思想转变:
5.1 学到的知识
-
Java核心语法:接口、抽象类、继承、多态、泛型、集合框架
-
设计模式:工厂模式、单一职责原则、开闭原则
-
数据结构:图(邻接表)、队列(BFS)、哈希表
-
算法:拓扑排序(隐式)、信号传播算法
-
工程实践:代码度量、类图设计、单元测试
5.2 需要进一步学习的领域
-
设计模式:深入学习访问者模式、观察者模式等更复杂的模式
-
测试驱动开发(TDD):先写测试用例,再写实现代码
-
并发编程:多线程环境下的电路模拟
-
编译原理:词法分析、语法分析
5.3 感悟
三次作业的迭代过程,让我深刻理解了软件开发不是一蹴而就的。第一次作业可能只需要考虑功能正确性,第二次需要考虑扩展性,第三次则需要考虑系统的整体架构。每一次迭代都是对前一次的重构和提升。
正如我在第三次作业中所体会到的,当系统复杂度超过某个阈值时,良好的设计比高级的算法更重要。一个清晰的架构可以让后续的修改和扩展变得容易,而一个混乱的架构会让任何修改都变得复杂。
通过这三次作业,我也体会到了代码度量的重要性。SourceMonitor提供的圈复杂度、耦合度等指标,为后续的重构提供了方向。
最后,感谢这三次作业给我带来的成长。从最初对"面向对象"的懵懂理解,到现在能够设计出具有一定复杂度的系统,这个过程虽然艰辛,但收获是巨大的。未来的学习中,我将继续在代码质量和系统设计方面下功夫。
