面向对象作业集4-6总结

前言:
课程第二阶段完成了,一共完成了三次作业集,从这三个代码中,我不仅学到了数字逻辑仿真的实现细节,更重要的是学会了如何通过迭代重构提升代码的扩展性、健壮性和可读性。关键收获包括:
• 设计可扩展的数据结构以适应新器件。
• 利用队列实现事件驱动仿真。
• 使用正则表达式解析复杂语法。
• 引入错误处理机制提高用户体验。
• 模块化设计(子电路)应对系统复杂度。
• 常量与枚举减少硬编码。
• 方法拆分与职责分离增强可维护性。

题量:

题集 类数 方法数 代码行数 功能点数
题集4 2(Main、Gate) 6(Gate构造、shezhiShuru、shifouJiuXu、jisuanShuchu;Main的main、jieximingcheng) 175 5(支持与、或、非、异或、同或五种基本门,单输出,基础信号传播)
题集5 2(Main、Gate) 8(Gate构造、initRequiredPins、setPin、allRequiredSet、compute、hasOutput;parseGateName、main) 288 9(在五次基础上增加三态门、译码器、多路选择器、解复用器,支持多输出和可变引脚配置)
题集6 6(Main、Gate、SubCircuit、Connection、SubCircuitInstance、TokenType) 约15(含各构造、setInput、allInputsReady、computeOutput、compute、parseGateName、analyzeToken、main、辅助函数等) 534 12(九种门 + 子电路定义与实例化 + 层次化信号传播 + 输入冲突/语法错误检测 + 多实例命名空间隔离)

难度:
从三个版本的代码演化中,可以清晰地看到一条从简单原型到功能丰富再到层次化可复用系统的典型软件演进路径:版本一聚焦于五种基本门电路的信号传播,逻辑直观、结构单一,难度最低,适合入门理解事件驱动仿真;版本二扩展至九种器件并引入多输出和可变引脚配置,通过required数组和配置参数提升了灵活性,但逻辑分支增多、计算复杂度上升,难度达到中等;版本三则通过子电路机制实现模块化设计,增加了定义与实例分离、命名空间管理、层次化信号传播以及完整的错误检测,解析和算法复杂度显著提高,综合难度最高。每一个版本都不是简单的功能堆砌,而是在前一版本的基础上,针对暴露出的设计瓶颈进行有针对性的重构与增强,体现了“迭代开发”和“渐进增强”的工程实践智慧。

设计与分析:

第一次作业:

作业要求:
实现数字电路模拟程序,根据输入信号和连接关系驱动与门(A,多输入全1出1)、或门(O,多输入全0出0)、非门(N,单输入取反)、异或门(X,两输入不同出1)、同或门(Y,两输入相同出1),按A、O、N、X、Y顺序输出所有输入引脚齐全且能计算出输出的元件的输出引脚电平(格式“元件名-0:值”),忽略引脚不全或无法计算的元件。

实现方式:

  1. 解析建图:用正则[AO](\d+)\d+和[NXY]\d+提取门名,将门存入Map<String, Gate>。同时解析[源 目标1 目标2...],直接构建Map<String, List>邻接表(源到目标字符串的映射)。
  2. 数据模型:Gate类存Integer[] yinjiao(引脚值,null表示未设置)、int yifenshu(已设置的引脚数量)、Integer shuchu(单输出)。隐式约定:所有引脚都必须设置才能计算(yifenshu == yinjiaoshu),没有“可选引脚”概念。
  3. 种子入队:将INPUT:A-1解析出的(信号名, 值)放入Queue<Object[]>。
  4. 队列循环传播:
    ◦ 弹出(源, 值),遍历该源连接的所有目标字符串。
    ◦ 若目标是门名-引脚号,调用gate.shezhiShuru(pin-1, val)——底层数组赋值,yifenshu++。
    ◦ 若yifenshu == yinjiaoshu(全部就绪),立即执行jisuanShuchu()(switch五种门逻辑),将结果赋值给shuchu,并构造新事件(门名-0, 结果)入队。
  5. 结果收集:遍历所有门,筛选shuchu != null的。排序依据[A,O,N,X,Y]和门编号,最终输出g.name+"-0:"+g.shuchu。
    关键实现特点:完全依赖“置数计数”判断就绪,没有required数组;输出固定为-0引脚;无任何错误检查;门名解析和计算逻辑紧密耦合在main和Gate中。

代码规模:
image

类图:

仅有 Main 和 Gate 两个类,Gate 使用简单数组和计数方式管理引脚,方法命名使用拼音。
image

复杂度分析:
第四次作业复杂度如下:
image

Gate 类仅封装门逻辑(单一、内聚),而 Main 类承担解析、调度、传播和格式化等全局控制任务,包含大量分支与循环,因此复杂度远高于 Gate。

踩坑心得:

公测:
门未提前批量初始化,下游级联门找不到实例,信号被丢弃;
2. 门输出节点没有注册进连接表,多级电路没有路由,信号无法向下传递;
3. 早期过多的格式过滤逻辑,误拦截合法连接、丢失连线关系;
导致一直卡在95分

image

自测:
目前来说没发现什么问题。

改进:

当前代码虽能实现电路仿真功能,但存在逻辑高度耦合、缺少统一异常校验、硬编码较多、变量命名简略、无注释、不易后续迭代扩展的问题,可通过方法拆分、常量提取、完善输入校验、规范命名并添加注释来优化提升。

第二次作业:

作业要求:
实现数字电路模拟程序,根据输入信号和连接关系驱动与门(A)、或门(O)、非门(N)、异或门(X)、同或门(Y)、三态门(S,控制为1导通否则高阻)、译码器(M,满足S1=1且S2+S3=0时根据输入编码选一路输出0其余1)、数据选择器(Z,控制端选一路数据输出)和数据分配器(F,控制端选一路输出其余无效'-'),按A、O、N、X、Y、S、M、Z、F的顺序输出所有有效元件的输出,其中单输出元件输出“元件名-引脚号:值”,译码器输出“元件名:输出0的引脚编号”,分配器按输出引脚顺序输出带“-”的字符串,忽略无效输出。

实现方式:

  1. 增强解析:parseGateName返回Object[]{type, num, controlCount, inputCount, outputCount, totalPins}。例如M(2)3解析出控制数2、输入数4(1<<2)、总引脚数2+4+1=7。解析时立即用这些参数实例化Gate,并调用initRequiredPins()标记哪些引脚是必需的(如三态门的使能引脚0和数据引脚1是必需的,输出引脚2不是输入,不标记)。
  2. 数据模型重构:Gate用Integer[] pinValues存值,boolean[] required标记必要性,Map<Integer,Integer> outputs支持多输出引脚(如译码器有多个输出位)。不再用“计数”,而是用allRequiredSet()遍历required数组检查是否全部赋值。
  3. 队列传播细化:
    ◦ setPin(pinIdx, val)赋值,compute()中先调用allRequiredSet()。
    ◦ 计算后,outputs可能包含多个键值对(如译码器给多个输出引脚赋值)。
    ◦ 遍历outputs.entrySet(),将每个(门名-引脚号, 值)作为独立事件入队,实现真正的多扇出。
  4. 特殊输出格式化:
    ◦ 对于普通门和MUX,取outputs中第一个值,输出name-输出引脚:值(三态门输出引脚固定为2)。
    ◦ 对于DECODER,遍历outputs找到值为0的引脚,输出name:选中索引。
    ◦ 对于DEMUX,按输出引脚顺序拼接位串,输出name:位串。
    关键实现特点:引入了“必需引脚”抽象,使得多输入门(如与门可接受任意数量输入)和带控制引脚的门(如三态门)能统一处理;多输出支持让译码器等器件得以实现;但仍是单层仿真,无模块化。

代码规模:
第五次作业规模如下:
image

类图:
Gate 增加了 required、outputs 等字段,支持多输出和必需引脚标记,Main 增加常量定义,解析方法返回更丰富的配置信息。
image

复杂度分析:
第五次作业复杂度如下:
image

因为 Gate 类仅封装引脚赋值、必需性检查及门逻辑计算(以 switch 分支为主,各分支内无复杂嵌套),而 Main 类承担输入解析、正则匹配创建门实例、队列驱动的信号传播、多门型输出格式化等全局流程,包含多重循环、异常处理、条件判断和比较器逻辑,导致圈复杂度远高于专注于单一职责的 Gate类。

踩坑心得:

公测:
本次编写数字电路仿真程序时,我既要面对不同逻辑门、三态门、译码器、多路选择器与分配器等器件引脚定义、使能规则、运算逻辑极易混淆出错的问题,还要处理基于队列的信号事件驱动下引脚仅可首次赋值导致多级电路电平无法更新、分步引脚赋值提前运算造成器件无法正常计算的时序调度难题,同时需要通过多类正则完成不同格式器件名称的参数解析、正确解析电路拓扑连线并按指定规则排序输出,再加上各类器件差异化的输出格式要求、异常边界情况考虑不足,且受限于作业查重只能在原有代码结构内做最小范围调试,使得基础元件与复杂电路两类测试错误反复出现、故障根因难以快速定位,整体调试排查难度较高。导致最后只有93分。

image

image

自测:
在题集结束后,我想尝试将这个题集完成,但还是失败了,一直卡在93分。

改进:

可通过将内部类 Gate 独立为顶层类并封装字段、用枚举替代字符常量定义门类型、使用 record取代 Object[] 提升类型安全、将 main 方法拆分为解析、构建、传播、输出等多个独立方法、将门计算逻辑和输出格式化职责内聚到门类或策略类中、引入 Signal 和 PinRef 等领域类、增加输入校验和异常处理、避免混合中英文命名、使用 switch 表达式和 Stream API 等现代特性、并将核心逻辑抽取为可测试的非静态方法等一系列重构手段,以降低圈复杂度、增强可维护性和可扩展性。

第三次作业:

作业要求:

请编程实现一个数字电路模拟程序,支持与、或、非、异或、同或五种基本逻辑门(允许多输入与门/或门,其他为单/双输入),并能通过输入格式定义主电路中的输入信号、引脚连接以及子电路(子电路需先定义并可在主电路中通过编号引用其输入/输出引脚),同时需检测并优先报告连接信息中的多种异常(包括包含多个输入、无输入、无输出、输入输出顺序错误、输入引脚信号冲突等),最终按与、或、非、异或、同或门的顺序及编号升序输出所有有效门的输出引脚电平,忽略未接有效输入的门。

实现方式:

  1. 两阶段解析:
    ◦ 阶段一(子电路定义):扫描Cid:块,提取INPUT:和OUT:列表,以及内部[连接]行,构建SubCircuit对象(包含门列表、连接列表、输入输出名称列表),存入subCircuits映射。
    ◦ 阶段二(主电路):扫描剩余行,解析主电路的INPUT:和[连接]。同时,用analyzeToken()函数分析每个标记(Token)的类型——是主输入?是子电路实例的引脚(C1-IN)?还是门引脚?该函数会结合subCircuits的输入/输出列表,判断C1-IN是子电路的输入还是输出,为后续错误检查做准备。
  2. 数据模型分层:
    ◦ SubCircuit是模板,存逻辑定义。
    ◦ SubCircuitInstance是运行时实例,包含prefix(如C1-)、inputValues映射、gateOutputs映射和outputValues映射。每个实例独立维护状态,通过前缀隔离命名空间。
  3. 冲突检测(关键新增):
    ◦ 在构建主连接图时,维护targetSourceMap(目标→源)。
    ◦ 若同一目标被多个源驱动,报ERROR: target input signal conflict。
    ◦ 检查连接源是否isOutput,目标是否包含isOutput,若违反则报ERROR: [...] input and output sequence error或include more than one input。
  4. 层次化队列传播:
    ◦ 主队列初始包含主输入信号和指向子电路引脚的信号(如C1-IN)。
    ◦ 当事件目标是子电路引脚(C1-IN)时,找到对应的SubCircuitInstance,将值填入其inputValues。
    ◦ 一旦该实例的inputValues.size() == def.inputs.size()(所有输入就绪),调用instance.compute()。
    ◦ 在compute()内部,克隆子电路定义中的所有门(避免影响模板),构建内部邻接表,然后执行一次完整的独立队列传播(递归使用相同的事件驱动模型),计算结果存入instance.gateOutputs和instance.outputValues。
    ◦ 最后将instance.outputValues中的输出事件(如C1-OUT)放回主队列,继续驱动上层电路。
  5. 结果合并与排序:
    ◦ 收集主电路中所有有输出的Gate,以及所有已计算实例的gateOutputs。
    ◦ 输出行可能带前缀(如C1-GATE-0:1)。排序前用extractType()和extractNumber()剥离前缀,提取原始门类型和编号,按[A,O,N,X,Y]排序,再输出。
    关键实现特点:通过“定义-实例”分离实现模块复用;通过递归调用队列传播实现层次仿真;通过在解析阶段前置语义检查保证输入合法性;通过前缀机制保证多实例下命名不冲突。

代码规模:
第六次作业规模如下:

image

类图:
新增 SubCircuit、Connection、SubCircuitInstance、TokenType 四个辅助类,实现了子电路定义、实例化、连接抽象和标记类型分析,Gate 简化回单输出模式,但通过 SubCircuitInstance 支持层次化仿真。Main 的方法也相应增加,负责解析、错误检测和协调仿真。
image

复杂度分析:

第五次作业复杂度如下:
image

Main 集成了解析、校验、实例化、传播、排序、输出等所有控制逻辑,而其他类仅作为被动数据模型。这种集中化设计导致 Main 成为整个程序的“大脑”,自然拥有最多的分支、循环和嵌套,从而复杂度远高于其他辅助类。若要降低,需将 Main 拆分为多个独立模块,每个模块负责单一关注点。

踩坑心得:

公测:
本人和本人室友觉得这次题集很难,经过长期挣扎,最终只得73分。有以下困难:

  1. 子电路内部连接格式:源必须是输出信号(输入端口、门输出或子电路端口),目标必须是输入引脚(门输入或子电路输入),且信号通过连接显式传递。
  2. 空目标处理:题目要求区分源信号类型,输出正确的错误信息(include none output 或 include none input)。
  3. 子电路输出端口:题目样例(如样例1)只要求输出门输出(包括子电路内部门输出),不要求输出子电路端口(如 C1-X)。

image
image

自测:
应最后没得到满分,一直在想办法得分,当还是失败。

自测:
代码存在结构混乱、可读性差、重复解析逻辑、错误处理不完善及缺乏注释等问题,需重构为模块化设计并加强输入校验与异常处理。

总结:
从版本一到版本三,这三个代码分别从2个类演进到6个类,从5种基本门扩展到9种器件并引入子电路层次化设计,生动展示了“事件驱动队列传播”这一核心算法的不变性与数据结构、解析策略、错误处理、状态管理持续演化的辩证关系,让我们深刻领悟到数据结构决定表达上限、配置化设计提升扩展性、定义实例分离实现模块复用、前置语义检查保障健壮性等工程原则,同时也暴露出正式语法分析、时序逻辑仿真、循环依赖检测、性能优化、设计模式重构、交互式可视化、HDL编译原理、形式化验证、参数化模块和单元测试等亟待深入研究的广阔方向,最终告诉我们:从“写出可用程序”到“构建可维护系统”的跨越,本质上是不断权衡抽象层次、管理复杂度、预见扩展需求的过程,而每一次迭代都是对软件工程核心智慧的实践诠释。

需要进一步学习及研究的方向:

  1. 正式的词法分析与语法分析
    当前使用正则表达式进行解析,虽然对当前语法足够,但随着语法不断复杂化(例如加入if条件生成、循环结构、参数化模块),正则将变得无法维护。
    学习:
    • 递归下降解析器:手写Tokenizer + Parser,实现更结构化的语法分析。
    • ANTLR / JavaCC:使用解析器生成器,用EBNF文法描述语法,自动生成解析代码。
    • 抽象语法树(AST):解析后构建AST,再对AST进行语义分析和仿真,而不是边解析边执行。

  2. 循环依赖与拓扑排序的检测
    当前代码假设电路是无环的组合逻辑。如果出现组合逻辑环路(如A的输出连接到B,B的输出又连接回A),队列传播将无法终止或产生不确定结果。
    学习:
    • 在仿真前对电路图进行拓扑排序,检测是否存在有向环。
    • 对于可合法的时序环路(如锁存器),需要引入迭代收敛法或断环分析。

  3. 性能优化与大规模仿真
    当门数量从几十个扩展到几百万个时,O(N)遍历所有门来收集输出会变得不可接受。
    学习:
    • 分层事件驱动:将电路划分为时间片或层次区域,只激活受影响的子图。
    • 增量计算:只重新计算输入发生变化的部分。
    • 并行仿真:利用多线程或GPU并行计算相互独立的门。

  4. 仿真可视化与交互式调试
    当前输出仅为文本,无法直观观察信号随时间的变化。
    学习:
    • 波形图生成:输出VCD(Value Change Dump)格式,用GTKWave等工具查看波形。
    • 交互式调试器:允许用户设置断点、单步执行、观察信号值变化。

  5. 形式化验证
    仿真只能验证有限测试用例下的行为,无法证明电路在所有输入下的正确性。
    建议学习:
    • 模型检查(Model Checking):用状态空间搜索验证性质。
    • 等价性检查(Equivalence Checking):验证两个电路实现是否功能等价。
    • SAT/SMT求解器在硬件验证中的应用。

posted @ 2026-06-22 23:41  srjtbb  阅读(4)  评论(0)    收藏  举报