面向对象——第四五六次PTA作业集总结
第一次作业
第一次作业共包含三个核心类:Gate(门电路类)、Source(信号源类)、Main(主类)。
本次作业采用面向过程的数据结构与单向信号传播机制,结构简单直白。其中Gate类为实体类,存储元件名称、类型、输入引脚数、编号、输出值以及各引脚的信号来源映射;Source类作为辅助记录类,用于存储信号来源的元件名和引脚号;Main类负责数据输入、元件创建、信号传播和结果输出。
类图
依赖关系:本次作业类之间依赖关系简单——Main类依赖Gate、Source以及集合容器、Gate类依赖Source作为引脚信号来源的映射值、Source类无任何依赖,属于独立辅助记录类。
复杂度分析
可以看到,复杂度怎么看都是有点爆的,这不仅是题目本身复杂性所致。
而且是代码规范性问题:
Main部分用大量 if-else 硬编码实现不同门电路(A/O/N/X/Y)的逻辑,每新增一种门类型都要修改这个方法,违反开闭原则。
每个分支里都重复了 “获取输入值→判空→计算结果” 的模板代码,冗余严重。
直接依赖 getVal 方法获取输入值,和外部状态耦合,难以单独测试。
用正则 + 多分支判断门电路类型,同时处理两种不同的命名格式(A(2)1 和 N1 等),逻辑复杂。包含大量格式校验和异常捕获,嵌套层级多,可读性差。
当然,也必然有可以优化之处:
如拆分main方法遵循, 单一职责重构 calc 方法;用策略模式消除 if-else;优化化 create 方法的逻辑等...
下为遵循单一职责示例:
查看代码
小结(一)
本次作业能拿100分是比较容易的,虽然main复杂度堆起来了,但大体无关紧要。
针对自定义对象存入哈希集合的需求,按照规范重写equals()与hashCode()方法,保证集合能够正确判断对象相等性,是面向对象开发中容器使用的重要实践。程序借助HashMap、List等集合类,统一管理大量逻辑门与输入信号,实现对象的批量组织与快速查找。
本次作业不仅完成了逻辑电路模拟的题目要求,也让我扎实掌握了类与对象、封装、集合框架、方法设计等核心知识点,学会用面向对象思维拆解实际问题、搭建程序结构。
第二次作业
第二次作业在第一次的基础上,新增了五类复杂器件:三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F),同时优化了信号传播机制。
本次作业共包含:Device抽象类及其五个子类(AndDevice、OrDevice、NotDevice、XorDevice、XnorDevice),以及专门处理复杂器件的独立类(TriDevice、DecDevice、MuxDevice、DemuxDevice)。此外引入了pinSignals全局信号池,实现引脚级别的信号存储与传播。
设计变化:
从第一次的Gate单一实体类,演变为抽象基类+具体子类的继承体系
引入pinSignals(Map<String, Integer>)作为全局信号池,替代了Gate.output的单一值存储
信号传播从calc方法的递归求值,演变为propagateSignals的迭代传播机制
类图
依赖关系:Main类依赖Device体系及pinSignals信号池;Device为抽象基类,五个子类(AndDevice、OrDevice、NotDevice、XorDevice、XnorDevice)各自独立实现calculate方法;复杂器件(TriDevice、DecDevice、MuxDevice、DemuxDevice)的calculate返回null,其逻辑在Main.propagateSignals中硬编码处理,pinSignals作为全局信号池被Main和evaluateDevice共同依赖。本次作业类之间依赖关系适中。
复杂度分析
与第一次对比,平均圈复杂度从7.00降至5.58,得益于职责拆分更加细致,方法数从11增至26。但最大圈复杂度从28升至40,说明个别方法承担了过重的职责,成为新的复杂度热点。
第二次作业在复杂度管理上有明显优化:平均圈复杂度从10.18降至5.58,方法数从11增至26,职责拆分更加细致。但复杂器件(S/M/Z/F)未纳入继承体系,导致evaluateDevice方法成为新的"上帝方法",圈复杂度高达40,认知复杂度77,远超第一次的calc方法(v(G)=24)。
此外,printOutputs也因九种器件输出格式各异而达到v(G)=24。两个方法合计贡献了64的圈复杂度,占全文件145的44%。如果继续沿用这种设计,第三次作业新增子电路功能时,这两个方法将进一步膨胀,最终导致架构失控。根本解决方案是将S/M/Z/F也纳入Device继承体系,各子类独立实现calculate和output方法,消除硬编码分支。
小结(二)
通过本次作业,我加深了对继承与多态的理解。五种基础门通过继承Device分别实现calculate方法,代码结构比第一次更加清晰。pinSignals全局信号池的引入也有效解决了多引脚器件的信号存储与读取问题。
优化方向可以将S/M/Z/F也纳入Device继承体系,各子类独立实现calculate和output方法,消除evaluateDevice和printOutputs中的硬编码分支。同时废弃Gate体系,统一信号存储为pinSignals单一信号池,避免双重路径问题。
第三次作业
第三次作业在前两次的基础上,新增了两大核心功能:子电路(模块化)和异常输入检测。
本次作业共包含:BlockModule(模块类)、LogicUnit抽象类及其五个子类(AndUnit、OrUnit、NotUnit、XorUnit、XnorUnit)、WireLink(线网类),以及Main类中大量的静态解析方法。此外,第一次的Gate体系、第二次的Device体系与第三次新增的LogicUnit体系三套元件表示同时共存。
类图
依赖关系:Main类依赖BlockModule、LogicUnit、WireLink以及前两次遗留的Gate/Device体系;BlockModule包含members(模块内元件映射)和internalLinks(内部线网列表),递归解析子电路定义;LogicUnit为抽象基类,五个子类各自实现compute方法;WireLink作为独立线网记录类,存储驱动端和接收端信息。本次作业类之间依赖关系急剧增加,且三套元件体系功能重叠,导致架构混乱。
复杂度分析
第三次作业最大圈复杂度虽从40降至22,但复杂方法数量从4个增至5个,parseMain(v(G)=22)和runSim(v(G)=19)成为新的复杂度热点。runSim的认知复杂度高达81,是全文件最难理解的方法。根本问题在于三套元件体系(Gate/Device/LogicUnit)共存,导致信号存储存在三重路径,子电路克隆时内部映射丢失。parseMain承担了四个独立职责,严重违反单一职责原则。有时候做题就是图方便难免违反。
改进思路:废弃Gate和Device体系,统一为LogicUnit一套体系;子电路采用命名空间重命名而非克隆;将parseMain拆分为解析、创建、连接、验证四个独立方法。
下为删除 cloneUnit 方法 + 改造 registerUnitByPin
查看代码
小结(三)
通过本次作业,我对模块化设计有了更深刻的理解,第三次作业也是这三次中最重量级也最重要的一个。子电路的本质不是简单的类嵌套,而是命名空间隔离——将一组元件封装为独立单元,对外只暴露输入输出端口,内部信号对外部不可见。这让我联想到操作系统中的进程隔离概念,虽然层级不同,但思想相通。异常检测机制的引入也让我认识到,在复杂系统中输入验证不是可有可无的附属品,而是保证系统健壮性的第一道防线,一个设计良好的系统应该在错误发生时给出清晰的提示,而非静默失败。
本次作业已完成大部分测试点,能想到的输入基本测试过无问题,但仍没有满分,还是有细节待究。
采坑心得
第一次作业踩坑记录
坑一:忽略引脚0的限制
第一次作业中,getVal方法没有s.pin == 0判断,导致读取信号时可能取到输入引脚而非输出引脚。
当上层元件连接到X1-1(异或门的输入引脚)时,getVal会返回g.output,但此时输出引脚还没计算完,或者返回的是错误的信号值。
查看代码
第二次作业踩坑记录
坑一:第一次的Gate.output和第二次的pinSignals同时存在,读取和写入走不同路径。
可以废弃Gate.output,统一从pinSignals读取:
查看代码
坑二:evaluateDevice中过早返回
evaluateDevice在检查控制引脚时,一个引脚缺失就直接return false,但其他引脚可能还没传播到。
控制引脚和数据引脚同时传播时,数据先到控制后到,由于控制缺失直接返回false,导致同一轮次内无法完成求值。
应该所有引脚读取完再统一判断:
查看代码
第三次作业踩坑记录
坑一:第一次的Gate、第二次的Device、第三次的LogicUnit同时存在,一个元件可能出现在三个Map里。
registerUnitByPin遇到子电路引脚时,先克隆LogicUnit,但runSim中又从Device体系读取信号,导致信号断裂。
可以废弃前两套体系,只用LogicUnit一套。
查看代码
坑二:异常检测过于激进
某些正常连接格式(如多个输出引脚连接到同一个输入引脚?题目说一个输入引脚不能连接多个输出引脚,但反过来是可以的)被误判为错误。->原本正确的Case因为异常检测误判而输出错误信息。
要仔细对照题目要求,只检测明确禁止的情况(没办法只能猜,猜也猜不到满分😀)
查看代码
踩坑小结
能跑通的代码不一定对,引脚0的坑就是血的教训
信号源只能有一处,多路径必然混乱
- 不要打补丁,要重构——欠下的技术债连本带利都要还
以样例为准,题目描述与样例冲突时信样例
一个方法只做一件事,parseMain和calc就是反面教材
(PS.最大的坑是无法理解题目要求。在第一次数字电路作业中,题目看似写的规则十分清晰,实则暗藏玄机,示例中的OUT是什么?0是哪里来的?原来OUT不是很有所谓,0是默认👴等等,全是看示例自己猜,自己摸索,自己去想,自己去试试是不是这样的,后面的一些测试点更是这样,找极端逻辑极端示例,本次作业比上三次作业最强悍之处在于测试点更为阴间,能想到的点都能过,也许是一些答案与出题者想的有所出入?本次作业想做到全对花费的时间无疑是巨大的,我也没有做到满分的地步,做到高分应该算是很好的了)
改进建议
一、代码结构改进
问题:三次作业的方法圈复杂度持续超标,Main.calc(v(G)=24)、Main.evaluateDevice(v(G)=40)、Main.parseMain(v(G)=22)都是典型的"上帝方法"。
建议:严格遵循单一职责原则,每个方法只做一件事。以parseMain为例,拆分为parseInputLines、parseConnections、validateConnections、buildNetwork四个方法,各自独立测试。
问题:三套元件体系(Gate/Device/LogicUnit)共存,信号存储存在多重路径。
建议:只保留一套元件体系,统一信号源。若从第三次作业重构,应废弃Gate和Device,只保留LogicUnit,所有信号通过唯一的sigMap存取。
二、架构设计改进
问题:子电路采用克隆物理元件的方式实现,导致内部信号映射丢失。
建议:采用命名空间重命名。子电路实例化时,给其内部所有信号名加上前缀(如C1.And1-0),信号池统一维护,无需克隆。
问题:复杂器件(S/M/Z/F)未纳入继承体系,在evaluateDevice中硬编码处理。
建议:将S/M/Z/F也纳入Device继承体系,各子类独立实现calculate和output方法,消除硬编码分支。
三、可扩展性改进
问题:新增器件类型需要修改create、calc、evaluateDevice、printOutputs等多个方法。
建议:引入工厂模式创建元件,策略模式处理计算,新增器件只需新增一个子类,无需修改已有代码。
问题:引脚号计算分散在各处(数据选择器输出引脚、译码器输出起始引脚等)。
建议:在Device基类中定义getOutputPin()和getInputPins()等方法,由各子类自行计算,外部只需调用接口。
四、异常处理改进
问题:异常检测逻辑与解析逻辑混在parseMain中,圈复杂度22。
建议:独立的InputValidator类负责所有异常检测,解析完成后统一校验,职责清晰。
问题:错误信息不够明确,难以定位问题。
建议:异常发生时输出具体的错误位置和期望格式,如ERROR: Pin X1-1 not connected,而非笼统的ERROR: invalid input。
总结
三次迭代作业做下来,最大的感受就是:能跑和好改是两码事。
一、代码结构改进
问题:三次作业的方法圈复杂度持续超标,Main.calc(v(G)=24)、Main.evaluateDevice(v(G)=40)、Main.parseMain(v(G)=22)都是典型的"上帝方法"。
建议:严格遵循单一职责原则,每个方法只做一件事。以parseMain为例,拆分为parseInputLines、parseConnections、validateConnections、buildNetwork四个方法,各自独立测试。
问题:三套元件体系(Gate/Device/LogicUnit)共存,信号存储存在多重路径。
建议:只保留一套元件体系,统一信号源。若从第三次作业重构,应废弃Gate和Device,只保留LogicUnit,所有信号通过唯一的sigMap存取。
二、架构设计改进
问题:子电路采用克隆物理元件的方式实现,导致内部信号映射丢失。
建议:采用命名空间重命名。子电路实例化时,给其内部所有信号名加上前缀(如C1.And1-0),信号池统一维护,无需克隆。
问题:复杂器件(S/M/Z/F)未纳入继承体系,在evaluateDevice中硬编码处理。
建议:将S/M/Z/F也纳入Device继承体系,各子类独立实现calculate和output方法,消除硬编码分支。
三、可扩展性改进
问题:新增器件类型需要修改create、calc、evaluateDevice、printOutputs等多个方法。
建议:引入工厂模式创建元件,策略模式处理计算,新增器件只需新增一个子类,无需修改已有代码