南昌航空大学2025级学生面向对象程序设计作业集4-6总结
一、前言
数字电路是计算机与数字系统的物理基础,其核心思想是用“0”和“1”两种状态表示并处理信息。为了深入理解数字电路的工作原理,本次作业以“数字电路模拟程序”为题,通过三次迭代作业,逐步构建一个从简单逻辑门到层次化、容错性强的电路仿真系统。
第一次作业实现与门、或门、非门、异或门、同或门五种基本逻辑门。通过解析输入信号和连接关系,构建电路拓扑并计算各门输出,重点训练了字符串解析、有向图建模和面向对象设计,是后续迭代的基础。
第二次作业
在基础门电路上新增三态门、译码器、数据选择器和数据分配器,引入控制引脚和多输出引脚,输出格式也更为多样(如译码器输出有效引脚号、分配器输出电平串)。该次作业加深了对组合逻辑元件工作原理的理解,并锻炼了处理复杂引脚类型和无效状态的能力。
第三次作业
聚焦于子电路的定义与引用,支持将一部分电路封装为可复用的模块;同时增加了五类异常输入的检测与优先级处理,使程序具备模块化设计和健壮性保障。该次作业综合运用了组合模式、递归求值和错误处理机制。
1.1知识点分析
- 第一次作业
基本数字逻辑门:与门(A)、或门(O)、非门(N)、异或门(X)、同或门(Y)的真值表与功能。
元件命名规则:多输入门用“标识符(输入引脚数)+编号”,二输入门用“标识符+编号”。
连接信息解析:[输出引脚 输入引脚1 输入引脚2 ...] 格式,处理扇出(一个输出驱动多个输入)。
电路拓扑构建:根据连接关系建立有向图,并计算各元件输出。(题目已经给出)
输出排序:按元件类型(与、或、非、异或、同或)及编号升序输出有效元件的输出引脚电平。
未连接完整输入的元件忽略输出。
- 第二次作业
新增元件:三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F)。
引脚类型分类:三态门(控制、输入、输出),译码器(控制、输入、输出),数据选择器(控制、数据输入、输出),数据分配器(控制、数据输入、输出)。
引脚编号顺序规则:按“控制-输入-输出”分别排序。
普通门输出:元件名-0:电平
译码器输出:M(输入数)编号:有效输出引脚号(只输出为0的引脚编号)
数据分配器输出:F(控制数)编号:所有输出引脚电平串,无效状态用“-”表示。
元件命名扩展:数据选择器/分配器用标识符(控制引脚数)+编号,译码器用标识符(输入引脚数)+编号。
- 第三次作业
子电路引用:主电路中可将子电路视为一个元件,端口用 C编号-输入/输出名 引用。
子电路输出格式:子电路编号-元件名-引脚号:电平,同类元件按编号排序(全局)。
异常检测(共5种,优先级从高到低):
连接信息中包含两个或以上输出引脚 → ERROR: [连接信息] include more than one input(注意这里的“input”是相对于连接系统,指输出引脚)。
连接信息中没有输入引脚(即连接信息中除第一个外无其他引脚) → ERROR: [连接信息] include none input。
连接信息中没有输出引脚(即连接信息只有一个引脚或无引脚) → ERROR: [连接信息] include none output。
连接信息中输入输出顺序颠倒(即第一个引脚不是输出引脚,而是输入引脚) → ERROR: [连接信息] input and output sequence error。
同一输入引脚被多个不同输出驱动 → ERROR: 输入引脚 input signal conflict。
异常处理规则:每条连接信息只报告优先级最高的一种异常;多条连接信息中,只处理排在最前面的异常。
1.2题量及难度
-
第一次题目
题量:需实现5种门逻辑、解析约3种输入格式、构建元件对象图、拓扑计算。代码为346行,涉及字符串解析、集合映射、递归/迭代求值。
难度:中等。作为第一次迭代,其主要作用是为后续迭代的作业打下基础,题目给予的逻辑比较清晰,主要难点在于字符串解析和电路依赖关系的处理,没有复杂算法。顺利通过拿到满分。 -
第二次题目
题量:较第一次增加4种新元件,每种逻辑不同,需为每种实现求值函数;输出格式更复杂,需处理多输出引脚。代码775行。
难度:困难。元件种类相较第一次的作业明显增多,需要达到的要求也同样增多,包括引脚分类和输出格式多样化,需仔细处理控制引脚、无效态和多输出情形等,整体逻辑复杂度明显提升。只取得了85分,拼尽全力无法战胜,个人认为较难。 -
第三次题目
题量:在第一次基础上新增子电路定义与解析、端口映射、子电路内部求值;同时增加5类异常检测,需优先级排序。代码726行。
难度:困难。本次迭代与第二次的关联较小,依然是从第一次作业中进行迭代,子电路增加了递归计算和上下文管理,异常处理需精细设计优先级和错误报告,整体设计和调试难度较大。这次也只拿到了83分。
1.3做题感受
说实话,刚开始看到第一次作业的时候,我就感觉到了题目较往常的不同。之前的1-3次作业集的代码量都不过三百多行,结果这次第一次作业就来到了346行,并且刚上手的时候发现,给出的题目我都有点不太能理解,无从下手。开始尝试写才发现,难点根本不是门电路本身,而是怎么把那一堆字符串解析成有意义的电路结构。一边写一边对着题目反复确认格式,看的人眼花,写到后面更是要找自己把后面需要用到的类和方法定义到哪里去了,给我的第一感觉就是代码量大,其次才是复杂。
第二次作业直接给我上了一课。三态门、译码器、数据选择器、数据分配器,光记这些名字就够呛,更别说每种元件的引脚编号规则还不一样。译码器要输出“哪个引脚为0”,分配器要输出一串带横杠的电平串,还有新迭代加入的大量要求,写的时候已经彻底头晕了。
到了第三次,我已经做好心理准备了——子电路、异常检测、优先级排序,全是硬骨头。最折磨的是那五种异常情况,优先级还分先后,一条连接信息可能同时犯好几个错,只能取最前面那一个。写了好多异常处理逻辑,跑样例的时候发现顺序不对,又推倒重来。结果最终测试点还是没有完全通过。
通过这次作业,我才真正体会到程序设计的世界究竟有多大,我自认为学习到的那点鸡毛蒜皮,在真正的程序设计中还是完全不够看,我的那片认识完全只是冰山一角。 不过我同样认识到,虽然这次代码量确实大,给我心理以及身体上的双重折磨确实痛苦,但熬过来之后,会发现自己的工程能力和耐心都上了一个台阶。回头再看第一次作业,似乎也不是那么的难以接受,这次的迭代作业练习,虽然做题下来给我的总体感受是不好的,身心俱疲,改一个测试点改了一晚上才通过,但也让我收获良多,或许我们要成为一名合格的代码工程师,这些路都是必须要去走一走的。
二、设计与分析
2.1第一次作业集
SourceMonitor报表

行数/语句数:346行 / 205条语句。体量最小,符合基础逻辑门模拟的定位。
类与接口:11个,是所有版本中最多的。每个类平均5.91个方法,平均每个方法仅0.28条语句,数据低,结合最大复杂度3来看,说明方法拆分非常细碎,每个方法只做很少事情,虽然可读性好,但略有“过度拆分”之嫌。
分支语句占比24.4%:三次中最高,因为五种门的逻辑计算本身就需要大量 if/else 或 switch 分支。
最大嵌套深度9+:存在很深的条件嵌套,这个深度是三次中最高的,代码可维护性略受影响。
平均复杂度1.75,最大复杂度3:整体逻辑不复杂,最大复杂度的 calculate() 方法位于38行,表明此代码核心求值逻辑相对集中且清晰。
PowerDesigner类图

以 CircuitElement 接口为顶层规范,LogicGate 抽象类抽取公共属性(编号、引脚值、输出结果),具体门类(yuGate、huoGate、feiGate、tongGate)各自实现 calOutPrint() 完成真值表计算。主控类 CircuitSimulator 通过 Map 管理元件和连接关系,负责解析输入信号、构建电路并触发求值。
优缺点总结
- 优点
层次化设计清晰:
采用 CircuitElement 接口 → LogicGate 抽象类 → 具体门类的三层结构,职责分明,符合面向对象设计原则。
扩展性好
新增门类型只需继承 LogicGate 并实现 calOutPrint(),无需修改主控逻辑,体现了开闭原则。
逻辑复杂度控制得当
最大圈复杂度仅3,核心计算逻辑简洁,虽然代码总行数不多,但也保持了良好的可读性。
数据管理规范
使用 Map 和 List 统一管理元件、信号和连接关系,避免了硬编码数组,适应动态数量的元件。
- 缺点
命名不规范
门类使用拼音(yuGate、huoGate、feiGate),可读性差,不利于团队协作和后期维护。
耦合度偏高
CircuitSimulator 直接依赖具体门类的构造函数,增加新元件需修改主控代码,未引入工厂模式解耦。
硬编码问题
引脚编号规则(输出为0、输入从1开始)在代码中隐含,未通过常量或配置显式声明,扩展性受限。
方法拆分过细
平均每个方法仅0.28条语句,大量方法只有一行代码,存在过度工程化倾向,增加了类数量(11个)和方法调用开销。
嵌套深度偏大
最大块深度达9+,主要存在于连接解析部分,影响代码可维护性。
- 总结
作业集1是一个基础实现。它在有限的代码规模内,构建了清晰的分层架构和良好的扩展接口,为后两次迭代奠定了坚实的基础。虽然在命名规范、解耦程度和方法粒度上还有改进空间,但作为第一次课程作业在可用性方面还勉强合格,这次让我初步掌握了面向对象分析与设计的基本方法,也暴露了工程实践中需要注意的细节问题,后续应当多改进。
2.2第二次作业集
SourceMonitor报表

行数/语句数:775行 / 531条语句。相比第一次直接翻倍还多,因为新增了四种元件(三态门、译码器、数据选择器、数据分配器),且输出格式变得复杂。
类与接口:14个,略有增加,但每个类的方法数飙升至14.36(第一次只有5.91),平均每个方法仅0.14条语句,得出结论:方法拆分更加细致。
分支语句占比16.4%:反而比第一次低,原因是新增元件的逻辑多通过查表或序列化处理,分支占比下降。
最大复杂度4:仅比第一次高1,但最复杂方法 getOutputValue() 位于69行,说明核心计算逻辑较第一次更集中但没有爆炸式变复杂——得益于良好的方法拆分。
最大嵌套深度8:比第一次略降,说明代码在控制结构上有所优化。
PowerDesigner类图

第二次作业在保留 CircuitElement 接口与 LogicGate 抽象类的基础上,新增三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F)四种复杂元件。LogicGate 依然服务于五种基本门(继承关系),而复杂元件因引脚体系差异(多输入/多输出/控制引脚)直接实现 CircuitElement 接口,形成了继承与直接实现并存的“双轨”结构。主控类 CircuitSimulator 不再依赖 setInput 被动注入,而是通过 getPinValue() 主动拉取信号,并引入 pinCache 缓存与 computing 集合防止循环依赖;工厂类 LogicGateFinish 利用正则匹配统一解析元件命名并实例化对象。
- 优点
功能覆盖完整
成功实现了9种数字电路元件的模拟,包括三态门、译码器、数据选择器和数据分配器,全面覆盖了组合逻辑电路的常见组件,功能完整性较强。
信号传播机制健壮
CircuitSimulator.getPinValue() 作为求值中枢,支持外部输入查表、递归查询等,并引入 pinCache 缓存避免重复计算、computing 集合检测循环依赖,为复杂电路的稳定求值提供了可靠保障。
多输出统一抽象
getAllOutputs() 方法将译码器(多输出)、分配器(多输出)与普通门(单输出)统一为相同的接口,使得 Results() 输出遍历变得通用,降低了输出模块的复杂度。
工厂模式解耦
LogicGateFinish 使用6个正则表达式将元件名字符串映射到具体构造函数,主控类无需感知具体元件类型,扩展新元件时只需修改工厂类,符合开闭原则。
格式化输出灵活
不同元件实现了各自的 formatOutput() 方法,充分适配了题目的多样化输出要求。
- 缺点
代码重复严重
ThreekindGate、Translations、Choice、Allocate 四个类各自重复实现了 getNum()、getId()、getType()、setSimulator()、isAssign() 等完全相同的样板方法,本可抽取一个 BaseElement 抽象类消除重复,却未做复用。
耦合度不降反升
第一次作业中,门类通过 setInput() 被动接收输入值,与模拟器解耦;第二次作业中,门类直接调用 sim.getPinValue() ,导致门逻辑与 CircuitSimulator 强耦合,不利于单元测试和模块独立。
继承体系分裂
基本门继承 LogicGate,复杂元件直接实现 CircuitElement,形成了“双轨制”,失去了统一抽象带来的多态便利性,也使得新增元件时需要判断应继承还是实现。
硬编码常量遍布
引脚编号偏移值直接写在多个方法中,未定义为常量,若引脚规则变化需多处修改。
- 总结
第二次作业是一次功能优先的快速迭代。它在第一次的基础上将元件数量从5种扩展至9种,成功构建了包含三态门、译码器、数据选择器、数据分配器的组合逻辑电路模拟系统,为后续时序电路的模拟提供了稳固的计算基础。
同时,功能快速膨胀也暴露了前期架构设计不足的问题,可以说,第二次作业虽然功能更加完善,但似乎只是为了通过测试点而去改,设计水平有待提升。
总体而言,第二次作业承载了从基础门电路向复杂数字系统跃迁的功能需求,也为第三次作业积累了宝贵的重构经验。
2.3第三次作业集
- SourceMonitor报表

行数/语句数:726行 / 507条语句。比第二次略少,可能是因为移除了测试代码或进行了重构。
类与接口:12个,比第二次少2个,但每个类的方法数达到16.75,是三次最高的——方法更加密集。
分支语句占比30.8%:最高,新增了5类异常处理,每条连接信息都可能触发多种异常,需要大量 if/else 判断优先级,分支增加。
方法调用语句159次:远高于第二次,说明代码大量依赖方法间协作,而非只在一个代码中。
最大复杂度3:最复杂方法 calculate() 位于36行,复杂度反而回落到第一次水平。说明虽然整体分支语句多,但被分散到了多个方法中,没有形成超级复杂函数。
最大嵌套深度9+:再次回到9+,可能是异常处理的嵌套优先级判断造成的。
- PowerDesigner类图

第三次作业在第一次的基础上迭代,保留了 CircuitElement 接口与 LogicGate 抽象类,并新增 reset() 方法,使用了第一次的被动 setInput 信号注入模式。核心新增为子电路模块和异常处理模块。模拟器采用分阶段解析,先收集所有子电路定义,再处理主电路,并通过 do-while 收敛循环反复驱动信号传播,直至所有门输出稳定。
- 优点
子电路封装设计合理:内部元件与主电路元件互不干扰,支持多次实例化。
耦合度显著降低:门类不再依赖 CircuitSimulator,仅依赖自身的 inputs 列表,便于单元测试和模块复用。
收敛循环保证了正确性:do-while 机制确保在多级门依赖时信号能完整传播,避免漏算。
异常处理覆盖全面,优先级逻辑符合题意。
- 缺点
异常处理模块过于臃肿:checkExceptions 方法包含大量嵌套条件和辅助方法,代码行数过长,职责过重,违反单一职责原则。
克隆机制脆弱:cloneGate 使用 instanceof 强制类型转换并依赖具体门类的 inputPinCount 字段,每新增一种门类型都需要修改此方法,扩展性差。
性能与设计冗余:simulate 中每轮迭代都重置所有门并重新设置输入,存在大量重复计算;输出排序中重复编译正则表达式,效率较低。
拼音命名与历史错误遗留:orGate 依旧表示异或门,命名错误未修正;yuGate/huoGate 等拼音命名持续存在。
- 总结
第三次作业功能上较为完善,成功实现了子电路封装和五类异常检测,使模拟程序具备了模块化和健壮性。通过回归被动注入模式,代码耦合度较第二次大幅改善,设计方向正确。然而,为了赶上功能节点,CircuitSimulator 被塞入了过多职责(解析、异常检查、传播、输出),导致单一职责原则被违背,可维护性受到影响。子电路的克隆机制和异常检测的具体实现也暴露了设计上的仓促。整体而言,它为数字电路模拟画上了句号,但也暴露出了我的一个大问题:在设计时为了追求测试点通过,不管不顾的去修改代码,最后导致未遵循单一职责原则这一悲剧。
三、踩坑心得
3.1第一次作业集
-
预先计算所有元件
错误做法:解析完输入后立即对所有元件调用 calculate()。
正确做法:只在 getPinValue 按需触发计算。
教训:预先计算会导致依赖关系不完整的元件输出无效值并标记 computed=true,后续递归无法纠正。 -
静态变量与实例变量混用
错误做法:components 设为静态但构造函数中又创建实例,导致状态残留。
正确做法:统一使用静态变量,在构造函数中重新初始化。
教训:静态变量生命周期长,多次运行必须手动清空。
3.2第二次作业集
-
引脚分配顺序
错误做法:控制引脚从1开始,或数据引脚从0开始。
正确做法:控制引脚从0开始,数据引脚从controlCount开始。
教训:严格按照题目 "按控制-输入-输出的顺序排序"。 -
输出引脚号错误
错误做法:强制改为 -0。
正确做法:使用物理引脚号(controlCount + dataCount)。
教训:数据选择器的输出引脚是最后一个物理引脚,不要强行改为0。 -
反向连接映射缺失
错误做法:只存储 connections,不存储 inputToSource。
正确做法:同时维护 inputToSource 映射(输入引脚→源引脚)。
教训:递归求值需要快速查找输入引脚的源,inputToSource 是必需的。
3.3第三次作业集
1.信号重置不彻底
错误做法:在多轮迭代求值时,只重置部分门的状态,或不清空旧值。
正确做法:每轮迭代开始时对所有门执行 reset()(清空输入和输出),然后根据当前信号池重新赋值。
教训:数字电路可能含组合环路,信号变化后旧值可能干扰新结果,必须完全重置再重新计算,否则输出可能保持错误状态。
2.子电路内部连接异常检测遗漏
错误做法:仅检查主电路连接信息的异常,忽略子电路定义内部的连接。
正确做法:按全局顺序收集所有连接,并根据上下文(主电路/子电路)正确判定引脚角色。
教训:子电路内部的连接同样可能出现“多个驱动”“无输出”等异常,题目要求的异常检测范围是全局的,漏检会导致部分用例失败。
3.引脚角色判定不考虑上下文
错误做法:用统一的规则判断引脚是驱动还是负载,不区分子电路内部。
正确做法:在子电路内部,INPUT 列表中的引脚是驱动端(向内部提供信号),OUTPUT 列表中的引脚是负载端(接收内部信号)。主电路中,外部输入是驱动,外部输出是负载。
教训:同一标识符在不同层次的角色可能相反,若不按模板的输入/输出定义判定,异常检测结果会错误,正常连接也可能被误判。
四、改进建议
4.1第一次作业集
现状:解析输入的同时计算元件。
修改建议:先完全解析所有元件和连接,构建完整电路图,再统一计算。
好处:避免解析顺序影响计算结果,便于调试。
现状:散乱使用 List
修改建议:使用 Map<Integer, Integer> 存储引脚值,支持任意引脚号(不强制从1开始)。
好处:为后续扩展(如控制引脚从0开始)打好基础。
现状:无循环依赖检测。
修改建议:在递归求值中维护 computing 集合,检测环路并返回 null。
好处:防止因输入错误导致无限递归或栈溢出。
4.2第二次作业集
现状:无循环依赖检测。
修改建议:在递归求值中维护 computing 集合,检测环路并返回 null。
好处:防止因输入错误导致无限递归或栈溢出。
4.3第三次作业集
现状:模拟采用简单的多轮迭代直至收敛,未区分组合逻辑与时序逻辑,无法正确处理触发器在时钟边沿的采样和保持特性。
建议:在每次时钟信号变化时,先计算所有组合逻辑的稳定状态,再在时钟边沿更新触发器的内部状态(使用当前输入值更新存储值),确保触发器的输出仅在下一次状态更新后改变。
好处:避免组合反馈与时序行为混淆,能正确模拟D触发器、JK触发器等边沿敏感元件的动作,防止因迭代顺序导致触发器提前翻转或振荡,提高时序电路仿真的准确性。
五、总结
总的来说,这三次数字电路模拟作业堪称一次困难的历练。三次迭代从基础门电路到复杂组合元件,再到子电路与异常处理,难度递增、题量饱和,覆盖面广,既考查了面向对象设计、继承多态、工厂模式等理论知识,更是对工程实践能力的一次严酷考验。回顾整个做题历程,第一次我还沾沾自喜地以为门电路真值表早已烂熟于心,结果却被繁杂的字符串解析和类结构设计打了个措手不及;第二次面对三态门、译码器、数据选择器和数据分配器时更是彻底崩溃,为了迁就这些“异类”元件,不得不牺牲原有的继承体系,导致代码量翻倍、耦合度不降反升;第三次来临时我的心态已经从“要写出优雅的代码”退化为“能跑就行”,就子电路的封装还勉强能让我接受,但异常检测的五种优先级判断差点把我逼疯。虽然这几次作业集的结果不尽人意,但当样例最终跑通的那一刻,我却感到前所未有的充实——这份收获远超几行代码。通过这三次的作业集,我学会了如何拆解复杂问题,把几百行输入迅速梳理成“解析-构建-计算-输出”的清晰主线;我深刻体会到代码规范的重要性,那些拼音命名、魔法数字和错误类名,在三次迭代中累积成了沉重的债务;更重要的是,我收获了面对大规模代码的钝感力,从畏惧几百行代码到坦然面对近千行的规模,学会冷静梳理逻辑,这种隐形成长才是最宝贵的财富。我甚至想如果还有第四次迭代,我一定会先把CircuitSimulator大卸八块,违背了单一职责原则也是无比致命的。但无论如何,这次作业教会我最重要的一件事就是:别怕,硬啃,啃完你就变强了。 虽然现在的代码依然带着不少缺陷,但我知道它为什么会这样,也知道该怎么让它变得更好。感谢这次作业集,我的见识再次得到丰富,也开始能够在上百行的代码中保持从容,希望能够在未来突破更好的自我。

浙公网安备 33010602011771号