第二次博客作业
一、前言
在本阶段的Java面向对象编程实践中,我完成了“数字电路模拟程序”的三次迭代开发。该系列作业要求模拟一个由逻辑门元件构成的数字电路系统,逐步增加功能:从基础逻辑门模拟,到组合电路元件与控制引脚,再到子电路封装与异常输入检测。三次作业环环相扣,既考察了面向对象设计能力,又锻炼了对复杂业务需求的分析与实现能力。
-
第一次作业:实现与门、或门、非门、异或门、同或门五种基本逻辑门,支持任意输入引脚数(与/或门),要求解析引脚连接关系并计算每个门的输出。题目输入较为规整,无异常检测,侧重正则解析和递归求值。
-
第二次作业:新增三态门、译码器、数据选择器、数据分配器四种元件,引入控制引脚概念,输出格式更加多样(译码器输出0对应的引脚编号,分配器输出带高阻符号的序列)。元件种类从5种扩展到9种,设计复杂度明显上升。
-
第三次作业:在基础门(仅五种)之上迭代,增加子电路定义与引用、五种异常输入检测(多输入、无输入、无输出、顺序错误、信号冲突)。要求采用组合模式将子电路视为复合元件,输出带子电路前缀。异常检测具有优先级,且仅报告最先出现的异常。
三次作业的题量逐渐增大,第三次作业代码量接近400行。难度曲线从线性解析增长到处理嵌套结构和组合模式,再到复杂的异常规则,每次都需要重新审视架构的可扩展性。下面我将详细分析每次作业的设计思路、源码结构,并分享踩坑经历和改进思考。
二、设计与分析
第一次作业:基础逻辑门电路
设计思路
第一次作业的核心任务是解析门引脚、建立连接关系、计算门输出。我采用了面向过程的函数式解析方法,将每个门抽象为一个包含类型、编号、输入引脚数、输入源列表的结构体。通过正则表达式匹配A(2)1-0等格式,使用HashMap保存目标引脚到源引脚的映射,最后通过递归求值函数得到最终输出。
类图

并没有明确的元件类层次,所有逻辑都耦合在Main类中。
SourceMonitor简要报表
| Method | ev(G) | iv(G) | v(G) |
|---|---|---|---|
| Main.main(String[]) | 5 | 8 | 11 |
| Main.getFullName(Matcher) | 1 | 2 | 2 |
| Main.registerGateIfOutput(String) | 2 | 2 | 3 |
| Main.registerGate(String) | 3 | 4 | 6 |
| Main.evalSource(String) | 3 | 4 | 5 |
| Main.evalGate(String) | 6 | 9 | 13 |
| Gate.Gate(char,int,int,String) | 1 | 1 | 1 |
| Class | OCavg | WMC | |
| Main | 6.67 | 40 | |
| Gate | 1.00 | 1 |
代码分析
-
正则表达式([AONXY])(?😦(\d+)))?(\d+)-(\d+)成功匹配各种门引脚,但对后续新增元件扩展性差。
-
递归求值使用computing集合检测环路,但门之间的依赖通过映射动态查找,存在重复计算,性能尚可。
-
排序输出时使用字符串"AONXY"定义类型顺序,手动比较,逻辑简单直接。
心得
初次实现时,我过于关注功能实现,忽视了代码的扩展性和可维护性。例如,当需求变为新增元件时,必须修改多处if-else分支。但从快速原型角度,该设计能快速验证逻辑正确性,为后续重构提供了基线。
第二次作业:多元件与控制引脚
设计目标
新增四种元件:三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F),每种元件的引脚含义不同,输出格式各异。例如译码器需输出“M(3)1:0”表示Y0有效,数据分配器需输出“F(2)1:--0-”表示高阻状态。
重构与类设计
为应对多样性,我引入了抽象类Gate,定义getInputPins()、getOutputPins()和calculate()三个抽象方法,并派生出BasicGate(门)、TriGate(三态门)、Decoder、Multiplexer、Demultiplexer等子类。这种方式将不同类型元件的引脚布局和计算逻辑封装在各自的类中,符合开闭原则。
类图

关键实现细节
-
引脚编号约定:三态门0控制、1输入、2输出;译码器0~2控制、3~(2+n)输入、之后为输出;选择器/分配器类似。在子类中通过循环构建引脚列表,确保getInputPins()返回正确排序。
-
计算有效性:calculate()会根据引脚连接完整性和控制条件决定valid标志。例如译码器要求S1=1且S2+S3=0才工作,否则valid=false,输出时忽略。
-
输出格式化:对Decoder,寻找输出为0的引脚相对偏移;对Demultiplexer,遍历输出引脚,未选中的设为-1,输出时'-'。
SourceMonitor简要报表
| Method | ev(G) | iv(G) | v(G) |
|---|---|---|---|
| Main.main(String[]) | 6 | 8 | 12 |
| Main.getFullName(Matcher) | 2 | 3 | 3 |
| Main.registerGate(String) | 4 | 6 | 9 |
| Main.evalSource(String) | 3 | 4 | 5 |
| Main.evalPin(String,int) | 3 | 4 | 6 |
| BasicGate.BasicGate(char,int,String,int) | 1 | 1 | 1 |
| BasicGate.getInputPins() | 1 | 1 | 1 |
| BasicGate.getOutputPins() | 1 | 1 | 1 |
| BasicGate.calculate(Map,Function) | 3 | 5 | 7 |
| BasicGate.allOne(int[]) | 1 | 1 | 1 |
| BasicGate.anyOne(int[]) | 1 | 1 | 1 |
| TriGate.TriGate(char,int,String) | 1 | 1 | 1 |
| TriGate.getInputPins() | 1 | 1 | 1 |
| TriGate.getOutputPins() | 1 | 1 | 1 |
| TriGate.calculate(Map,Function) | 2 | 3 | 5 |
| Decoder.Decoder(char,int,String,int) | 1 | 1 | 1 |
| Decoder.getInputPins() | 1 | 1 | 1 |
| Decoder.getOutputPins() | 1 | 1 | 1 |
| Decoder.calculate(Map,Function) | 4 | 6 | 8 |
| Decoder.getPin(Map,Function,int) | 2 | 2 | 2 |
| Multiplexer.Multiplexer(char,int,String,int) | 1 | 1 | 1 |
| Multiplexer.getInputPins() | 1 | 1 | 1 |
| Multiplexer.getOutputPins() | 1 | 1 | 1 |
| Multiplexer.calculate(Map,Function) | 3 | 4 | 7 |
| Demultiplexer.Demultiplexer(char,int,String,int) | 1 | 1 | 1 |
| Demultiplexer.getInputPins() | 1 | 1 | 1 |
| Demultiplexer.getOutputPins() | 1 | 1 | 1 |
| Demultiplexer.calculate(Map,Function) | 3 | 4 | 7 |
| Gate.Gate(char,int,String) | 1 | 1 | 1 |
| Gate.getInputPins() | 1 | 1 | 1 |
| Gate.getOutputPins() | 1 | 1 | 1 |
| Gate.calculate(Map,Function) | 1 | 1 | 1 |
| Class | OCavg | WMC | |
| Main | 4.60 | 46 | |
| Gate | 1.00 | 4 | |
| BasicGate | 2.17 | 13 | |
| TriGate | 1.75 | 7 | |
| Decoder | 2.75 | 11 | |
| Multiplexer | 2.25 | 9 | |
| Demultiplexer | 2.25 | 9 |
分支语句覆盖率:约85%
最大圈复杂度:evalSource()函数因需判断多种元件类型,圈复杂度达12
代码行数:约280行
心得
第二次作业让我深刻体会到面向对象设计在应对变化时的优势。通过抽象基类和工厂式注册(registerGate),新增元件仅需添加一个子类和相应的switch分支,对已有的基本逻辑门代码影响很小。但evalSource和evalPin等求值函数仍需维护一个巨大的类型判断链,这是由于计算逻辑与数据结构耦合过紧造成的。
第三次作业:子电路与异常处理
设计目标
在保留第一次作业的五种基础门基础上,增加子电路封装(可多层嵌套)和五种语法异常检测。题目明确建议使用组合模式,将子电路视为复合元件。输出格式要求带子电路前缀,如C1-A(2)1-0:0。
架构设计
我采纳了组合模式,核心类层次如下:

-
BasicGate作为叶节点,实现与第一次相同的逻辑;SubCircuitInstance作为组合节点,内部包含克隆的元件集合和连接关系。通过compute()递归驱动整个树的求值。
-
解析流程与异常检测优先级
-
首先提取所有子电路定义(C1: … endc),构建SubCircuitDef模板。
-
解析主电路输入、连接信息方括号。
-
异常检测:在构建连接前,遍历所有连接信息,按优先级顺序检测:
-
多个源(输出引脚) → ERROR: ... include more than one input
-
源数量为0 → ERROR: ... include none input
-
无输入引脚 → ERROR: ... include none output
-
源不在首位 → ERROR: ... input and output sequence error
-
遍历连接信息注册元件,同时检测引脚冲突(同一个目标引脚被不同源驱动)。
-
设置元件输入源,实例化子电路并建立端口映射,最后递归计算所有叶节点并排序输出。
关键改进点
-
outputMapping:子电路定义中,输出端口通过OUT:声明,内部连接信息中输出端口作为目标出现(例如[X1-0 C])。在构建SubCircuitInstance时,预先提取C对应的内部源(X1-0),存为outputMapping,当外部引用C1-C时,通过映射找到内部源并递归求值。
-
异常检测独立于电路构建,一旦发现错误立即输出并结束程序,符合“只处理排在最前面的异常”要求。
-
为避免子电路模板被修改影响其他实例,采用原型克隆:在每个SubCircuitInstance构造时,深拷贝templateGates和templateConn,并加上实例前缀(如C1-),实现命名空间隔离。
SourceMonitor简要报表
| Method | ev(G) | iv(G) | v(G) |
|---|---|---|---|
| Main.main(String[]) | 7 | 10 | 14 |
| Main.checkConnectionError(String) | 4 | 6 | 8 |
| Main.isSource(String) | 4 | 7 | 9 |
| Main.registerComponent(String,String) | 3 | 5 | 6 |
| Main.connect(String,String) | 2 | 3 | 4 |
| Main.evalSource(String) | 6 | 9 | 12 |
| Main.findComponent(String) | 3 | 4 | 5 |
| Main.findInSub(SubCircuitInstance,String) | 2 | 3 | 4 |
| Main.buildTemplateGates(SubCircuitDef) | 3 | 4 | 5 |
| BasicGate.BasicGate(String,char,int,int) | 1 | 1 | 1 |
| BasicGate.setInput(int,String) | 1 | 1 | 1 |
| BasicGate.compute() | 4 | 6 | 8 |
| BasicGate.getOutput() | 1 | 1 | 1 |
| BasicGate.getOutputPinName() | 1 | 1 | 1 |
| BasicGate.allOne(int[]) | 1 | 1 | 1 |
| BasicGate.anyOne(int[]) | 1 | 1 | 1 |
| SubCircuitInstance.SubCircuitInstance(String,SubCircuitDef,String) | 2 | 2 | 2 |
| SubCircuitInstance.connectInput(String,String) | 2 | 2 | 2 |
| SubCircuitInstance.compute() | 3 | 4 | 5 |
| SubCircuitInstance.getOutput() | 1 | 1 | 1 |
| SubCircuitInstance.getOutputPinName() | 1 | 1 | 1 |
| SubCircuitInstance.getAllGates() | 1 | 1 | 1 |
| SubCircuitDef.SubCircuitDef(String) | 1 | 1 | 1 |
| Component.Component(String) | 1 | 1 | 1 |
| Component.compute() | 1 | 1 | 1 |
| Component.getOutput() | 1 | 1 | 1 |
| Component.getOutputPinName() | 1 | 1 | 1 |
| Class | OCavg | WMC | |
| Main | 7.00 | 70 | |
| Component | 1.00 | 3 | |
| BasicGate | 2.33 | 14 | |
| SubCircuitInstance | 2.00 | 12 | |
| SubCircuitDef | 1.00 | 1 |
总行数:约380行
最大复杂度:evalSource方法圈复杂度18(需处理输入信号、门输出、子电路端口)
心得
组合模式的运用使代码结构清晰,子电路可以无限嵌套,求值过程统一。但在实现过程中,由于时间紧张,部分职责划分不够干净,例如Main类仍然承担了过多的解析和构建职责,导致其圈复杂度较高。如果进一步重构,可将解析器独立为Parser类,构建过程独立为CircuitBuilder。
三、踩坑心得
正则表达式与引脚分类
三次作业都要靠正则提取元件信息。早期我编写的正则([AONXY])(?😦(\d+)))?(\d+)-(\d+)无法匹配第二次新增的S、M、Z、F类型,导致解析失败。解决方案是将字符集扩展为[AONXYSZFM],并动态适应不同元件的引脚数捕获组。另一个坑在于输入信号名称可能包含数字(如IN1),原正则[A-Za-z]\w不够严谨,后来改为[A-Za-z_]\w才完全匹配样例。测试时一定要覆盖边界命名。
异常检测优先级与短路逻辑
第三次作业要求“如果一条输入出现了多种异常,按以上异常先后顺序为优先级,最后输出优先级最高的异常输出”。我最初实现时,依次检查所有条件,一旦发现错误就存储,最后统一输出。结果忽略了优先级的严格顺序,导致在同时存在多源和无输入时,可能输出较低优先级的错误。修正方案是严格按题目给出的顺序执行if-else链,一旦匹配立即返回,确保优先级。测试用例样例8([A(2)1-0 O(2)1-0]无输入同时多源)证实了正确性。
子电路命名空间与实例隔离
子电路克隆时,我直接引用了def.templateGates中的BasicGate对象,然后在多个SubCircuitInstance间共享同一对象,导致一个实例计算会污染另一个实例的inputSources。例如定义两个子电路C1和C2均使用N1,若不做深拷贝,修改C1的N1输入会影响C2。踩坑后,我在SubCircuitInstance构造时为每个模板门创建全新的BasicGate对象,并添加实例前缀(prefix)作为新全名,保证了隔离性。对应测试样例2(两个相同子电路)通过。
输出引脚映射与连接误导
子电路输出端口在定义中是作为连接信息的目标(如[X1-0 C]),但在主电路引用时C1-C是作为源。这种角色转换容易让人混淆。我最初在构建outputMapping时,错误地将端口名映射到端口名本身,导致求值死循环。后来通过追溯templateConn中该端口作为目标时的源,正确存储了内部源路径。该问题通过单步调试并绘制连接关系图得以发现。
递归求值与环路检测
递归求值存在潜在的栈溢出风险。虽然在作业环境中未出现环路,但我主动加入了computing集合,防止意外环路(如门输出反馈到自身输入)导致无限递归。在第三次作业的evalSource中,我加入了evaluating与evalCache,既避免重复计算,又防止环路,这一机制在所有测试中表现稳定。
四、改进建议
应用设计模式进一步解耦
虽然第三次作业应用了组合模式,但解析和构建部分依旧高度耦合在Main类中。建议将解析、构建、计算三大职责分离:
Parser:负责从文本提取子电路定义、顶层输入、连接信息,并进行异常检测。
CircuitBuilder:根据解析结果创建元件实例并建立连接。
Simulator:仅负责执行compute()并收集输出结果。
这样的分离符合单一职责原则,会使单元测试更加容易,也便于未来支持图形化界面或文件输入。
引入策略模式处理元件输出格式化
第二次作业中不同类型元件的输出格式差异很大(基本门引脚值、译码器编号、分配器高阻序列)。当前是将格式化逻辑写在main的输出循环中,通过instanceof判断。更好的做法是让每个元件重写formatOutput()方法,返回字符串,这样增加新元件时无需修改输出逻辑。例如:
java
abstract String formatOutput();
在BasicGate中返回fullName + "-0:" + output,在Decoder中返回fullName + ":" + activeLowPin,符合开闭原则。
异常检测模块化
目前的异常检测是顺序式if-else链,若未来增加更多异常类型会变得臃肿。可以考虑实现责任链模式,将每种异常检测封装为独立的ConnectionChecker对象,按顺序链接。这样不仅提高扩展性,还可以方便地动态调整优先级。
性能优化
当前求值过程中,findComponent采用遍历所有元件和子电路内部递归查找,时间复杂度较高。对于大型电路,可维护一个全局名称到Component的HashMap,在注册元件时直接添加(含子电路内部的元件),将查找降为O(1)。本次作业规模下虽无性能问题,但养成性能意识有备无患。
五、总结
通过本阶段三次数字电路模拟程序的迭代开发,我深刻体会到了面向对象设计在软件演化中的价值。
-
技术层面:掌握了正则表达式在复杂文本解析中的高级应用;熟练运用了组合模式解决树形结构问题;理解了如何通过抽象类和接口为系统构建可扩展的骨架。此外,异常处理的优先级设计、原型克隆解决实例隔离等具体技巧都是书本上学不到的宝贵经验。
-
过程层面:理解了敏捷开发中“简单设计—重构—扩展”的螺旋模型。第一次作业快速实现功能,第二次在原有基础上抽象出元件类体系,第三次引入设计模式进一步解耦,每次迭代都带来架构上的反思与优化。
-
测试与调试:在实现过程中,我学会了用边界值测试、等价类划分等方法验证解析器和异常处理逻辑,体会到了单元测试的重要性。虽然本次没有使用JUnit框架,但手动构造的大量测试用例(如样例1-8)保障了代码可靠性。
-
不足与改进方向:我在异常检测模块化、代码规范注释以及性能前瞻等方面仍有提升空间。未来将进一步学习设计模式、重构手法,并尝试使用UML工具进行更规范的建模。

浙公网安备 33010602011771号