面向对象程序设计作业4-6总结
一、前言
本阶段三次作业围绕组合逻辑数字电路仿真核心主题展开,以 Java 语言为实现载体,从基础门电路计算逐步演进到支持多级级联、多类复杂器件的完整仿真系统。三次作业在知识点覆盖、实现难度、工程化程度上呈现明显的递进关系,完整呈现了一个小型仿真系统从原型到可用版本的迭代开发过程,既是对数字电路理论知识的代码化落地,也是对 Java 面向对象设计、算法实现与工程调试能力的综合训练。
1.1 知识点覆盖
三次作业覆盖的核心知识点可分为数字电路原理与 Java 工程设计两大维度,形成 “理论 + 实践” 的双向强化:
数字电路原理维度:基础逻辑门(与门 AND、或门 OR、非门 NOT、异或 XOR、同或 XNOR)的运算规则;三态门(TRI)的高阻态特性与使能控制逻辑;译码器(Decoder)、数据选择器(MUX)、数据分配器(DEMUX)三类中规模组合逻辑器件的引脚定义与功能特性;信号级联传递规则;高阻态(无效值)的传播逻辑;电路环路的物理特性与结果判定。
Java 编程与工程设计维度:面向对象封装、抽象与职责分离思想;集合框架(HashMap、ArrayList、HashSet)的特性与适用场景;正则表达式的文本解析应用;递归回溯算法的设计与边界控制;有向图环检测原理;定制化排序与比较器实现;枚举类型的类型安全优势;代码迭代优化与缺陷修复方法。
1.2 题量与难度演进
三次作业均为单道综合编程题,但实现要求与隐含约束逐层提升,呈现清晰的阶梯式难度:
第四次作业:入门级原型实现。核心要求是实现指定格式输入下的基础器件仿真,隐含假设电路无多级级联,所有器件输入均直接连接全局输入引脚。代码以面向过程风格为主,逻辑集中在主方法中,重点考察文本解析与基础逻辑运算的编码能力,题量适中,难度较低。
第五次作业:进阶级架构重构。核心要求升级为支持任意多级电路级联,器件输入可连接至其他器件的输出,信号可跨多级器件传递。本次作业需要完成从 “单遍批量计算” 到 “递归回溯求值” 的架构跃迁,同时引入面向对象设计对器件进行封装,是三次作业中架构改动最大、设计思维要求最高的版本,难度显著提升。
第六次作业:优化级鲁棒增强。在第五次功能完整的基础上,重点解决边界场景下的稳定性问题,包括循环依赖栈溢出、集合无序导致的计算错误、历史数据残留等。核心考察缺陷定位、边界测试与最小侵入式修复能力,要求在不破坏原有架构的前提下提升代码健壮性,难度偏向细节与深度。
整体而言,三次作业遵循 “原型实现→架构升级→鲁棒性优化” 的经典软件开发迭代路径,总代码量从 300 行量级逐步增长到 550 行量级,代码可维护性、正确性与健壮性持续提升,完整模拟了工业界小型软件的迭代开发流程。
二、设计与分析
本章节分别对三次作业的源码架构、核心设计与实现细节进行拆解分析,结合类结构设计与代码度量指标说明各版本的设计思路与演进逻辑,还原每次版本迭代的设计决策与取舍。
2.1 第四次作业:朴素单遍仿真原型
2.1.1 整体架构设计
简化类图如下:

第四次作业采用面向过程为主、轻量数据封装的设计风格,所有逻辑集中在DigitalCircuit主类中,仅定义Component静态内部类作为数据载体,通过全局静态容器维护引脚状态与器件列表,是典型的快速原型实现。
核心数据结构包括三类全局容器:
pinValue:Map<String, Integer>类型的全局引脚电平表,键为引脚全名(如A(2)1-1、全局输入A),值为 0 或 1 的逻辑电平。
components:List
ORDER常量数组:字符串数组形式定义器件输出排序规则,严格按照 A/O/N/X/Y/S/M/Z/F 的顺序输出结果。
Component内部类仅承担数据存储职责,未封装任何行为方法,包含器件类型、编号、输出值列表、有效输出标记、分配器输出字符串等字段,所有计算逻辑均在主方法中通过多层 if-else 分支实现,数据与行为高度分离。
2.1.2 核心执行流程
程序执行分为四个线性阶段,全程单遍扫描,无回溯与依赖处理,逻辑高度扁平化:
输入读取阶段:逐行读取标准输入,直到遇到end标记结束,自动过滤空行,将有效行存入列表统一处理。
INPUT 解析阶段:通过正则表达式匹配INPUT:开头的行,解析全局输入引脚的初始电平,以 “引脚名 - 电平值” 的格式拆分后存入输入映射表。
连线解析阶段:通过正则匹配[源引脚 目标引脚1 目标引脚2 ...]格式的连线行,直接将源引脚的电平值批量赋值给所有目标引脚,一次性完成所有引脚的电平传递。
器件计算与输出阶段:遍历所有器件,按器件类型分支执行对应逻辑运算,得到输出结果后按照ORDER数组定义的优先级排序,最终格式化打印输出。
2.1.3 代码度量与设计评价
参考 SourceMonitor 的代码度量维度,该版本呈现典型的原型代码特征:
代码规模:约 320 行,单文件单主类,包含 1 个内部数据类,代码集中度极高。
方法数量:仅包含main主方法与extractNum数字提取辅助方法,方法粒度极粗。
圈复杂度:主方法圈复杂度超过 20,大量 if-else 分支嵌套,单一方法承担解析、计算、输出全部职责,可测试性极差。
可维护性:低水平,新增器件类型需同时修改解析逻辑、计算逻辑、排序逻辑三处以上代码,违背开闭原则。
该版本的核心优势是逻辑直观、上手简单,能够快速实现单级电路的仿真需求;但局限性非常突出:完全不支持多级级联电路,所有器件输入必须直接连接全局输入引脚,若某器件输入连接另一器件输出,则计算时该引脚值为空,导致结果错误。本质上这是一个 “扁平化” 的仿真器,只能处理单层组合逻辑,无法应对真实电路的层级结构,是功能优先、架构让步的原型实现。
2.2 第五次作业:面向对象重构与递归级联求值
针对第四次作业无法支持级联的核心缺陷,第五次作业进行了架构级重构,全面引入面向对象设计思想,采用递归回溯求值解决信号的多级传递问题,是三次作业中架构跃迁最大的版本,完成了从 “能用” 到 “可用” 的质变。
类图如下:

2.2.1 类结构设计
第五次作业主类为DigitalCircuitSim,核心抽象分为三层,结构较第四版有本质提升:
DevType枚举类型:统一管理 9 种器件类型(AND/OR/NOT/XOR/XNOR/TRI/DECODER/MUX/DEMUX),替代第四版的字符串常量,从语法层面避免类型拼写错误,类型安全性显著提升。
Device内部实体类:从单纯的数据载体升级为具备完整属性描述的器件实体,实现Comparable
基础属性:器件全名、器件类型、参数值、器件编号
引脚分类:控制引脚集合ctrlPins、输入引脚集合inPins、输出引脚集合outPins,从语义上区分不同功能的引脚
状态缓存:引脚电平映射pinVal,存储已计算完成的引脚电平,避免重复计算
全局容器层:
devMap:器件名到器件实体的映射,支持 O (1) 快速查找器件
globalInput:全局输入引脚电平表,存储初始输入值
sourceMap:引脚来源映射表,记录每个输入引脚的信号源,构成电路的连接有向图
对应 PowerDesigner 类图结构中,DigitalCircuitSim作为主控类依赖Device实体类与DevType枚举类,Device类聚合多组引脚集合,整体呈现清晰的 “主控类 + 实体类 + 枚举类型” 三层结构,职责划分较第四版有本质提升。
2.2.2 核心算法:递归引脚求值
第五次作业的核心突破是getPinValue递归方法,实现了 “按需求值、回溯溯源” 的级联计算逻辑,从根本上解决了多级电路的依赖问题:
若当前引脚为全局输入引脚(名称不含 “-”),直接从全局输入表中返回初始电平。
若当前引脚为器件引脚,先检查引脚缓存pinVal,已计算则直接返回缓存值。
未计算则通过sourceMap查找该引脚的信号源,递归调用getPinValue获取源引脚的电平值。
将获取到的结果存入缓存后返回当前引脚值。
在此架构下,每个器件的计算逻辑只需遍历自身输入引脚,调用getPinValue即可自动完成多级溯源,无需关心上游连接了多少级器件。各类器件的计算逻辑被拆分为calcAnd、calcOr、calcNot等独立方法,主方法只需遍历调用calcDeviceOut统一调度,代码职责清晰。
2.2.3 代码度量与设计评价
参考 SourceMonitor 度量指标,该版本在代码量增长的同时,质量指标全面优化:
代码规模:约 520 行,较第四版增长 60% 以上,但结构更清晰,可读性更强。
方法数量:拆分为 15 个左右方法,单方法职责单一,平均方法行数从百行级降至 30 行以内。
圈复杂度:主方法圈复杂度降至 5 以内,各计算方法圈复杂度普遍在 3-8 之间,整体可测试性大幅提升。
可维护性:达到中等水平,新增器件类型只需扩展枚举、新增创建逻辑与计算方法,符合开闭原则的雏形。
该版本基本实现了任意多级组合逻辑电路的仿真,功能上达到了可用状态,但仍存在多处鲁棒性缺陷:一是无循环依赖检测,若电路存在环路,递归会无限调用导致栈溢出;二是引脚集合无序,HashSet遍历顺序不固定,导致译码器、数据选择器等依赖引脚顺序的器件计算结果随机出错;三是无脏数据清理机制,多次仿真可能残留历史数据。
2.3 第六次作业:鲁棒性增强与边界问题修复
第六次作业在第五次架构基础上进行缺陷修复与鲁棒性增强,核心解决了循环依赖、引脚顺序、数据一致性三大边界问题,整体架构保持稳定,属于优化迭代版本,是三次作业中最完善的版本。
简化类图如下:

2.3.1 核心修复点 1:循环依赖检测
在getPinValue方法中引入Set
该实现以极小的代码侵入性解决了环路崩溃问题,同时保持了递归求值的架构不变,兼顾了修复效果与架构稳定性。
2.3.2 核心修复点 2:引脚顺序标准化
针对HashSet无序导致的计算错误,在所有遍历引脚集合的场景前统一执行Collections.sort()排序,确保三类核心逻辑的确定性:
译码器控制引脚、输入引脚按编号从小到大遍历,编码高位在前,输出序号与器件定义一致
数据选择器控制引脚、数据引脚按编号升序遍历,选通逻辑符合器件功能定义
数据分配器输出引脚按升序拼接输出字符串,格式完全符合题目要求
该修复统一了所有器件的引脚遍历规则,彻底消除了集合无序带来的随机性 bug,计算结果的确定性与可复现性得到根本保障。
2.3.3 核心修复点 3:计算前状态清理
在calcAllDeviceValue方法中,遍历所有器件前先调用dev.pinVal.clear()清空引脚缓存,避免多次计算时历史数据残留导致的错误,保证每次仿真都是纯净的初始状态,杜绝了脏数据引发的隐蔽错误。
2.3.4 代码度量与设计评价
代码规模:约 570 行,较第五版增长约 10%,新增代码全部为环检测与排序逻辑,改动集中且边界清晰。
鲁棒性:显著提升,可应对环路、无序引脚、重复计算等边界场景,异常输入下不再崩溃。
可维护性:较高水平,所有修复均为局部修改,未破坏原有递归求值架构,后续扩展仍保持较低成本。
正确性:边界用例通过率大幅提升,计算结果稳定可复现,功能完整性与可靠性达到作业要求
。
三、采坑心得
三次作业的开发调试过程中,遇到了多类典型问题,涵盖算法逻辑、数据结构特性、边界场景处理等多个维度。以下结合具体问题现象、根因分析、解决方案与验证结果进行详实总结,所有问题均有实际测试数据支撑。
3.1 级联电路求值失效问题
问题现象
第四次作业中,当电路出现两级及以上级联(例如非门输出连接与门输入,与门输出再连接或门输入)时,下游器件输出结果始终为无效值,极端情况下直接抛出空指针异常。构造 3 级级联用例测试,正确率为 0。
根因分析
第四次作业采用 “先批量赋值所有连线,再批量计算所有器件” 的单遍策略,默认所有器件的输入都直接来自全局 INPUT 引脚。但级联场景下,第二级器件的输入引脚连接的是第一级器件的输出引脚,而连线赋值阶段第一级器件尚未计算,因此该引脚值为空,导致后续计算错误。
本质是对电路的 “依赖关系” 缺乏认知,没有意识到器件计算存在严格的先后顺序,上游器件必须先完成计算,下游器件才能获得正确输入。单遍扫描只能处理扁平化的单层电路,无法应对有向图结构的真实电路。
解决方案
第五次作业重构为递归求值架构,将 “主动批量计算” 改为 “按需回溯求值”。每个引脚的值都追溯其信号源,若源是器件输出则触发该器件计算,自动保证依赖顺序。该方案无需手动拓扑排序,天然支持任意深度的级联电路,且实现逻辑简洁直观。
验证结果
重构后,对 3 级级联电路(输入→非门→与门→或门)进行多组输入测试,各级输出均符合真值表预期,电平传递完全正确,该问题彻底解决。进一步测试 5 级深度级联电路,结果依然准确,验证了架构的通用性。
3.2 循环依赖导致栈溢出崩溃
问题现象
第五次作业中,若输入电路存在环路(例如两个非门的输入输出交叉连接,形成双非门振荡电路),程序直接抛出StackOverflowError,进程崩溃退出,无任何容错能力。
根因分析
递归求值没有环路检测机制。环路场景下,引脚 A 的值依赖器件 B 的输出,器件 B 的输入依赖引脚 C,引脚 C 又依赖器件 A 的输出,最终递归调用链形成闭环,无限递归直至栈空间耗尽。
这是典型的图遍历未做访问标记的问题。电路本质是一张有向图,存在环时 DFS 遍历必须引入访问标记,否则必然出现死循环。该问题属于边界场景遗漏,是原型开发阶段的常见疏漏。
解决方案
第六次作业在getPinValue中引入访问集合vis,采用 “进入标记、离开回溯” 的 DFS 环检测模式:
进入方法时,若当前引脚已在vis中,直接返回null表示无效
否则将引脚加入vis,执行递归逻辑
方法返回前将引脚从vis中移除,不影响其他路径的遍历
验证结果
对双非门环路、三级环路、自环等多组环路测试用例进行测试,程序均不再崩溃,环路相关引脚输出无效值,符合数字电路中振荡信号无法稳定输出的物理特性。同时对正常无环电路进行回归测试,计算结果不受影响,修复方案无副作用。
3.3 HashSet 无序导致的计算随机性错误
问题现象
第五次作业中,译码器、数据选择器的输出结果不稳定,相同输入多次运行可能得到不同结果;部分测试用例时对时错,本地测试通过但在线评判失败,复现难度极大。统计 10 次重复运行结果,3 输入译码器的正确率仅为 30%。
根因分析
器件的输入引脚、控制引脚均使用HashSet存储,而HashSet的遍历顺序不保证与插入顺序一致,且不同 JVM 版本、不同运行环境下顺序可能不同。
以 3 输入译码器为例,输入引脚编号 3、4、5 对应 A2、A1、A0,若遍历顺序变成 5、3、4,则编码计算完全错误,最终输出的低电平引脚序号偏离预期。由于错误具有随机性,调试时极易被忽略,是典型的 “偶发疑难 bug”。
解决方案
第六次作业中,所有遍历引脚集合的位置,均先转为 List 再执行Collections.sort(),严格按引脚编号升序遍历,保证编码顺序高位在前、与器件定义一致。修复覆盖译码器编码、数据选择器选通、数据分配器输出拼接等所有依赖顺序的场景。
验证结果
对译码器、数据选择器进行 100 次重复运行测试,输出结果完全一致,随机性错误彻底消除。对全 0 输入、全 1 输入、边界编码等多组用例逐一验证,计算结果均严格符合器件功能定义,正确率达到 100%。
3.4 器件命名解析的边界错误
问题现象
第四次作业中,部分多位数编号的器件(如A(2)10、M(3)12)解析错误,编号提取异常,器件创建失败。测试 10 以上编号的器件,解析正确率为 0。
根因分析
第四次作业采用简单的字符串替换提取数字,或按固定位置取字符,例如译码器参数直接取字符串第 2 位字符作为数值,仅适用于单位数参数与单位数编号的场景,一旦参数或编号超过 9,解析结果完全错误。
该问题属于典型的 “用特例代替通用规则”,只满足了题目样例的简单情况,未考虑通用边界场景,是原型开发阶段的常见问题。
解决方案
第五次作业重构了解析逻辑,通过查找左右括号的索引位置,精确截取括号内的参数字符串与括号后的编号字符串,再通过Integer.parseInt()转为整数。该方案支持任意位数的参数与编号,解析鲁棒性大幅提升。
验证结果
对A(4)12、M(3)10、Z(2)25等多组多位参数、多位编号的器件命名进行测试,参数与编号均能正确解析,器件创建与引脚生成完全符合预期,通用场景下解析正确率达到 100%。
3.5 高阻态与无效值的传递偏差
问题现象
初期版本中,未连接的输入引脚默认值为 0,导致三态门关闭时输出错误,悬空输入的器件输出不符合物理规则。三态门关闭场景下,下游与门输出错误率 100%。
根因分析
数字电路中,未连接的引脚、三态门关闭的输出均为高阻态,逻辑上属于无效值,不能等价为 0 或 1。若用 0 代替无效值,会导致与门、或门等器件错误计算 —— 例如悬空输入的与门本应输出无效,却被当作 0 输入输出 0,完全违背电路物理特性。
解决方案
引入null表示无效 / 高阻态,所有器件计算时严格校验输入有效性:只要有一个输入为null,输出即为null;三态门关闭时输出null;未连接引脚返回null。通过统一的无效值传递规则,保证仿真结果符合物理逻辑。
验证结果
三态门关闭时下游器件输出无效,悬空输入的器件不产生有效输出,完全符合数字电路的物理特性,无效值传递逻辑正确。同时对全有效输入的正常场景进行回归测试,功能不受影响。
四、改进建议
当前版本已实现组合逻辑电路的基础仿真,满足作业要求,但在架构性能、功能边界、工程化程度上仍有较大优化空间。以下从架构、功能、工程化、交互四个维度给出可持续改进的方向,形成可落地的迭代路径。
4.1 架构优化:从递归求值到拓扑排序仿真
当前递归求值方案实现简单,但存在两大固有局限:一是深度极深的电路可能触发栈溢出(尽管有环检测,但正常电路深度过大也会有栈风险);二是存在重复计算,多个下游器件引用同一个上游输出时,会多次触发上游回溯计算,性能存在冗余。
建议采用拓扑排序 + 事件驱动仿真替代递归架构:
解析完所有连线后,构建电路的有向无环图(DAG),统计每个器件的入度(依赖的上游器件数量)。
使用 Kahn 算法进行拓扑排序,得到器件计算的全局先后顺序。
按拓扑顺序依次计算每个器件,计算完成后将输出值传递给所有下游器件,完成电平传播。
该方案的优势显著:无递归栈溢出风险,支持超大规模电路;每个器件仅计算一次,时间复杂度稳定为 O (N),性能更优;天然支持环检测,入度无法清零的器件即为环路部分,无需额外的 DFS 检测。对于大规模电路仿真,拓扑排序架构的性能与稳定性显著优于递归方案,是仿真器向专业级演进的必经之路。
4.2 功能扩展:支持时序电路与多比特位宽
当前仅支持单比特组合逻辑电路,功能边界较窄,可向两个核心方向扩展:
时序电路支持:增加 D 触发器、JK 触发器等时序器件,引入时钟信号与状态存储,实现同步时序电路仿真。增加最小时间片概念,按时钟沿更新器件状态,支持寄存器、计数器、状态机等常见时序电路。同时可配套实现波形输出功能,直观展示时序信号变化。
多比特位宽支持:当前引脚均为单比特 0/1,可扩展为多位总线信号,支持加法器、比较器、ALU 等多位运算器件,大幅降低大规模电路的描述复杂度,提升仿真实用性。
此外还可增强错误提示能力,对未连接引脚、器件参数非法、环路等问题输出明确的警告信息与位置提示,辅助用户定位电路设计错误,而非静默输出无效值,提升使用体验。
五、总结
此次4-6作业集难度较大,我力不从心,借助AI侥幸完成,但其中还是有相当多的测试
没有通过,但收获同样巨大,相信再次面对时会比较从容。这次也为我面向对象设计打
下了基础。

浙公网安备 33010602011771号