面向对象程序设计作业集4-6总结
一、前言
本阶段的三次PTA作业围绕“数字逻辑电路仿真系统”这一主题逐步展开,是一个典型的迭代式开发项目。三次作业遵循“基础实体封装→功能模块扩展→系统架构升级”的递进规律,从基础逻辑门电路模拟逐步扩展到包含复杂组合逻辑元件、子电路嵌套和异常检测的全功能数字电路仿真系统。
作业集4(数字逻辑电路模拟-1): 核心任务是完成基础逻辑门(与门、或门、非门、异或门、同或门)的解析、连接和输出计算。重点考察类的封装、字符串解析、集合管理以及图状连接关系建模。该版本为系统的最小可行版本,实现了从输入信号到逻辑门输出计算的基本闭环。
作业集5(数字逻辑电路模拟-2): 在作业4基础上扩展了三态门、译码器、数据选择器、数据分配器四种复杂组合元件。核心难点在于多输入、多输出器件的统一表达,要求程序能够识别某个端口究竟是信号源还是信号接收端。这一阶段重点考察面向对象的多态与扩展能力。
作业集6(数字逻辑电路模拟-3): 在前两次基础上引入子电路层次化封装与五类异常检测功能。要求把一组内部元件封装为子电路,并允许主电路通过子电路端口完成连接。同时需要检测连接线中的多信号源冲突、缺输入、缺输出、输入输出顺序错误、同一输入脚被多个信号源驱动等异常情况。
从题量与难度来看,三次作业均属于综合性程序设计题,题目数量不多但单题规模较大,调试成本高。作业4为基础入门,作业5难度明显提升,作业6在保持前作功能的前提下引入了子电路和异常处理机制,难度最大。三次作业的整体变化不是简单增加若干输入格式,而是不断提高对抽象能力、对象协作能力、边界处理能力和程序可维护性的要求。
以下将结合我的三次作业源码、SourceMonitor度量报告以及PowerDesigner类图,对三次作业进行系统性的设计与分析。
二、设计与分析
2.1 作业集4:基础逻辑门电路仿真
2.1.1 整体架构设计

作业4采用较为简洁的架构设计。从PowerDesigner类图可以看出,核心类结构如下:
-
Main类:程序入口,负责整体流程控制,包含
main()和getOrCreateGate()两个方法。 -
Gate类:逻辑门实体类,封装了门的名称、类型、ID、输入引脚数、信号源数组以及计算后的输出值。
compute()方法负责递归计算门的输出。 -
GateType枚举:定义了AND、OR、NOT、XOR、XNOR五种门类型。
类间关系方面,Main类依赖Gate类和GateType枚举,Gate类依赖自身(通过递归计算时的相互引用)以及Map集合。整体类结构较为扁平,尚未引入继承体系。
2.1.2 SourceMonitor指标分析

从数据来看,作业4的代码量适中(187行),但存在几个值得关注的问题:
第一,Main.main()方法的圈复杂度高达21。 这意味着main方法包含了过多的分支判断和循环逻辑,承担了超出其职责范围的工作——既负责输入解析,又负责门对象创建、连接建立、排序输出等多个环节,违反了单一职责原则。
第二,平均每个方法27条语句,方法粒度过粗。 理想的方法应该保持简短(通常建议不超过20行),但作业4中的方法平均长度达到27条语句,说明方法拆分不够细致。
第三,注释率仅2.1%,几乎为零注释。 这对于代码的可读性和后续维护构成了隐患,尤其是在迭代开发场景下,缺乏注释的代码会让后续的修改和扩展变得困难。
第四,最大块深度达到6,平均块深度3.24, 说明代码中存在着较深的嵌套结构,这通常意味着逻辑复杂度较高,增加了理解和调试的难度。
2.1.3 设计亮点与不足
亮点:
-
使用枚举类型
GateType管理门类型,避免了魔法值的出现,增强了代码的可读性。 -
Gate.compute()方法采用递归方式计算门输出,能够正确处理信号的多级传播。 -
使用
Map<String, Gate>管理所有门对象,便于通过名称快速查找和引用。
不足:
-
Main类过于臃肿。 输入解析、门创建、连接建立、排序输出全部塞在main方法中,职责不清晰。
-
缺乏异常处理机制。 当输入格式错误或连接不完整时,程序缺乏友好的错误提示。
-
扩展性受限。 所有逻辑门共用同一个Gate类,通过type字段区分类型,而非通过继承实现多态。这种方式在门类型较少时尚可工作,但一旦门类型增多(如作业5新增三态门、译码器等),将导致
compute()方法中的switch-case急剧膨胀。 -
注释严重缺失。 代码几乎没有注释,不利于理解和维护。
2.2 作业集5:组合逻辑元件扩展
2.2.1 整体架构设计

作业5在架构上进行了较大的重构,从扁平结构演进为更清晰的层次化设计。核心类结构如下:
-
Component抽象类:所有元件的基类,定义了
isOutputPin()、getOutputValue()、compute()、isValid()、formatOutput()等抽象方法。 -
具体元件类:
AndGate、OrGate、NotGate、XorGate、XnorGate(基础逻辑门),以及新增的TriStateGate(三态门)、Decoder(译码器)、Mux(数据选择器)、Demux(数据分配器)。 -
Main类:负责输入解析、元件注册、连接建立、排序输出等控制逻辑。
-
全局缓存机制:
pinValueCache用于缓存引脚值,computing集合用于检测循环依赖。
2.2.2 SourceMonitor指标分析

与作业4相比,作业5的代码质量有了显著提升:
第一,类数量从3个增加到8个,通过引入Component抽象类和多个具体子类,构建了清晰的继承体系,体现了面向对象的多态特性。
第二,平均每个方法语句数从27.00骤降至7.51,说明方法拆分更加细致合理,每个方法的职责更加单一。
第三,平均圈复杂度从11.40降至2.24,最大圈复杂度从21降至11,表明代码逻辑更加清晰,分支判断更加合理。
第四,注释率从2.1%提升到7.6%,虽然仍然偏低,但已有改善趋势。
第五,最大块深度从6降至4,平均块深度从3.24降至1.73,嵌套层次显著减少,代码可读性提高。
2.2.3 设计亮点与不足
亮点:
-
引入了Component抽象基类,通过继承实现了对多种元件的统一管理,体现了面向对象的多态特性。
-
采用工厂方法
Component.create()根据元件名称字符串创建对应的元件实例,将实例化逻辑集中管理。 -
引脚值缓存机制(
pinValueCache)和循环检测机制(computing)有效避免了重复计算和死循环。 -
每个元件类独立实现自己的
compute()逻辑,避免了作业4中巨型switch-case的问题。 -
方法粒度显著细化,平均每个方法仅7.51条语句,符合高内聚低耦合的设计原则。
不足:
-
Main类仍承担过多职责。 虽然相比作业4有所改善,但输入解析、元件注册、连接建立、排序输出等逻辑仍然集中在Main类中。
-
注释率仍然偏低(7.6%) ,部分关键方法缺少必要的说明文档。
-
部分元件的引脚映射规则较为复杂(如译码器的引脚布局:0,1,2为控制引脚,3..3+addrBits-1为输入引脚),缺少清晰的文档说明。
-
错误处理仍然不够完善,对于非法输入或未连接引脚的处理较为简单。
2.3 作业集6:子电路与异常检测
2.3.1 整体架构设计

作业6在作业5的基础上引入了子电路机制和异常检测功能,架构进一步复杂化。从PowerDesigner类图可以看出:
-
Component抽象类:作为所有元件的基类,包含
name、fullName、pinValues、inputPins、outputPins、parent等属性。 -
Gate类:继承自Component,表示基础逻辑门。
-
SubCircuit类:继承自Component,表示子电路,包含
components列表、internalConnections映射、outputSourceMap映射等。 -
Connection类:封装了一条连接线的信息,包括原始行、令牌列表和所属上下文。
-
Simulator类:负责整体仿真控制,包括解析、错误检查、模拟计算和结果输出。
2.3.2 SourceMonitor指标分析

从数据来看,作业6呈现出一些值得关注的趋势:
第一,总行数从773降至521,但功能复杂度却显著增加。这说明通过更好的架构设计(如将部分逻辑拆分到更多类中),可以用更少的代码实现更复杂的功能。
第二,方法调用语句数从147大幅增加到249,增幅达69.4%。这反映了方法间的交互和协作更加频繁,也说明代码的模块化程度更高。
第三,注释率为0.0%,这是一个严重的倒退。在引入子电路和异常检测这样复杂的机制后,完全没有注释的代码将极难理解和维护。
第四,最大圈复杂度18出现在SubCircuit.compute()方法中,说明子电路的递归计算逻辑较为复杂,存在优化的空间。
第五,最大块深度达到9+,远超作业4的6和作业5的4,说明代码中出现了极深的嵌套结构。这通常是由于子电路的层次化递归处理导致的。
2.3.3 设计亮点与不足
亮点:
-
成功引入了子电路机制,实现了电路的层次化封装和复用,这是从单层电路到多层电路仿真的关键飞跃。
-
实现了五类异常检测:多信号源冲突、缺输入、缺输出、输入输出顺序错误、同一输入脚被多个信号源驱动。
-
采用了原型/模板的设计思想,子电路定义与实例分离,便于复用。
-
建立了命名空间机制,通过
fullName区分不同层级的元件,避免名称冲突。
不足:
-
注释率为0%,这是最严重的问题。复杂的分层电路仿真代码完全没有注释,不仅影响当前的理解,也为后续的维护和迭代埋下了隐患。
-
SubCircuit.compute()方法圈复杂度过高(18),包含了多层循环和条件判断,需要进一步拆分。 -
最大块深度达到9+,嵌套层次过深,代码可读性差。
-
异常处理逻辑与计算逻辑耦合,使得代码更加复杂。
-
缺少单元测试,对于如此复杂的系统,没有自动化测试将导致回归风险极高。
2.4 三次作业纵向对比
将三次作业的核心度量指标进行纵向对比,可以清晰地看到代码质量的演变轨迹:
| 指标 | 作业4 | 作业5 | 作业6 | 趋势 |
|---|---|---|---|---|
| 总行数 | 187 | 773 | 521 | ↑178.6% |
| 有效语句数 | 151 | 509 | 405 | ↑168.2% |
| 分支语句占比 | 35.1% | 21.6% | 28.6% | ↓6.5% |
| 方法调用语句数 | 62 | 147 | 249 | ↑301.6% |
| 注释率 | 2.1% | 7.6% | 0.0% | ↓ |
| 类与接口数 | 3 | 8 | 6 | ↑100% |
| 平均每类方法数 | 1.67 | 7.88 | 4.67 | ↑179.6% |
| 平均每方法语句数 | 27.00 | 7.51 | 12.36 | ↓54.2% |
| 最大圈复杂度 | 21 | 11 | 18 | ↓14.3% |
| 平均圈复杂度 | 11.40 | 2.24 | 5.46 | ↓52.1% |
| 最大块深度 | 6 | 4 | 9+ | ↑ |
| 平均块深度 | 3.24 | 1.73 | 3.59 | ↑10.8% |
发现问题:
-
方法粒度先改善后恶化。 作业5的方法粒度最优(平均7.51条语句),但作业6回升至12.36,说明子电路机制的引入使部分方法变得臃肿。
-
注释率波动剧烈。 作业5注释率7.6%是三次中最高,但作业6骤降至0%,这是最需要警惕的信号——越是复杂的系统,越需要良好的文档注释。
-
复杂度先降后升。 作业5在复杂度控制上表现最好,但作业6的
SubCircuit.compute()方法圈复杂度达到18,最大块深度超过9,说明层次化电路的引入带来了新的复杂度挑战。 -
方法调用激增。 从62到249,方法调用语句数增长了301.6%,说明模块间的协作更加频繁,这是架构复杂化的必然结果,但也意味着需要更加谨慎地管理类间依赖。
三、采坑心得
3.1 作业4的主要问题
问题一:引脚解析的边界条件处理不当
在作业4中,引脚字符串的解析采用了lastIndexOf('-')的方式提取元件名和引脚号。但在实际测试中,当引脚名为纯数字(如外部输入a-0)时,解析逻辑容易出现混淆。我最初的实现没有充分考虑外部输入和元件引脚在格式上的统一性,导致部分测试用例失败。
问题二:递归计算的循环依赖检测缺失
Gate.compute()方法采用递归方式计算门输出,但最初没有加入循环检测机制。当电路中出现环形连接时(虽然题目保证无环,但实际测试中仍可能出现),程序会陷入无限递归导致栈溢出。后来通过在compute()方法中添加output != null的短路判断,一定程度上缓解了这个问题,但并未从根本上解决循环依赖的检测。
问题三:输出排序逻辑不够健壮
作业要求按照AND → OR → NOT → XOR → XNOR的顺序输出各逻辑门的计算结果。最初的实现中,排序逻辑直接写在main方法中,且对于门名称的解析使用了较为粗糙的正则匹配,当门名称格式不符合预期时容易出现异常。
3.2 作业5的主要问题
问题一:Component抽象类的设计不够完善
在作业5的设计中,Component抽象类定义了isOutputPin(int pinNum)方法,但不同元件类型的引脚布局差异很大。例如,基础逻辑门的输出引脚固定为0,三态门的输出引脚为2,译码器的输出引脚从3+addrBits开始。这种差异导致每个子类都需要独立实现引脚判断逻辑,增加了代码冗余。
问题二:译码器(Decoder)的控制逻辑容易出错
译码器的控制条件要求S1=1, S2=0, S3=0,且地址线从引脚3开始(A0为最低位)。在实现时,我最初将地址线的顺序搞反了(将引脚3当作最高位),导致计算结果错误。后来通过逐位调试才发现了这个问题。
问题三:三态门的高阻态处理不够清晰
三态门在控制引脚为0时输出高阻态,此时输出无效。但题目要求中对于高阻态的输出格式没有明确说明,我最初的处理方式是输出null,但后续发现有些测试场景需要将高阻态视为“未连接”而不是“输出0”。最终采用了valid标志位来区分有效输出和高阻态。
3.3 作业6的主要问题
问题一:子电路的递归计算顺序难以控制
子电路内部包含多个元件,元件之间可能存在复杂的依赖关系。最初的实现采用简单的do-while循环反复遍历所有元件,直到没有新的输出产生为止。这种方法虽然能够工作,但效率较低,且当子电路嵌套层次较深时,计算顺序的确定性难以保证。
问题二:引脚命名空间的混淆
作业6引入了fullName的概念来区分不同层级的元件(如顶层A1和子电路内部的C1-A1)。但在实现连接解析时,我最初没有清晰区分name和fullName的使用场景,导致部分连接无法正确匹配到对应的元件引脚。后来通过统一使用fullName作为Map的键,才解决了这个问题。
问题三:异常检测的优先级和覆盖范围
作业6要求检测五类异常,但不同异常的检测时机和优先级不同。例如,“输入输出顺序错误”需要在语法解析阶段检测,而“信号冲突”需要在连接关系建立后检测。我最初的实现将所有异常检测混在一起,导致检测逻辑混乱。后来通过将检测分为“语法检查”和“语义检查”两个阶段,才使逻辑更加清晰。
问题四:子电路输出端口的映射
子电路的输出端口需要映射到内部某个元件的输出引脚。这个映射关系的建立和查询在实现中容易出错。我最初采用在解析连接时直接记录outputSourceMap的方式,但在子电路嵌套的场景下,映射关系的传递变得复杂,需要递归查找。
3.4 通用问题总结
问题一:缺乏系统的测试用例
三次作业我都是依赖PTA平台提供的测试点进行验证,没有自己编写系统的单元测试。这导致每次修改代码后,只能通过提交到PTA来验证是否正确,调试效率极低。
问题二:注释严重不足
从SourceMonitor的报告中可以看出,三次作业的注释率分别为2.1%、7.6%和0.0%,整体处于极低水平。在迭代开发过程中,缺乏注释的代码让我自己在回头看时都难以快速理解当初的设计意图,更不用说后续的修改和扩展了。
问题三:迭代过程中的技术债务积累
由于每次作业都在前一次的基础上增加功能,而前一次作业中存在的设计缺陷(如Main类过于臃肿、缺乏异常处理等)没有得到及时修复,导致技术债务不断积累。到了作业6,虽然功能更加复杂,但代码的可维护性却在下降。
四、改进建议
4.1 架构层面
建议一:引入MVC或分层架构
目前三次作业的控制逻辑(输入解析、连接建立、计算调度、结果输出)都集中在Main类和Simulator类中,职责过重。建议将系统划分为以下层次:
-
模型层(Model) :元件类(Gate、SubCircuit等)、引脚类(Pin)、连接类(Connection)。
-
视图层(View) :负责输入解析和输出格式化。
-
控制层(Controller) :负责调度计算流程、管理元件生命周期。
这样可以使各层职责清晰,便于独立测试和修改。
建议二:使用设计模式优化代码结构
-
工厂模式:
Component.create()已经初步体现了工厂模式的思想,但可以进一步抽象为独立的ComponentFactory类。 -
观察者模式:引脚值的变化可以触发依赖该引脚的其他元件重新计算,观察者模式可以很好地实现这种信号传播机制。
-
访问者模式:对于异常检测,可以使用访问者模式遍历电路图,而不必将检测逻辑分散在各个元件类中。
建议三:建立完善的异常处理体系
目前作业6虽然实现了五类异常检测,但异常处理逻辑与计算逻辑耦合。建议定义独立的异常类层次结构(如CircuitException、ConnectionException、PinConflictException等),将异常检测与正常计算分离。
4.2 代码质量层面
建议一:大幅提升注释覆盖率
从SourceMonitor报告可以看出,三次作业的注释率分别为2.1%、7.6%和0.0%,远低于行业标准(通常建议20%以上)。建议:
-
为每个类添加类级别的Javadoc注释,说明类的职责和使用方式。
-
为每个公共方法添加方法级别的Javadoc注释,说明参数、返回值和可能的异常。
-
为复杂的业务逻辑添加行内注释,解释设计决策和算法思路。
建议二:降低方法复杂度
Main.main()(作业4,圈复杂度21)和SubCircuit.compute()(作业6,圈复杂度18)的圈复杂度过高。建议将这些复杂方法拆分为多个小方法,每个方法只做一件事。例如:
-
将
SubCircuit.compute()拆分为propagateInputs()、evaluateComponents()、propagateOutputs()、updateOutputPorts()等子方法。 -
将
Main.main()拆分为parseInput()、buildCircuit()、simulate()、printResults()等阶段方法。
建议三:减少嵌套深度
作业6的最大块深度达到9+,严重影响了代码可读性。建议通过以下方式减少嵌套:
-
使用早返回(early return)模式,在检测到无效条件时立即返回。
-
将深层嵌套的逻辑提取为独立方法。
-
使用流式API(如Stream)替代复杂的循环嵌套。
建议四:建立单元测试体系
建议使用JUnit为关键类和方法编写单元测试,特别是:
-
每个元件类的
compute()方法(测试各种输入组合下的输出是否正确)。 -
引脚解析和连接建立的逻辑(测试各种格式的输入是否能正确解析)。
-
异常检测逻辑(测试各种异常场景是否能被正确识别和报告)。
4.3 功能层面
建议一:优化子电路的计算算法
目前SubCircuit.compute()采用反复遍历所有元件的迭代方式,效率较低。建议改为拓扑排序算法:
-
根据元件间的依赖关系建立有向无环图(DAG)。
-
对DAG进行拓扑排序,得到元件的计算顺序。
-
按照拓扑顺序依次计算每个元件的输出。
这样可以保证每个元件只计算一次,大幅提升计算效率,同时也能自然地检测出循环依赖。
建议二:增强错误信息的友好性
目前异常检测的输出信息较为简略,建议提供更详细的错误定位信息,包括出错的行号、具体的引脚名称、冲突的具体原因等,方便用户定位和修复问题。
建议三:支持更复杂的电路特性
在现有架构的基础上,可以进一步扩展支持:
-
时序逻辑元件(触发器、寄存器等)。
-
总线(多比特信号)。
-
参数化元件(可通过参数配置行为)。
五、总结
通过这三次作业的迭代开发,我在多个方面获得了成长和收获:
5.1 所学所得
第一,深刻理解了面向对象设计的核心原则
从作业4的扁平结构到作业5的继承体系,再到作业6的层次化设计,我切身感受到了封装、继承、多态在应对系统复杂度时的强大力量。特别是作业5中引入Component抽象基类后,新增元件类型变得非常方便——只需继承Component并实现compute()方法即可,无需修改已有代码。这让我对“开闭原则”有了切身的体会。
第二,掌握了代码度量工具的使用方法
通过SourceMonitor对三次作业的代码质量进行量化分析,我学会了用数据来评估代码质量。圈复杂度、方法长度、嵌套深度等指标不再是抽象的概念,而是能够指导我优化代码的具体依据。从作业4到作业5,平均圈复杂度从11.40降至2.24,这个进步让我真切感受到了代码重构的价值。
第三,认识到文档和注释的重要性
三次作业的注释率始终处于极低水平(最高仅7.6%),这让我在后期维护时付出了额外的代价。尤其是作业6引入了子电路这种复杂机制后,没有注释的代码几乎无法快速理解。这个教训让我深刻认识到:代码是写给计算机执行的,但首先是写给人类阅读的。
第四,体验了完整的系统迭代开发流程
从基础逻辑门到组合元件,再到子电路和异常检测,我完整经历了一个小型系统从简单到复杂的迭代过程。这个过程让我体会到,好的架构设计不是一蹴而就的,而是在不断的需求演进中逐步打磨出来的。每一次迭代都是一次重构和优化的机会。
5.2 待改进之处
第一,需要加强设计阶段的工作
在三次作业中,我往往是在编码过程中逐步调整设计,缺乏事前的充分设计。这导致了一些设计缺陷(如Main类臃肿)在早期就埋下了隐患,后期难以彻底修复。未来在开始编码之前,应该花更多时间进行类图设计、接口定义和职责划分。
第二,需要建立测试驱动开发的习惯
目前我主要依赖PTA平台的测试点进行验证,缺乏自主编写单元测试的意识。这导致调试效率低、回归风险高。未来应该养成先写测试、再写代码的习惯,通过自动化测试来保障代码质量。
第三,需要持续关注代码质量指标
SourceMonitor的度量报告显示,虽然作业5在各项指标上表现优异,但作业6出现了明显的倒退(注释率归零、最大块深度超过9等)。这说明代码质量的维护是一个持续的过程,不能因为赶进度而放松对质量的要求。
第四,需要加强异常处理的设计
作业6虽然引入了异常检测,但异常处理逻辑与正常业务逻辑耦合较紧,导致代码复杂度上升。未来应该将异常处理作为独立的设计关注点,建立清晰的异常处理策略和统一的错误报告机制。
5.3 展望
这三次作业虽然已经结束,但学习和改进的道路永无止境。数字逻辑电路仿真系统作为一个经典的面向对象设计案例,还有很多可以深入研究和优化的方向——时序逻辑、行为级建模、波形仿真等。我将把在本次作业中积累的经验和教训应用到未来的学习和项目中,持续提升自己的软件设计能力。
最后,感谢这三次作业带给我的挑战和成长。正是这些“痛苦”的调试过程和“纠结”的设计决策,让我真正理解了面向对象程序设计的精髓所在。

浙公网安备 33010602011771号