第二次blog作业

一、前言
这段时间,我们一共完成了三次数字电路模拟程序相关的编程作业。这三次作业对我来说,既是对课堂知识的巩固,也是一次难得的实操锻炼。三次作业是同一个题目的迭代开发——从最开始的基础逻辑门电路,到后来加入复杂元件,再到最后引入子电路和异常检测,题量和难度都是慢慢增加的,一步步提升。
涉及到的知识点,主要是Java面向对象编程的内容,比如类的继承和多态、抽象类的设计、ArrayList集合的使用,还有字符串解析、信号传播算法、排序输出等。另外,像设计模式中的组合模式(Composite),也在第三次作业中用到了,虽然一开始不太理解,但写完代码后慢慢有了感觉。整体来说,这三次作业没有脱离我们大一的学习范围,但把课堂上的理论知识和数字电路这个实际场景结合了起来,不再是单纯的语法练习。
题量上,第一次作业(基础逻辑门)比较简单,40个测试用例全部通过;第二次作业(加入复杂元件)难度略有提升,34个用例也全部通过;第三次作业(加入子电路和异常检测)题量最多,43个用例通过了39个,有4个没有通过。下面我详细说说这三次作业的情况。
二、设计与分析
(一) 整体设计思路
这三次作业的核心都是围绕数字电路的模拟来展开的。简单来说,就是设计元件类(Gate),通过代码实现输入解析、信号传播计算、结果格式化输出。第一次作业我只搭了个基本的继承框架;第二次作业在此基础上新增了四种复杂元件,代码量增加不少;第三次作业又加了子电路和异常检测,还用到了Composite设计模式。我一开始并没有什么清晰的设计思路,只是想到什么写什么,后来慢慢摸索,才学会先把类图画出来,再一步步完善功能。
(二) 第一次作业源码设计分析
第一次作业的核心是五种基础逻辑门元件:与门(A)、或门(O)、非门(N)、异或门(X)、同或门(Y)。我的核心类是Gate抽象类,以及它的五个子类AndGate、OrGate、NotGate、XorGate、XnorGate,再加上Circuit类负责电路管理和信号传播,Printer类负责按格式输出结果,Main主类负责调度。
Gate抽象类定义了name(元件名)、type(类型标识符)、inputs(输入引脚值列表)、result(输出值)这些属性,还有canCompute()判断输入是否就绪、getresult()计算输出值这些方法。各个子类重写getresult()来实现各自的逻辑——比如AndGate要求所有输入都是1才输出1,OrGate只要有一个1就输出1。
信号传播这块,我用了多轮迭代的方法:每轮遍历所有门元件,输入就绪的门就计算输出,然后看这一轮有没有输出发生变化,如果没有就说明电路稳定了,结束循环。这个方法虽然简单,但在元件数量不多的情况下完全够用,所有40个用例运行时间都在150毫秒以内。
第一次作业的类结构比较简单,但是Circuit类里的dataCount()方法写得有点臃肿,把遍历、计算、变更检测都堆在一起了,后面用SourceMonitor分析才发现这个方法复杂度有12,偏高了。类图如下:
image
image

第一次作业40个测试用例全部通过,说明基础框架搭建得还算扎实,这为后面的迭代开发打下了基础。
(三) 第二次作业源码设计分析
第二次作业在基础门电路上新增了四种元件:三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F)。这些元件跟基础门不一样,它们有控制引脚——除了输入和输出之外,还有一个控制端决定元件的工作模式。比如三态门,控制端为高电平时导通(输出等于输入),低电平时断开(高阻态);译码器则要在控制引脚满足"S1=1且S2+S3=0"时才正常工作。
为了支持这些新元件,我的类结构也做了不少改动。新增了OutPin类,把输出引脚从简单的int值升级成一个独立对象——因为译码器有8个输出引脚,数据分配器也有多个输出,原来的单一result属性不够用了。还新增了MultiPinGate抽象类,封装多输出引脚元件的公共行为,MGate、ZGate、FGate都继承它。SGate因为是单输出,直接继承Gate。
Printer类的输出逻辑也改了不少。译码器的输出格式是"元件名:输出0的引脚编号",比如"M(2)1:0"表示Y0引脚输出0;数据分配器输出"元件名:各引脚状态序列",无效状态显示"-";三态门只在导通时才输出。这些格式一开始搞混了好几次,后面专门列了个对照表才理顺。类图如下:
image
image

第二次作业34个测试用例也全部通过。这次最大的收获是学会了怎么在已有代码基础上做扩展——第一次搭的Gate继承体系,在第二次加新元件的时候,基本没动原来的代码,直接新增子类就行了。这就是老师讲的"对扩展开放、对修改关闭",当时觉得是理论,现在有了真实的体会。
(四) 第三次作业源码设计分析
第三次作业是改动最大的一次,新增了两大块功能:子电路和异常检测。子电路的意思是可以把一部分电路打包成一个"元件",然后在主电路里像用普通门一样使用它。题目要求用Composite设计模式来实现——Component是抽象基类,Gate(基础门)是叶子节点Leaf,SubCircuit(子电路)是组合节点Composite,它可以包含基础门甚至其他子电路。
SubCircuit类里有独立的元件集合(gates),还有inputMap和outputMap两个映射表,用来把子电路对外的端口名(比如A、B)映射到内部的具体引脚。主电路引用子电路内部元件时,用三段式命名,比如"C2-X1-0"表示子电路C2内部的异或门X1的输出引脚。这个命名方式一开始很容易跟普通引脚名搞混,后来专门写了个解析方法才稳定下来。
异常检测这块,要求检测5类错误,按优先级处理:多输出(一个连接信息里有多个输出引脚)、无输入、无输出、输入输出顺序反了、信号冲突(一个输入引脚接了多个来源)。如果一条连接同时有好几种异常,只报优先级最高的那个;如果多条连接都有异常,只处理第一条。这个短路逻辑跟Java的异常处理机制有点像,我一开始做错了——每条连接都检查全部5项,导致已经出错的连接还在继续处理,后面引发了空指针异常,改了好几次才理清。类图如下:
image
image
image

第三次作业43个用例通过了39个,有4个没过。没过的用例(case8、case24、case29、case35)都跟子电路有关,我推测是多个子电路同时存在的时候,里面的同名元件会互相干扰,还有就是跨子电路的信号冲突没有检测到。这几个问题我到现在还没有完全解决,其难度也是比较大的。
三、采坑心得
这三次作业,我踩了很多坑,每一个坑都让我印象深刻,也学到了很多实用的东西,下面我就结合自己的代码和测试结果,详细说说。

  1. 信号传播只算了一轮,级联电路就出错了。第一次作业初期,我按元件创建的顺序,一轮遍历就算完了所有门的输出。结果遇到"A→非门→或门→输出"这种链式电路的时候,或门排在非门前面,或门计算的时候非门的输出还是默认值0,最终结果就错了。我加了好多System.out.println()打印中间值,才定位到是计算顺序的问题。后来改成多轮迭代,每轮检查有没有输出发生变化,没变化才停止,问题就解决了。这个坑让我记住了:有依赖关系的时候,不能想当然地按顺序一次算完,要确认上游算完了再算下游。
  2. 译码器和数据分配器的输出格式跟其他元件不一样。第二次作业,译码器有8个输出引脚,但题目要求只输出那个为0的引脚编号,比如"M(3)1:3",而不是把8个引脚的电平都列出来。数据分配器的未选中引脚要输出"-"表示无效。我一开始没注意,按基础门的方式全部输出,结果跟样例对不上。后来在Printer类里给type='M'和type='F'单独写了输出逻辑,还用-1作为"无效"的内部标记(因为0和1都是合法信号,-1没人用),输出时自动转换成"-"。这个坑提醒我,读题的时候要把每种元件的输出格式提前列好,不能凭惯性写代码。
  3. 子电路里的元件名跟主电路冲突了。第三次作业,我用了一个全局的HashMap来存所有元件,键就是元件的名字。结果子电路C1里有个N1(非门),C2里也有个N1,主电路里还有一个N1——后面解析的N1把前面存的覆盖掉了,最终只有一个N1能用,另外两个丢失了。我当时打印了HashMap的所有键,才发现只剩一套名字。后来给每个子电路单独建了一个HashMap,用"子电路编号-元件名"作为全限定名来查找,大部分问题解决了,但跨子电路连接的时候还是有一些残余的bug(也就是那4个没过的用例)。这个坑让我第一次体会到了"命名空间"这个概念的重要性。
  4. 异常检测的短路逻辑写错了。第三次作业,题目要求:一条连接如果有多种异常,只报优先级最高的;多条连接有异常,只处理第一条。我一开始把每条连接的所有5项检查都跑了一遍,收集到一个列表里再取第一个,结果已经出错的连接还在往下走,后面直接报了空指针。后来改成了严格的短路逻辑——按优先级1→2→3→4→5的顺序检查,任何一个命中就立刻输出错误信息,然后跳过这条连接和后面所有连接的解析。改完后大部分异常用例都过了,但case35(信号冲突检测)还是有一个场景没覆盖到——我推测是子电路引脚的命名在主电路里和子电路里对不上,导致冲突检测漏掉了。
  5. 忽略了边界情况和特殊场景。第三次作业,我自认为代码已经很完善了,前两次作业也都是满分通过,但第三次提交后还是有4个用例没通过。排查下来,发现主要问题是多子电路共存的时候,有些边界组合没有考虑到,还有跨子电路的信号冲突检测不够严谨。虽然找了很久的原因,但最后也没有完全解决,第三次作业测试点过不去。这个坑让我明白,代码不能只满足于常规场景,要多想想各种极端情况,测试覆盖率要更全面才行。
  6. 一开始把太多逻辑堆在了一个方法里。第一次作业的dataCount()方法,把遍历、判断、计算、变更检测全写在一起了,后来用SourceMonitor一分析,圈复杂度12,超出正常范围了。第二次和第三次作业我慢慢学会把功能拆分开——解析的逻辑放一个方法,计算的逻辑放另一个方法,输出的逻辑再单独写。这样代码结构清晰了,调试的时候也能快速定位问题,效率提升了很多。
    四、改进建议
    (一) 代码编写层面改进
    首先,以后编写代码,我要养成先设计类图再动手写的习惯。这三次作业中,第二次和第三次的扩展之所以还算顺利,很大程度上是因为第一次的Gate继承体系设计得比较合理。如果第一次把逻辑都硬编码了,后面扩展起来就很痛苦。所以以后拿到题目,第一步先画类图,想清楚类之间的关系再写代码。
    其次,针对元件创建那部分——createGate()方法里写了一大堆if-else来判断元件类型,代码又长又乱。以后可以考虑用工厂方法模式来改写,每种元件对应一个工厂类,主程序通过元件名的首字母来找对应的工厂,这样新增元件类型的时候只需要加一个新工厂类,不用改原来的代码。
    另外,异常检测的代码现在跟输入解析耦合在一起,改起来很麻烦。以后可以考虑把这5类异常检测拆成独立的模块,每个检测一个类,用责任链的方式串联起来,这样每类检测可以单独测试,优先级顺序要改的时候也方便。
    (二) 数据存储与逻辑优化改进
    对于子电路的命名空间管理,目前虽然给每个子电路建了独立的HashMap,但跨子电路查找的时候还是容易出问题。以后可以专门设计一个Namespace类,里面存当前命名空间的元件集合,还有一个指向父命名空间的引用——这样查找的时候先在本空间找,找不到就递归往上找,跟编程语言的作用域解析一样。元件统一用全限定名(比如"C1::N1::0"),从根源上杜绝同名冲突。
    信号传播算法目前用的是多轮迭代,虽然能过测试,但效率不够好。以后可以改成拓扑排序——先把元件之间的依赖关系画成有向图,然后用Kahn算法(BFS加一个入度计数器)算出正确的计算顺序,一轮就能全部算完,时间复杂度从O(n²)降到O(n+e)。而且拓扑排序还能自动检测电路中是否存在反馈环路,这对后续迭代也会有帮助。
    还要多考虑边界情况,比如子电路嵌套子电路、同一编号在不同子电路中的使用、跨子电路的信号传播路径等,把这些边界场景都考虑到,代码的鲁棒性才能跟上来。
    (三) 输出与调试优化改进
    遇到题目描述比较复杂的情况,我会先把每种元件的输入格式、输出格式列一个对照表,确保写代码的时候不会搞混。尤其是像译码器、数据分配器这种输出格式跟其他元件完全不同的情况,更要在动手写之前就搞清楚。
    以后完成代码之后,不能只依赖PTA平台的测试用例来验证。我会先自己设计一些测试用例,包括常规的、边界的、异常的场景都覆盖到,自己先跑一遍,排查隐藏的逻辑漏洞。比如多个子电路共存、子电路引脚信号冲突这种场景,应该在本地就提前测好,而不是等到提交了才发现问题。
    另外,代码注释的比例要提上去。关键的方法和复杂的逻辑判断,都加上注释说明为什么要这样写——不光是自己以后回来看能看懂,别人看我的代码也能明白我的设计意图。
    五、阶段性总结
    (一) 学习收获
    通过这三次数字电路模拟程序的迭代作业,我真的学到了很多东西。一开始,我对继承、多态、设计模式这些知识点,只是停留在课本上背定义的层面,不知道怎么运用到实际中。通过这三次作业,我亲手搭了一个从5种门扩展到9种门、再到加入Composite模式的类体系,慢慢把理论知识转化成了实操代码。
    在反复调试、排查错误的过程中,我解决问题的能力也提升了不少。一开始遇到PTA的测试点不通过,我会很慌,不知道该从哪里入手。后来慢慢学会了二分注释法缩小范围、对比预期输出和实际输出定位差异、构造最小测试用例隔离问题——这些调试方法不只是用在这门课上,以后写任何代码都能用上。
    另外,通过这三次作业的迭代开发,我切身体会到了"好的初始设计很重要"——第一次作业搭的Gate继承体系,在第二次加4种新元件的时候几乎没动原来的代码,只新增了子类。这就是老师上课讲的"对扩展开放、对修改关闭",当时觉得是空洞的理论,现在有了真实的感受。
    (二) 自身不足与后续学习方向
    虽然有了很多收获,但我也清楚自己还有很多不足。首先,第三次作业的4个用例到现在也没有通过,说明我在子电路命名空间管理和边界条件处理上还有明显的漏洞。后续要专门花时间去理解和修复这些bug,不能放着不管。
    其次,我对单元测试还完全没有概念,这三次作业全都依赖PTA平台的黑盒测试来验证代码。实际开发中,程序员应该自己写JUnit测试用例来验证每个模块的正确性——这个能力我目前完全是空白,以后要重点学习。
    另外,编写代码的时候我还是容易把逻辑写在一起,虽然比一开始好了一些,但拆分的还不够彻底。后续的学习中,我要多看看优秀的代码案例,学习别人是怎么组织代码结构、怎么做到高内聚低耦合的。
    (三) 对课程和作业的一些建议
    结合这三次作业的经历,我也想提一些自己的想法。在课程教学方面,希望老师在讲解设计模式的时候,能多结合我们作业里会真实用到的场景来讲,比如讲Composite模式的时候直接拿子电路的例子来演示,这样我们理解起来会快很多。还有就是我们写代码时高频出现的错误,比如输入解析的字符串处理、ArrayList的遍历方式选择这些,老师可以在课堂上集中演示一下排错思路。
    在PTA作业方面,题目难度大幅增加,代码量也更多了,压力明显变大了。另外,也希望测试用例的覆盖范围能更清楚一些,这样我们在本地测试的时候能更有针对性地准备。
posted @ 2026-06-23 21:52  黄沙古渡  阅读(4)  评论(0)    收藏  举报