作业
前言
这三次作业都是围绕数字电路模拟器做迭代升级,难度一步比一步高,相当于同一个项目分三阶段持续加需求,我分开简单梳理下每一次的知识点、做题感受:
第四次作业(电路模拟器基础版)
只需要实现五种基础逻辑门:与、或、非、异或、同或。用到的知识点不算复杂,基础 Java 面向对象封装、字符串切割解析输入、用数组存引脚电平、循环迭代传递电路信号、自定义排序输出结果。题目输入结构简单,只有全局输入、连线、结束标记,边界坑不多,主要难点是多级串联电路的信号传递要循环更新到稳定。整体题量适中,属于入门练手,只要理清信号流转逻辑基本能全过样例。
第五次作业(新增复合型电路元件)
在基础门之上新增三态门、译码器、数据选择器、数据分配器四类元件,一下子复杂不少。新知识点有:区分控制引脚 / 数据输入引脚 / 多路输出引脚、不同元件差异化运算逻辑、多输出引脚数据存储、元件特殊输出格式。题量直接翻倍,每种新元件都有单独的计算规则和输出格式,而且老代码完全适配不了多路输出、控制引脚的设定。我当时没提前重构代码,直接在原来代码上硬加判断,写到一半逻辑彻底混乱,译码器、分配器相关功能没写完。
第六次作业(子电路 + 输入异常检测)
这是三次里最难的一次,两大新增核心内容:一是子电路,可以把一段电路打包复用,要求用组合模式做分层设计;二是全套输入语法错误检测,分 5 类报错,还要区分错误优先级,同时新增引脚多驱动冲突校验。知识点涵盖组合设计模式、文本分段状态解析、分级异常判断、子电路内外引脚映射。输入流分成子电路定义区和主电路两部分,格式约束极多。我之前的代码是扁平结构,没有分层抽象,完全撑不起子电路功能,最后只写完了基础输入读取,子电路、异常校验两大模块都没实现。
整体来看三次作业是递进式迭代,一次比一次看重代码架构和程序鲁棒性,不是单纯写逻辑跑样例,越往后越考验前期代码设计好不好。第四次我完整写完并通过全部测试点,第五、六次因为前期偷懒没做分层,代码拓展性太差,大量功能缺失,下面结合我自己写的源码细说问题。
一、设计与代码分析
4.1 第四次作业源码结构分析
这次代码一共三个类:Connect 存连线信息、Gate 保存单个逻辑门所有数据、Main 主类负责全部解析和仿真流程,整体结构简单直白。
Connect 连线类
代码很简单,只存信号来源引脚和一堆目标引脚,相当于单纯的数据载体。
缺点很明显:所有输入格式校验、引脚匹配判断全堆在 Main 里,这个类本身啥校验逻辑都没有,不符合单一职责,只是当时赶时间随便写的。
Gate 门核心类
这个类算是这次代码唯一做得还行的部分,把门的类型、编号、每个引脚电平、输出值全部封装到一起,写了两个方法:判断输入引脚是否全部赋值、根据门类型计算输出电平。
优点:引脚编号直接对应数组下标,取值很方便,基础五种门的计算逻辑单独抽在 calc 方法里,主类不用管内部运算。
致命短板(给后面两次作业埋大坑):
只支持单一输出引脚,只有一个 outVal 变量,完全不能适配第五次译码器、分配器多路输出的需求;
不区分控制引脚和普通输入引脚,所有引脚数据塞一个数组,后面新增控制端元件根本没法拓展;
用 switch 硬编码五种门,以后加新元件必须直接改 calc 方法,拓展性很差。
Main 主程序流程
整体流程:逐行读取输入→区分 INPUT、连线、结束标识→提取电路里所有门对象→循环传播电路信号直到不再更新→按规定顺序排序门→打印有效输出。
信号传播是循环迭代思路:每一轮先推送全局输入信号,再推送已经算出结果的门的输出,之后重新计算所有输入齐全的门,一轮没有任何引脚更新就停止循环。
这个逻辑对付基础串联电路没问题,但效率很低,每次循环都要遍历全部连线和门,电路一多会很慢。
从类结构就能看出来,当时完全没考虑后续迭代,全程靠字符串匹配引脚,没有用对象引用绑定,每次找对应门都要循环遍历集合,效率低还难维护。
4.2 第五次作业拓展适配问题
第五次新增的四种元件直接把第四次的 Gate 类废掉了,拿 3-8 线译码器举例:它有控制引脚、输入引脚、8 路输出引脚,原来一个 inVal 数组、单个 outVal 根本存不下这么多数据,也没法区分控制引脚做工作条件判断。
按规范思路,本来应该抽一个抽象父类,再分出基础门、三态门、译码器、选择器多个子类,靠多态统一调用计算和输出方法。但我图省事,直接在原有代码上加一堆 if 判断区分新元件,复制粘贴大量重复的引脚解析代码,代码量暴涨五百多行,圈复杂度飙升。
最后结果就是三态门勉强写完,译码器、数据分配器多路输出存储逻辑彻底写不下去,放弃了这部分功能,只能跑原来五种基础门的样例。
现在回头看,前期为了快速过样例省略分层抽象,到拓展需求的时候重构成本直接翻倍,得不偿失。
4.3 第六次作业架构层面的硬伤
第六次明确要求用组合模式,把基础门当成叶子节点,子电路作为可以容纳多个元件的复合容器,两者共用一套统一操作接口。
标准设计思路:
抽象电路组件父类,定义统一计算、输出接口;
叶子子类:基础逻辑门、三态门等单个元件;
复合子类:子电路,内部可以存放一堆元件、连线,自带内外引脚映射;
单独工具类处理输入校验、语法报错;
全局记录表,跟踪每个输入引脚的信号来源,检测多驱动冲突。
但我完全没重构原有扁平代码,还是沿用第四次的老结构,带来一堆问题:
没有分段解析的状态标记,分不清当前读取的是子电路定义还是主电路内容,子电路数据和主电路混在一起;
没有引脚驱动记录表,无法检测一个引脚接收多路信号的冲突报错;
不存在子电路容器,没法实现子电路引脚别名映射,主电路不能调用子电路。
最后只能完成最基础的输入读取,子电路、异常检测两大核心功能全部空白。
二、踩坑心得
2.1 第四次作业踩过的坑
信号循环顺序写错,多级串联电路输出不全
测试样例 4 是三级串联电路:与门→非门→或门。最开始我写代码时,每轮循环先计算门输出,再推送信号,导致后面两级门永远接收不到信号,只能打印第一层与门的输出。
修正思路调整循环顺序:先把所有现有有效信号推送到位,再统一计算门输出,多轮迭代直到没有引脚电平更新,改完后三级门输出全部正常打印。
引脚重复赋值导致死循环超时
一开始没有判断引脚是否已经赋值,每次循环都会重复覆盖引脚电平,updated 标记一直为 true,程序无限循环触发时间限制。
修复时加了判断,只有引脚是未赋值状态(-1)才更新电平并标记本轮有改动,彻底解决超时卡死问题。
多位数元件编号解析出错
比如 A (10) 10 这种两位数字编号,最初只截取单个字符,门编号识别错误,最后输出顺序混乱。改用定位括号下标截取完整数字后,所有编号都能正常识别。
输出排序逻辑缺失
一开始直接按创建顺序打印,不符合 “与门、或门、非门、异或、同或,同类按编号从小到大” 的输出要求,测试点大量报错。新增自定义比较器,给每种门固定排序优先级,输出才匹配样例。
2.2 第五次作业踩坑
单一输出变量存不下多路输出数据
译码器有四路、八路输出引脚,但原来 Gate 只有一个 outVal 变量,只能保存一个电平值,没法同时记录多路引脚状态,完全实现不了译码器输出规则。这也让我意识到,写实体类之前一定要预判业务是否存在多值存储场景。
控制引脚和普通输入引脚混在一起无法校验
译码器需要校验三个控制引脚电平才能正常工作,但所有引脚数据都存在一个数组里,没办法区分哪几个是控制引脚,校验逻辑根本写不了,元件输出永远判定为无效。
2.3 第六次作业核心大坑
缺少引脚驱动记录,无法检测引脚信号冲突
样例 7 中同一个输入引脚接收两路不同信号,题目要求输出冲突 ERROR。我没有用哈希表记录每个引脚的信号来源,根本没法判断冲突,报错功能完全无法实现。
输入无分段状态机,子电路解析混乱
子电路必须全部写在主电路 INPUT 之前,我逐行读取没有状态标记,分不清子电路起止标记 Cx: 和 endc,子电路内部的输入、连线全部混入主电路数据,元件命名、引脚映射全乱套。
异常优先级判断顺序颠倒
题目规定了错误优先级:多输出连线 > 无输入 > 无输出 > 输入输出颠倒 > 引脚冲突,同一行连线多条错误只输出优先级最高的。我最开始判断顺序写反,低优先级错误先打印,和样例输出对不上。
通用共性问题总结
三次作业暴露了我编码的固定坏习惯:
喜欢用字符串名字匹配元件,不用对象关联,代码执行效率低;
做开发只追求当下跑通样例,完全不考虑后续拓展,违反开闭原则;
滥用全局静态集合,所有数据堆在 Main 里,没有封装上下文类;
很少提前预判边界场景,多位数编号、多级电路、语法异常都是写到一半才临时修补;
功能优先,架构设计后置,越迭代代码越臃肿,重构工作量巨大。
三、改进优化建议
短期轻量优化:
把零散的全局静态集合封装成一个上下文类,减少全局变量;
把反复出现的引脚名称、编号切割逻辑抽成工具方法,消除重复代码;
信号传递改成队列模式,只处理电平变动的引脚,不用每次遍历全部连线,提升运行速度;
拆分门电路计算逻辑,降低单个方法复杂度,方便后续修改维护。
中长期完整架构重构(适配 4、5、6 全部需求)
抽一个电路组件抽象父类,统一规定计算、输出接口;再细分基础门、三态门、译码器、子电路子类,依靠多态减少重复代码;
拆分独立工具类:专门解析引脚名称的工具、信号仿真传播工具、输入错误校验工具,把 Main 主类逻辑拆分干净;
新增全局上下文类,统一存放全局输入、子电路集合、引脚冲突记录表,彻底删掉所有 static 全局变量;
新增输入解析状态机,区分子电路定义、主电路两种读取状态,隔离两段输入数据;
增加元件工厂类,根据元件标识自动生成对应元件对象,以后新增元件只需要加子类,不用改动主流程。
预留拓展空间(适配之后时序电路、多层子电路)
引脚新增状态枚举,预留时钟、复位引脚标识,以后写 D、JK 触发器可以直接复用;
支持子电路多层嵌套,子电路内部可以再调用其他子电路;
增加循环次数上限,防止反馈电路无限循环卡死程序;
统一自定义错误输出体系,区分语法错误、运行警告,调试大型电路更方便。
四、整体学习总结
收获
基础面向对象实操落地:第四次作业完整练会类封装、集合使用、自定义排序、文本解析,能独立梳理仿真电路的信号流转流程,定位基础逻辑 bug;
理解迭代开发的架构重要性:第五、六次因为前期代码没分层,拓展功能处处受限,实实在在体会到设计模式不是纸上谈兵,好的架构能大幅减少后续开发工作量;
建立程序健壮性思维:第六次的异常检测让我明白,正规程序不能只处理合法输入,各类语法冲突、格式错误都要提前校验,不能等到运行出错才处理;
学会靠测试样例、代码量化数据找问题,不再凭感觉判断代码好坏,能清晰梳理代码设计上的短板。
自身不足,之后需要补的内容
设计模式掌握很浅,只会简单封装实体,组合、工厂模式只能看懂理论,自己动手分层架构很吃力,之后多拿练习题动手重构代码练手;
复杂分段文本解析能力差,遇到子电路这种多块混合输入只会堆 if 判断,打算学习有限状态机的写法;
性能优化意识弱,写代码只保证功能正常,不考虑循环遍历带来的效率损耗,后续多用哈希表、队列优化遍历逻辑;
开发习惯不好,写代码前不会先画类图梳理需求拓展点,总是上手就写,导致后期大量重构,之后做题先梳理整体结构再编码;
异常处理体系混乱,只会简单打印错误,没有统一的校验工具类,后续整理一套规范的分层校验写法。
整体感悟
这三次数字电路模拟器相当于一套小型软件完整迭代流程,从简单功能到拓展元件,再到复合模块和输入容错,一步步暴露我编码习惯和架构思维上的短板。最深刻的感受就是写代码不能只顾眼前跑通测试样例,前期偷的懒,后面迭代都会加倍还回来。
这次复盘把自己写代码踩的坑、代码的缺陷、优化方向都整理清楚了,之后会按照优化方案完整重构整套代码,补全第五、六次没做完的扩展元件、子电路和异常检测功能,把这套电路模拟器改成一套分层清晰、方便持续拓展的完整程序。
浙公网安备 33010602011771号