从逻辑模拟到面向对象重构的踩坑与成长

作业集4~6 总结:从逻辑模拟到面向对象重构的踩坑与成长

一、前言

本次作业集4到6围绕数字电路逻辑模拟器展开,是一次完整的从“功能实现”到“架构优化”的迭代训练。三次作业的核心知识点、题量与难度层层递进:

  • 作业集4:基础门电路模拟,核心考察字符串解析、Java集合框架、拓扑排序算法的落地应用,仅1道综合大题,但需要把数字电路的硬件依赖逻辑转化为可执行代码,难度中等偏上。
  • 作业集5:扩展复杂器件,在基础门的前提下新增传输门、译码器、多路选择器、信号分配器四种器件,核心考察代码的扩展性、边界条件处理,题量1道,难度中等,重点是在原有代码上兼容新功能。
  • 作业集6:面向对象重构+子电路嵌套,用多态替代分支判断,支持子电路定义、嵌套调用与错误检测,核心考察面向对象思想、封装继承多态、递归展开与错误校验,题量1道,难度中等偏上,重点是代码可维护性与架构设计。

三次作业题量不大,但每一道都需要抠细节、卡边界。从最开始只求“能跑通”,到后来追求“写得好、易扩展、少出bug”,整个过程踩了不少实际的坑,也对代码质量有了更具体的认知。

二、设计与分析

我用SourceMonitor对三个版本的代码做了代码度量,结合类结构与核心逻辑,逐版本分析如下。

095F1F59CE1E19DFE776E941042DF0FB

464B2B466D5C30DF836DE4F82B2672FD

D74CAF1986A45F1E9E183FDFFA6234B0

1. 作业集4:基础门电路模拟器(V1版本)

核心指标

总代码265行,有效语句208条,分支语句占比28.4%;共2个类(器件类C、主类Main),平均每个类2.5个方法;最复杂的方法是main(),集中了几乎全部业务逻辑。

设计思路

这一版是典型的“面向过程+简单数据类”结构:

  • 用一个C类封装所有器件的通用属性:类型、名称、编号、输入引脚数、引脚映射、输出值、有效性标记,本质是一个数据容器。
  • 主方法main承担全流程:读取输入行→解析外部输入信号→逐行解析连线关系→构建器件映射表→构建有向依赖图→Kahn算法拓扑排序→按拓扑顺序计算每个器件输出→按类型排序输出结果。
  • 核心算法用拓扑排序解决电路的依赖问题:每个门的输出必须等所有输入都就绪后才能计算,通过入度表和队列保证计算顺序正确。

分析总结

优点是逻辑集中,初期写得快,顺着流程一口气就能写完;缺点非常明显:main方法过于臃肿,分支多、嵌套深,出了问题要在几百行里定位,加新功能就要改主方法,扩展性很差。这一版的代码更像“把解题步骤翻译成Java”,还没有面向对象的设计意识。

2. 作业集5:扩展复杂器件(V2版本)

核心指标

有效语句增加到166条,分支占比13.3%;依然是2个类,但方法数量明显增加,新增了器件解析、引脚解析、计算逻辑等独立方法,单方法平均语句数从14.8降到了5左右。

设计思路

在不改动整体架构的前提下,兼容了四种新器件:

  • 扩展parseComp方法,新增对S、M、Z、F四种器件的名称解析,分别计算控制引脚数、数据引脚数、输出引脚数。
  • 把计算逻辑从main里拆成独立的calcComp方法,用switch-case按器件类型执行不同的计算逻辑。
  • 新增了按器件类型分类的打印方法,不同器件输出格式不同,各自独立处理。

分析总结

这一版开始有意识地拆分方法,把“解析、计算、打印”分开,单方法复杂度下降了。但核心问题依然存在:所有器件共用一个C类,靠类型字段区分行为,加新器件就要改switch-case,违反开闭原则;数据和行为没有绑定,计算逻辑全在主类里,还是面向过程的内核。

3. 作业集6:面向对象重构+子电路支持(V3版本)

核心指标

类的数量大幅增加:引入抽象基类Component,以及AndGateOrGateNotGate等多个具体门电路子类,还有引脚信息、子电路定义等辅助类;方法粒度进一步细化,平均每个方法仅3-4条语句;最复杂的方法从main转移到了子电路展开与连接校验的业务方法中。

设计思路

这一版做了彻底的面向对象重构:

  • 多态替代分支:抽象出Component基类,定义输入设置、就绪判断、计算输出的通用方法,每个具体门电路写子类重写calc()方法。新增器件只需要加新子类,不用修改原有代码,符合开闭原则。
  • 职责下沉:器件自己的计算逻辑放在自己的类里,主类只负责流程调度、连线解析、信号传递,不再关心具体门的运算规则。
  • 子电路支持:新增SubDef类封装子电路的端口定义和内部连线,通过队列+前缀拼接的方式递归展开嵌套子电路,把所有内部器件扁平化后统一计算。
  • 错误前置校验:新增连接规则校验,比如一条连线只能有一个输入源、输入引脚不能重复连接、输入输出顺序不能颠倒,出错立即给出明确提示。

类结构说明

用PowerDesigner梳理的类图核心关系为:

  • 抽象类Component 派生出 与门、或门、非门、异或门、同或门 五个具体子类
  • SubDef 类聚合管理子电路内部的器件与连接关系
  • PinInfo 类封装引脚的类型、归属、编号等解析结果
  • Main 类作为入口,负责输入解析、子电路展开、信号传播计算、结果输出

整体从“一个主方法跑全程”变成了“多个类各司其职,协作完成功能”,架构清晰了很多。

三、采坑心得

三次作业做下来,踩的坑大多不是语法问题,而是“想当然”的逻辑漏洞和设计缺陷,每一个都是调试出来的实感。

1. 引脚编号约定的坑

这是作业4最折磨我的一个问题。
一开始我默认引脚编号从0开始,结果计算出来的结果全错。调试了半天才发现规则:器件的输出引脚固定是0,输入引脚从1开始计数。我解析的时候把输入引脚当成从0开始,导致依赖关系全乱,拓扑排序的入度计算完全不对。
改完引脚编号的约定后,又踩了第二个坑:没有校验引脚数量匹配。比如一个2输入的与门,只接了1个输入,代码依然往下算,直接空指针。后来加了校验:所有器件计算前先判断实际引脚数是否等于预期输入数,不一致就标记为无效,不参与后续计算,这才把边界兜住。
也正是这些校验逻辑,让第一版代码的分支占比冲到了28.4%,基本全是边界判断。

2. 字符串解析的边界坑

输入格式看起来简单,但实际跑起来各种边界情况:

  • 空行、行首行尾的多余空格,直接split会得到空字符串,解析器件名的时候直接报错。后来所有行都先trim(),拆分后过滤空串,才解决。
  • 器件名称格式比如A(3)10,一开始用字符串切割找括号,经常索引越界。后来改成正则表达式匹配,一次性提取类型、输入数、编号,稳定性高了很多。
  • 子电路的端口名和普通输入名重名的问题,一开始没区分作用域,导致信号覆盖,结果错误。后来展开子电路的时候统一加前缀,用命名空间隔离,才彻底解决。

3. 面向对象拆分的职责模糊坑

作业6重构的时候,我一开始走了弯路:把所有计算逻辑都放在主类里,类只负责存数据,美其名曰“面向对象”,其实还是面向过程套了个类的壳。
后来翻课件的时候看到“信息专家原则”:谁拥有数据,谁就负责处理相关逻辑。器件自己有输入引脚数据,就应该自己算输出;货舱自己有货物列表,就应该自己算当前载重。想明白之后,我把calc()方法下沉到每个器件子类里,主类只负责传信号,代码立刻清爽了。
这个转变也能从度量数据里看出来:前两版最复杂的方法都是main,第三版最复杂的方法变成了子电路展开和连接校验的业务方法,主方法只做流程调度,复杂度降了一大截。

4. 子电路嵌套的递归坑

做子电路功能的时候,一开始用递归展开,遇到嵌套层级深的情况容易栈溢出,而且重复展开相同的子电路会浪费性能。
后来改成了广度优先的队列展开:把遇到的子电路放进队列,逐个展开并加前缀标记,遇到已经展开过的就直接复用,既避免了栈溢出,也不会重复创建对象。同时增加了环路检测,防止子电路自己引用自己导致死循环。

四、改进建议

虽然三个版本的功能都能跑通,但从代码质量的角度,还有不少可以持续优化的地方。

1. 功能层面的改进

  • 完善错误提示:现在的错误提示还比较粗糙,比如“引脚不存在”“器件类型未知”这类问题,只返回null不报错,用户不知道哪里错了。后续可以自定义异常类,给出具体的行号、错误位置,方便调试。
  • 增加波形仿真功能:现在只能计算静态输入的输出,后续可以扩展支持时序逻辑,比如加D触发器、时钟信号,输出波形结果,更贴近真实的电路仿真。
  • 支持文件输入输出:现在只能控制台输入,题目多了很麻烦,后续可以加文件读写,直接读取电路文件输出结果。

2. 代码架构的改进

  • 引入工厂模式:现在创建器件还是手动switch判断,后续可以写一个ComponentFactory工厂类,把器件创建的逻辑封装起来,主类不用关心具体怎么实例化。
  • 优化子电路展开:现在是全量展开成扁平结构,子电路复用多的时候会创建大量重复对象。后续可以改成子电路实例化,共享定义、独立状态,节省内存。
  • 基于接口编程:比如定义CalculableConnectable接口,不管是基础门还是子电路,都实现相同的接口,主流程统一处理,不用区分是基础器件还是子模块。

3. 编码习惯的改进

  • 减少魔法值:现在代码里很多直接写的数字,比如输出引脚0、译码器控制引脚数3,后续都要定义成常量,比如OUTPUT_PIN = 0,可读性和可维护性都会更好。
  • 规范变量命名:有些变量名太简略,比如cMapvalMap,虽然自己能看懂,但时间久了或者别人看就很费劲,改成componentMapsignalMap会清晰很多。
  • 补充单元测试:现在都是手动测样例,边界情况很容易漏。后续针对每个器件的计算方法、解析方法写单元测试,重构的时候跑一遍就能知道有没有改坏,效率更高。

五、总结

三次作业做下来,最大的收获不是写完了三道题,而是真的感受到了“代码质量”和“设计思维”的重要性。

在知识层面,我有两个明显的成长:
第一是算法落地的能力。以前拓扑排序只是课本上的一个算法,这次真的用它解决了电路依赖顺序的实际问题,明白了“算法不是背的,是用来解决具体问题的”。同时也把数字电路的理论知识和代码实现对应了起来,两边都理解得更透了。
第二是面向对象的思维转变。从最开始“类就是装数据的结构体”,到后来理解封装、多态、职责划分,明白面向对象不是为了凑语法,而是为了让代码更易扩展、更好维护。加一个新器件,从改几十行代码变成加一个新类,这种便利是实实在在的。

当然也清楚自己还有很多不足:
一是设计模式的应用还很生疏,知道开闭原则、单一职责,但真写代码的时候还是会不自觉写成面向过程,需要刻意练习。
二是异常处理和健壮性做得不够,很多错误场景只是简单跳过,没有给出友好的提示,离“工业级代码”还有差距。
三是测试意识薄弱,基本靠手动测样例,没有单元测试的习惯,后面要补一下JUnit的使用,养成测试驱动的习惯。

接下来的学习里,我会继续打磨代码细节,把面向对象的思想用得更熟练,多琢磨“怎么写得更好”,而不是只满足于“能跑就行”。

posted @ 2026-06-24 21:11  袁锟  阅读(3)  评论(0)    收藏  举报