数字电路模拟程序迭代开发总结

一、前言

1.1 作业系列概述

在《面向对象程序设计》课程的本阶段学习中,我们完成了一个极具挑战性且高度贴合底层硬件逻辑的“数字电路模拟程序”系列作业。三次作业以敏捷迭代的方式展开:从最基础的五种逻辑门电路模拟,逐步扩展为包含三态门、译码器、数据选择器等复杂组合逻辑元件的系统,最终演进为支持子电路实例化、具备严格输入合法性校验的层级化电路仿真平台。整个过程不仅考察了 Java 语言的高级特性,更对面向对象中的接口抽象、设计模式应用(如组合模式)、以及复杂业务规则的解耦与建模进行了极为深刻的训练。

 

1.2 题量、难度

第一次作业(数字电路模拟程序-1)需实现与、或、非、异或、同或门,代码量约 150 行。主要考察接口与抽象类的应用,以及基于图的信号传播思想。由于元件单一、引脚规则固定(输入从1开始,输出固定为0),难度适中,重点在于建立“元件-连接-信号”的核心数据模型。

 

第二次作业(数字电路模拟程序-2)新增了四种复杂元件,代码量激增至 300 行以上。难度陡然上升,核心痛点在于引脚编号规则的突变——引入了“控制引脚、输入引脚、输出引脚”的顺序排序机制,且不同元件的输出引脚号不再固定为0。此外,输出格式出现了分化(如分配器需要输出带“-”的无效状态字符串),极大地考验了代码的扩展性。

 

第三次作业(数字电路模拟程序-4,跳过了时序电路的模拟程序3直接迭代)达到了本系列的复杂度巅峰,代码量逼近 450 行。引入了“子电路”概念和五大类异常输入校验。需求要求运用组合模式将子电路作为特殊元件嵌套,并要求以极其严苛的优先级顺序解析并报出连接异常。此次作业综合性极强,难度偏难,真正考验了在高压下进行架构重构与防御式编程的能力。

 

1.3 三次作业所覆盖的核心知识点

  • 面向对象设计原则:深度实践单一职责原则(SRP)与开闭原则(OCP),在新增元件时尽量减少对原有计算引擎的侵入。
  • 设计模式:第三次作业重点应用了组合模式,将子电路与基本门电路抽象为统一的接口,实现了树形结构的递归处理。
  • 图论思想应用:虽然没有显式写出图的数据结构,但电路网络本质上是有向无环图(DAG),信号传播采用了类似拓扑排序的迭代轮询机制。
  • 复杂字符串解析与状态判定:通过大量的 String 截取、正则匹配以及上下文环境类,实现了对如 A(8)1-2C1-X1-0 等复合标识符的精准词法分析。
  • 防御式编程与优先级控制:通过计数器与标志位结合的方式,实现了复杂异常情况下的严格优先级分支控制。
 

1.4 核心学习目标

通过本系列作业,期望达到以下目标:能够独立从复杂的物理业务(电路原理)中提炼出合理的类结构;深刻理解组合模式在构建层级系统中的优势;掌握在不能修改底层核心算法的前提下,通过“预处理展开”等技巧实现上层功能迭代的工程化手段;建立对输入数据“永不信任”的防御式编程意识,并能编写出高内聚、低耦合的校验逻辑。

 

二、设计与分析

2.1 第一次作业:基础逻辑门电路模拟

2.1.1 需求回顾

系统需要模拟五种基本逻辑门。输入包括外部信号源和元件间的引脚连接信息。程序需根据连接关系,从输入端逐步推演,计算出所有能够确定状态的元件输出,并按照元件类型和编号的特定字典序输出结果。未接收到有效输入的元件需被忽略。

2.1.2 关键代码分析

本次作业的核心逻辑集中在 Gate 抽象类的 process() 方法以及 Circuit 类的 calculate() 迭代驱动机制中。process() 方法并不直接执行逻辑运算,而是首先遍历当前元件的所有输入引脚,通过连接映射表去全局的 signals Map 中查找源信号。只有当所有输入引脚都找到有效值时,才调用子类实现的 calculate() 抽象方法得到结果,并将结果存回 signals Map。calculate() 方法则采用了一个“状态机”式的 while 循环,不断轮询所有元件,只要有一处计算出新的信号(返回 true),就开启下一轮遍历,直到某一轮毫无变化为止。这种将“信号查找与更新”放在父类、“具体运算”下沉到子类的设计,初步实现了控制流与业务逻辑的分离;而以 Map 作为全局信号总线,巧妙地解耦了元件之间的直接对象引用,使得网络拓扑的构建变得异常灵活。

下面为作业一代码分析图:

image

代码分析总结:

项目规模与结构: 项目被合理地拆分为了多个独立的 .java 文件(如 AndGate.javaCircuit.javaGate.java 等),总计包含 8 个类/接口,代码总行数在 150~200 行之间。相比于将所有代码塞进一个主类,这种物理上的拆分完美映射了逻辑上的“接口-抽象类-具体实现类”的继承层次结构,类的颗粒度划分十分清晰。

代码质量与复杂度: 整体代码质量极佳,复杂度处于极优水平。具体逻辑门实现类(如 AndGateOrGate 等)的语句数极少,基本只包含单层的 if 判断或简单的 for 循环,平均圈复杂度和嵌套深度都极低,阅读起来毫无负担。从报表数据可以推断,项目的最大复杂度最高分支比例(% Branches) 大概率集中在 Circuit 类的 checkAndCreate() 方法中。因为该方法需要通过 indexOfsubstring 对诸如 A(8)1 这样不规则的字符串进行大量的存在性判断和截取操作,产生了较多的条件分支,是整个项目中唯一稍微显得臃肿的地方。

潜在缺陷与改进点: 延续了初版代码的通病,图表中的注释率(% Comments)必然为 0.0%。虽然得益于良好的方法命名(如 processcalculate),代码做到了一定程度的“自解释”,但完全缺乏对于核心设计思想(如利用 Map 模拟全局信号总线、迭代轮询机制的原理)以及复杂字符串解析规则的注释说明。随着后续作业元件类型的增加和解析逻辑的复杂化,这种“零注释”状态将会严重阻碍代码的维护与迭代。

2.1.3 类的设计与分析

下面是第一次作业的类图:

image

从图中可知:

 

Component(组件接口):

属性: 无。

方法: 定义了四个方法——getName 获取名称、getId 获取编号、getOutputValue 获取输出值、process 根据连接信息和信号映射执行处理逻辑。

关系: 它是 Gate 的行为规范,所有门电路都必须遵循该契约。

 

Gate(门电路抽象类):

属性: name(String,私有)、id(int,私有)、inputCount(int,私有)、outputValue(int,私有,初始值为 -1 表示未计算)。

方法:

  构造函数: 初始化名称、编号和输入引脚数。

       getName / getId / getOutputValue / getInputCount: 各属性的 Getter 方法。

  calculate: 抽象方法,由子类实现具体的逻辑运算。

  process: 实现了 Component 接口的处理逻辑——遍历所有输入引脚,从 connectInfo 中查找信号源,从 signals 中读取信号值,组装成数组后调用 calculate,再将结果写回 signals,若已计算过则返回 false 避免重复计算。 关系: 它实现了 Component 接口,是所有具体门电路的父类,与 Component 是实现关系

 

AndGate(与门类):

属性: 无新增属性。

方法: 构造函数调用 super 委托父类初始化;calculate 遍历输入数组,所有值为 1 才返回 1,否则返回 0。

关系: 继承自 Gate,是泛化关系(继承)

 

OrGate(或门类):

属性: 无新增属性。

方法: 构造函数调用 supercalculate 遍历输入数组,任一值为 1 即返回 1,全为 0 才返回 0。

关系: 继承自 Gate,是泛化关系(继承)

 

NotGate(非门类):

属性: 无新增属性。

方法: 构造函数调用 super(固定输入数为 1);calculate 对输入取反,0 变 1、1 变 0。

关系: 继承自 Gate,是泛化关系(继承)

 

XorGate(异或门类):

属性: 无新增属性。

方法: 构造函数调用 super(固定输入数为 2);calculate 比较两个输入,不同返回 1,相同返回 0。

关系: 继承自 Gate,是泛化关系(继承)

 

XnorGate(同或门类):

属性: 无新增属性。

方法: 构造函数调用 super(固定输入数为 2);calculate 比较两个输入,相同返回 1,不同返回 0,逻辑与 XorGate 相反。

关系: 继承自 Gate,是泛化关系(继承)

 

Circuit(电路类):

属性: allMap(Map<String, Component>,私有)——按名称存储所有已创建的组件;list(List<Component>,私有)——组件列表,用于排序输出;connectInfo(Map<String, String>,私有)——存储引脚间的连接关系(目标引脚 → 信号源);signals(Map<String, Integer>,私有)——存储各节点的信号值。

方法:

  • addInput: 将外部输入信号写入 signals
  • addConnection: 解析连接关系,写入 connectInfo,并调用 checkAndCreate 确保目标门电路已创建。
  • checkAndCreate: 根据命名规则(首字符判断类型)解析门电路类型和参数,动态创建对应的 Gate 子类实例并注册到 allMaplist 中。
  • calculate: 循环遍历所有组件调用 process,直到没有新结果产生为止(拓扑驱动计算)。
  • printResults: 按门类型权重和编号排序后,输出各组件的输出值。
  • getWeight: 辅助方法,为不同类型门电路赋予排序权重(A=1, O=2, N=3, X=4, Y=5)。

关系: 它与 Component 是关联关系,通过 allMaplist 持有组件的引用,但不负责组件的生命周期创建(创建逻辑在 checkAndCreate 中按需进行)。

 

Main(主类):

属性: 无。

方法: main 静态方法,通过 Scanner 逐行读取输入——INPUT: 行解析外部输入信号,[...] 行解析连接关系,end 结束输入。随后调用 calculate 执行计算,调用 printResults 输出结果。

关系: 它与 Circuit 和 Cargo 是依赖关系,仅在 main 方法中临时使用,不持有其引用。

2.1.4 心得

初次接触这种将“硬件连线”转化为“软件数据结构”的题目,最大的感受是视角的转换。过去写代码是线性的流水线,而这次面对的是一个网状的依赖图。我设计的 Circuit 类中采用了一个极为朴素的 while(flag) 循环:不断遍历所有元件尝试计算,如果有一个元件算出了新结果,就继续下一轮,直到某一轮没有任何新结果产生。这种“暴力轮询”虽然时间复杂度不高(因为节点少),但让我深刻理解了信号传播的时序问题——必须等所有输入就绪,元件才能动作。将具体逻辑运算下放到 AndGateOrGate 等子类,而将“何时计算”的控制权留在父类和上下文中,这是我第一次真切体会到多态带来的代码整洁感。

 

2.2 第二次作业:复杂组合逻辑与多态引脚

2.2.1 需求回顾

在原有基础上,新增三态门、译码器、数据选择器、数据分配器。引脚编号规则发生颠覆性变化,需按照“控制-输入-输出”的物理顺序重新映射。不同元件的输出表现差异巨大:三态门可能断开(无效),分配器只有一路有效其他为无效状态,译码器需输出有效输出的引脚索引号。

2.2.2 关键代码分析

本次作业的核心难点集中在各类复杂元件对“引脚偏移量”的动态计算,以及 Demux 类的 printResult() 方法对无效状态的处理上。由于引入了“控制-输入-输出”的连续编号规则,例如译码器 M(3)1 的 0-2 号是控制引脚,3-5 号才是输入引脚。因此,在 DecoderMuxprocess() 方法中,必须根据构造时传入的引脚数量,动态计算出输入引脚的起始索引(如 startInputPin = 3)和输出引脚的起始索引。在数据分配器的输出阶段,printResult() 方法遍历所有输出引脚,针对 Map 中存为 null 的值(代表高阻态或未选中状态),将其转化为 "-" 字符拼接到结果字符串中。这种将复杂的物理引脚映射规则封装在各个实体类内部的做法,避免了底层信号传播逻辑的混乱;而统一使用 null 代表无效电平,既简化了状态判断,又完美契合了分配器特殊格式的输出需求。

下面为作业二代码分析图:

image

代码分析总结:

项目规模与模块化演进: 随着三态门、译码器、选择器、分配器四种复杂元件的引入,项目文件数量显著增加。这表明代码严格遵循了“一类一文件”的工程规范,每种具有独立引脚映射和输出格式的元件都被隔离在独立的物理文件中,模块化程度极高,避免了单一文件的过度膨胀。

核心复杂度集中与解析瓶颈: 从图表数据可以精准定位到,项目的核心复杂度高度集中在了 Circuit.java(120行,86条语句,分支比例高达 34.9%)。相比于第一次作业,Circuit 类的分支率出现了明显的跃升。这是因为在第二次作业中,checkAndCreate() 方法必须承担极其繁重的“词法解析”工作:它不仅要区分带括号与不带括号的元件名(如 A(8)1N1),还要根据标识符(MZF)的不同,解析出控制引脚数并动态计算输入/输出引脚的起始偏移量。大量的 if-else 判断和字符串索引查找直接推高了该类的圈复杂度。

实体类的低耦合保持: 尽管管理类的解析逻辑变得复杂,但可以预见,新增的 Decoder.javaDemux.java 等具体元件实现类,其内部的 process() 方法依然保持了较低的复杂度。这说明“复杂规则解析”与“纯粹逻辑计算”的职责分离是成功的:脏活累活(字符串拆解)都被 Circuit 类挡在了外部,门电路类内部依然保持纯粹的数学与逻辑运算。

顽疾与隐患: 延续了前序版本的特征,项目的注释率(% Comments)必然依旧为 0.0%。在 Circuit.java 分支率逼近 35% 的情况下,完全缺乏注释是非常危险的。例如,startInputPin = 3 这种魔法数字,如果没有注释说明“0-2为控制端,3开始为输入端”的物理背景,后续维护时极易引发理解偏差和修改错误。随着第三次作业子电路的加入,这种“零注释”状态将成为巨大的技术债务。

2.2.3 类的设计与分析

下面是第二次作业的类图:

image

 

从图中可知:

Component(组件接口):

属性: 无。

方法: 定义了四个方法——getName 获取名称、getId 获取编号、process 根据连接信息和信号映射执行处理逻辑、printResult 根据信号映射输出结果。

关系: 它是所有组件的顶层行为规范,任何电路元件都必须遵循该契约。

BaseComponent(基础组件抽象类):

属性: name(String,受保护)、id(int,受保护)。

方法:

  • 构造函数: 初始化名称和编号。
  • getName / getId: 提供属性的 Getter 方法。 

关系: 它实现了 Component 接口,但未实现 processprintResult,将这两个方法的实现责任推迟给子类。它与 Component 是实现关系,是整个组件体系的中间层,起到属性复用的作用。

LogicGate(逻辑门抽象类):

属性:

  inputCount(int,受保护)——输入引脚数;outputValue(int,受保护,初始值为 -1 表示未计算)。

方法:

  • 构造函数: 调用 super 初始化名称和编号,同时设置输入引脚数。
  • process: 实现了通用的信号采集逻辑——遍历所有输入引脚,从 connMap 查找信号源,从 sigMap 读取信号值,组装成数组后调用 calculate,将结果写回 sigMap,若已计算过则返回 false 避免重复计算。
  • printResult: 从 sigMap 中读取输出引脚的值并打印,仅当值存在时才输出。
  • calculate: 抽象方法,由具体门电路子类实现各自的逻辑运算。

关系: 继承自 BaseComponent,是泛化关系(继承)。它将 processprintResult 的通用逻辑在此层统一实现,子类只需关注 calculate 的具体算法,体现了模板方法模式。

AndGate(与门类):

属性: 无新增属性。

方法: 构造函数调用 supercalculate 遍历输入数组,所有值为 1 才返回 1,否则返回 0。

关系: 继承自 LogicGate,是泛化关系(继承)

OrGate(或门类):

属性: 无新增属性。

方法: 构造函数调用 supercalculate 遍历输入数组,任一值为 1 即返回 1,全为 0 才返回 0。

关系: 继承自 LogicGate,是泛化关系(继承)

NotGate(非门类):

属性: 无新增属性。

方法: 构造函数调用 super(固定输入数为 1);calculate 对输入取反,0 变 1、1 变 0。

关系: 继承自 LogicGate,是泛化关系(继承)

XorGate(异或门类):

属性: 无新增属性。

方法: 构造函数调用 super(固定输入数为 2);calculate 比较两个输入,不同返回 1,相同返回 0。

关系: 继承自 LogicGate,是泛化关系(继承)

XnorGate(同或门类):

属性: 无新增属性。

方法: 构造函数调用 super(固定输入数为 2);calculate 比较两个输入,相同返回 1,不同返回 0,逻辑与 XorGate 互反。

关系: 继承自 LogicGate,是泛化关系(继承)

TriStateGate(三态门类):

属性: outputValue(int,私有,初始值为 -1)。

方法:

  • 构造函数: 调用 super 初始化名称和编号,无输入数参数(引脚固定:0 为控制端、1 为输入端、2 为输出端)。
  • process: 读取控制端信号,若控制端为 1 则将输入端信号透传到输出端,否则输出端置为 null,并用 -2 标记高阻态。
  • printResult: 仅在输出值有效(非 null 且非高阻态)时打印输出引脚的值。

  关系: 继承自 BaseComponent 而非 LogicGate,是泛化关系(继承)。因为三态门的引脚语义(控制端+输入端+输出端)与标准逻辑门(多个输入+一个输出)不同,无法复用 LogicGate 的 process 逻辑,所以直接继承 BaseComponent 并自行实现全部接口方法。

Decoder(译码器类):

属性: inputCount(int,私有)——数据输入位数;calculated(boolean,私有)——标记是否已完成计算。

方法:

  • 构造函数: 调用 super,设置输入位数。
  • process: 引脚 0~2 为使能端(S1=1, S2=0, S3=0 时使能),引脚 3 起为数据输入,根据数据输入的二进制值确定选中哪条输出线置 0,其余置 1;未使能时所有输出置 null。
  • printResult: 遍历所有输出引脚,找到值为 0 的那一条,以 名称:索引 的格式输出。  

关系: 继承自 BaseComponent,是泛化关系(继承)。原因与 TriStateGate 类似——译码器的引脚结构(使能端+数据输入+多条输出)与标准逻辑门差异较大,需要完全自定义 processprintResult

Mux(多路选择器类):

属性: outputValue(int,私有,初始值为 -1)、controlCount(int,私有)——控制端位数、inputCount(int,私有)——输入端数量(2 的 controlCount 次方)。

方法:

  • 构造函数: 调用 super,根据控制端位数自动计算输入端数量。
  • process: 先读取所有控制端信号计算选中索引,再读取对应输入引脚的值作为输出。
  • printResult: 输出引脚的值。

关系: 继承自 BaseComponent,是泛化关系(继承)。引脚结构为控制端 + 多路输入 + 单路输出,与 LogicGate 不兼容。

Demux(多路分配器类):

属性: calculated(boolean,私有)、controlCount(int,私有)——控制端位数、outputCount(int,私有)——输出端数量(2 的 controlCount 次方)。

方法:

  • 构造函数: 调用 super,根据控制端位数自动计算输出端数量。
  • process: 读取控制端信号确定选中索引,将输入信号送到对应输出引脚,其余输出置 null。
  • printResult: 将所有输出引脚的值拼接成字符串,有效值显示数字,无效值显示 -,以 名称:输出序列 格式输出。

关系: 继承自 BaseComponent,是泛化关系(继承)。引脚结构为控制端 + 单路输入 + 多路输出,与 LogicGate 不兼容。

Circuit(电路类):

属性: allMap(Map<String, Component>,私有)——按名称索引所有组件;list(List<Component>,私有)——组件列表用于排序输出;connectInfo(Map<String, String>,私有)——存储引脚连接关系(目标引脚→信号源);signals(Map<String, Integer>,私有)——存储各节点的信号值。

方法:

  • addInput: 将外部输入信号写入 signals
  • addConnection: 解析连接关系写入 connectInfo,并调用 checkAndCreate 确保相关组件已创建。
  • checkAndCreate: 根据命名规则(首字符判断类型,括号内为参数)动态创建对应的组件实例并注册。支持 9 种组件:A(AndGate)、O(OrGate)、N(NotGate)、X(XorGate)、Y(XnorGate)、S(TriStateGate)、M(Decoder)、Z(Mux)、F(Demux)。
  • calculate: 循环遍历所有组件调用 process,直到没有新结果产生为止(迭代式拓扑驱动计算)。
  • printResults: 按类型权重(A=1, O=2, N=3, X=4, Y=5, S=6, M=7, Z=8, F=9)和编号排序后,逐个调用 printResult 输出。
  • getWeight: 辅助方法,为不同类型组件赋予排序权重。

关系: 它与 Component 是关联关系,通过 allMaplist 持有所有组件的引用。它是整个系统的编排者,负责组件的创建、连接管理、计算调度和结果输出。

Main(主类):

属性: 无。

方法: main 静态方法,通过 Scanner 逐行读取输入——INPUT: 行解析外部输入信号,[...] 行解析连接关系,end 结束输入。随后依次调用 calculate 执行计算和 printResults 输出结果。

关系: 它与 Circuit 是依赖关系,仅在 main 方法中临时使用,不持有其长期引用。

2.2.4 心得

第二次作业对我第一次建立的类图结构造成了巨大冲击。原本我让所有元件继承自一个统一的 Gate 抽象类,假设输出引脚都是 0。但三态门的输出变成了 2,数据选择器的输出引脚号变成了动态计算的 controlCount + inputCount。为了不让基类变得臃肿不堪,我被迫进行了重构,将原来的 Gate 拆分为 BaseComponentLogicGate,让那些“异类”元件直接继承基类并重写 process()printResult()。在处理分配器的无效状态时,我选择在 signals Map 中存入 null 来表示高阻态/断开,在输出时通过判断 null 来拼接 "-"。这个设计虽然有点取巧,但完美解决了多态输出格式的问题,让我明白了当业务规则出现分化时,及时拆分类层级比勉强塞进同一个模板要明智得多。

 

2.3 第三次作业:子电路嵌套与异常校验

2.3.1 需求回顾

引入子电路概念,子电路有独立的输入输出端口,可在主电路中像普通元件一样被实例化和连线。同时,增加对连接信息的严格语法和语义校验,要求能精准识别“多个输出短接”、“无输入”、“无输出”、“顺序错误”、“输入冲突”五种异常,并严格按照给定优先级只报第一个错误。

2.3.2 关键代码分析

本次作业的核心设计亮点集中在 CircuitSimulator.validateConnection() 的异常优先级判定机制,以及 expandUsedSubCircuits() 方法的子电路降维策略上。在异常判定中,该方法没有采用容易相互干扰的连续 if-else 分支,而是先遍历一遍连接数组,利用独立的计数器精准统计出真正的源端(sourceCount)和目的端(destCount)数量,随后严格按照题设的优先级顺序进行独立判断并立即返回。在子电路处理方面,expandUsedSubCircuits() 采用了一种极为巧妙的“预处理展开”思想:当检测到主电路使用了某个子电路时,它不去修改底层的计算引擎,而是直接将子电路内部定义的所有连接信息,加上子电路实例的前缀(如 C1-),拍平追加到主连接列表中。这种“先统计后判决”的解耦设计彻底消除了复杂逻辑下的短路陷阱,保证了校验的绝对严密;而“降维展开”策略则完美体现了组合模式的变体应用,它以极小的预处理开销,彻底屏蔽了运行时递归解析嵌套结构的复杂性,让底层引擎实现了零修改兼容。

下面为作业三代码分析图:

image

代码分析总结:

项目规模与结构:随着新增了四种复杂的组合逻辑元件,代码被拆分成了更多的独立 .java 文件(比如新增了 Decoder.javaDemux.java 等),总代码行数增加到了 300 行左右。相比于把新代码全塞进以前的类里,这种“一个元件一个文件”的做法,对应上了我们学的“一个类只干一件事”的原则。虽然文件变多了,但各个类的分工依然很明确,没有乱成一锅粥。

代码质量与复杂度:新增的具体元件类(如译码器、选择器等)内部代码质量依然很好,主要就是简单的循环和加减乘除,看着不费劲。但是从报表数据能明显看出,整个项目的“复杂度大户”变成了 Circuit.java(120行,86条语句,分支比例达到了 34.9%)。这主要是因为里面的 checkAndCreate() 方法变难写了,它不仅要处理 A(8)1 这种带括号的字符串,还要区分 M 是译码器、Z 是选择器,并且要根据括号里的数字去算引脚号从几开始。大量的 if-else 和找括号、截取字符串的操作,让这个方法成了整个项目里最绕、最容易出 bug 的地方。

潜在缺陷与改进点:延续了上一次作业的老毛病,图表里的注释率(% Comments)依然是 0.0%。虽然方法名起得还算明白,但这次 Circuit 类和新增元件类里出现了很多像 startInputPin = 3 这样的“魔法数字”(代表0、1、2是控制端,3才是输入端)。

2.3.3 类的设计与分析

下面是第三次作业的类图:

image

Main(主类):

属性: 无。

方法: main 静态方法,通过 Scanner 逐行读取输入,过滤空行并将全角冒号统一替换为半角,遇到 end 结束读取。随后实例化 CircuitSimulator 并调用 run 方法启动仿真流程。

关系: 它与 CircuitSimulator 是依赖关系,仅在 main 方法中临时创建并使用。

CircuitSimulator(电路仿真调度器):

属性: subCircuits(Map<String, SubCircuit>,私有)——子电路定义库;subCircuitDefineOrder(Map<String, Integer>,私有)——子电路定义顺序;mainConnections(List<String[]>,私有)——顶层连线;topInputValues(Map<String, Integer>,私有)——顶层输入值;topInputs(Set<String>,私有)——顶层输入名集合;expandedConnections(List<String[]>,私有)——展开后的扁平连线;destToSrc(Map<String, String>,私有)——目标引脚到信号源的映射表;signals(Map<String, Integer>,私有)——全局信号值;gates(Map<String, Gate>,私有)——全局门电路实例池;childInstanceOrder(Map<String, List<String>>,私有)——子电路实例的树状层级结构。

方法:

  • run: 仿真主流程,依次执行解析、验证、展开、构建、计算和输出。
  • parse: 分阶段解析子电路定义(C\d+:endc)以及顶层的输入与连线。
  • validateAll: 遍历主电路和各子电路,调用 validateConnection 校验连线合法性。
  • validateConnection: 校验单条连线的输入输出数量、首位置是否为源、以及目标引脚是否存在信号冲突。
  • expandUsedSubCircuits: 递归展开子电路,将层级结构打平为带前缀的全局连线列表。
  • expandSubCircuitInstance: 递归展开单个子电路实例的内部连线。
  • buildCircuit: 根据扁平化连线构建全局信号映射表,并调用 createGateFromToken 填充门电路实例池。
  • createGateFromToken: 解析引脚字符串,根据类型字符和括号参数按需创建对应的 Gate 实例。
  • calculateOutputs: 循环遍历门电路池调用 tryCalculate,直到无新结果产生。
  • printOutputs: 按层级和类型权重排序,调用 appendScopeOutputs 输出所有门电路结果。
  • typeOrder / appendScopeOutputs / compareScope / extractLeadingNumber: 辅助方法,用于排序和作用域遍历。
  • addChildInstance / extractSubCircuitId / extractSubCircuitIdFromDefinitionToken / isSubCircuitPortToken / isDirectSubCircuitPortReference: 辅助方法,用于解析子电路结构和端口引用。
  • isConnectionLine / parseConnection / isNumber: 辅助方法,用于字符串基础判断与解析。

关系: 它与 SubCircuit 是关联关系,通过 subCircuits 持有子电路定义;它与 Gate 是关联关系,通过 gates 持有门电路实例;它与 Context 是依赖关系,仅在 validateAll 方法中临时创建上下文对象进行校验。

SubCircuit(子电路定义类):

属性: id(String,包级私有)——子电路标识;inputs(Set<String>,包级私有)——输入端口集合;outputs(Set<String>,包级私有)——输出端口集合;connections(List<String[]>,包级私有)——内部连线列表。

方法: parseLine 按行解析 INPUT:OUT:[...] 格式的连线,将内容拆分后填入对应的集合中。

Context(验证上下文类):

属性: inputPorts(Set<String>,私有)——当前作用域的输入端口;outputPorts(Set<String>,私有)——当前作用域的输出端口;subCircuits(Map<String, SubCircuit>,私有)——全局子电路定义库的引用。

方法:

  • 构造函数: 初始化输入输出端口和子电路库引用。
  • isSource: 结合当前端口和子电路定义,判断指定 token 是否为信号源。
  • isDestination: 结合当前端口和子电路定义,判断指定 token 是否为信号终点。
  • isNumber: 辅助方法,判断字符串是否为纯数字。

关系: 它与 SubCircuit 是关联关系,通过 subCircuits 引用来识别子电路的输入输出端口。

Gate(门电路类):

属性: fullName(String,包级私有)——包含层级前缀的全名;prefix(String,包级私有)——层级前缀;type(char,包级私有)——门类型字符(A/O/N/X/Y);id(int,包级私有)——编号;inputCount(int,包级私有)——输入引脚数;outputValue(int,包级私有)——输出值;outputKnown(boolean,包级私有)——是否已完成计算标记。

方法:

  • 构造函数: 初始化全名、前缀、类型、编号和输入数。
  • tryCalculate: 尝试从外部传入的映射表中获取所有输入值,就绪则执行计算并写入信号表。
  • resolveValue: 沿映射链向上溯源,解析引脚的真实信号值(处理直连线的穿透逻辑)。
  • calculate: 根据类型字符执行对应的逻辑运算(使用 switch 分支代替多态)。

三、踩坑心得

3.1 第一次作业遇到的坑

3.1.1 坑一:字符串解析的脆弱性与括号陷阱

问题:在解析如 A(8)1-2 这样的元件引脚时,我直接使用 split("\\(")split("\\)") 来提取输入引脚数和编号。当遇到没有括号的 X1-1 时,截取出来的数组长度变了,直接按固定下标取值导致 ArrayIndexOutOfBoundsException,程序在解析阶段直接崩溃。

心得:不能盲目信任外部的字符串数据,更不能假设所有元件名的格式都一样。用 split 这种粗暴的方法太脆弱了,后来我改用 indexOf 找括号的位置来判断,才勉强解决。这让我明白处理文本输入时,必须像剥洋葱一样加一层层判断,不能想当然。

3.1.2 坑二:迭代计算中的“死循环”与状态标记缺失

问题:在实现电路信号传播时,我没有给元件设置“未计算”的初始状态标记,而是让输出值默认为 0。结果导致那些根本没有连线的与门,因为默认就是 0,被系统误认为“已经计算完毕”,直接输出了错误结果;甚至有时候因为输入不全反复重试,导致程序陷入死循环。

心得:在图遍历中,节点的“已访问/已计算”状态不是可有可无的。把“没算出来”和“算出来是低电平0”这两个概念在代码里混为一谈是致命的。后来我引入了 outputValue = -1 作为未计算标志,才算理清了状态。

 

3.2 第二次作业遇到的坑

3.2.1 坑一:译码器/选择器引脚编号的“反直觉”映射错误

问题:题目要求引脚按“控制-输入-输出”排序,比如译码器 M(3)1 的 0、1、2 号是控制引脚,3、4、5 号才是输入引脚。但我凭直觉认为 0 号就是第一个输入,在写获取输入值的代码时,错写成了 name + "-" + (i + 1),导致取到了控制端的值。因为控制端不满足使能条件,译码器直接输出无效,我查了半个多小时才发现是引脚偏移量算错了。

心得:不能凭借直觉去假设硬件接口的排列,必须严格按题目文档的规则来写代码里的数组下标偏移。这种物理规则向代码映射的过程,差一个数字整个逻辑就全盘皆输。

3.2.2 坑二:三态门高阻态下的信号传递污染

问题:三态门断开时,我在 Map 里存了 null 表示无效状态。但这引发了麻烦,当这个 null 顺着连线传到后续与门的输入端时,我在与门里没有严格拦阻,导致空指针异常,或者更糟的是,在某些判断里 null 被意外当成了有效信号往下传,污染了整个电路的状态。

心得:在复杂的网状数据流中,一个节点的异常状态如果没有被严格隔离,就会像病毒一样顺着连线传染给所有下游节点。这让我明白,无效状态不仅要在产生的地方标记好,更要在接收的地方坚决拦截,不能让“脏数据”流窜。

 

3.3 第三次作业遇到的坑

3.3.1 坑一:子电路输入输出端在不同上下文中的“双重身份”混淆

问题:子电路的引入导致 Token 语义分裂。比如 C1-A,在子电路内部定义时,A 是输入端口,C1-A 是接收信号的目的端;但在主电路连线时,主电路把信号发给 C1-A,它又变成了源端的衍生。我一开始没区分这种“双重身份”,直接统一判断,导致把主电路发给子电路输出的连线,误判成了“输入输出顺序错误”。

心得:同一个字符串在不同位置有不同含义,不能孤立地看它。这逼着我抽象出了一个 Context 环境类,在解析主电路和子电路时分别传入不同的上下文规则,才把这种绕人的逻辑理顺。

3.3.2 坑二:异常校验优先级逻辑的“短路陷阱”

问题:题目要求按“多个输入 > 无输入 > 无输出 > 顺序错误 > 冲突”的优先级报错。测试样例 [A(2)1-0 O(2)1-0] 两个都是输出引脚,既没有输入,也没有目的端。我一开始用连续的 if-else if 写,结果满足了“无输出”就直接报错退出了,根本走不到优先级更高的“无输入”判断那里,导致优先级完全乱套。

心得:复杂的业务规则绝对不能试图用一长串互斥的 if-else 一次性搞定,因为很多异常情况其实是同时存在的。我后来改用独立计数器,先遍历一遍统计出源端和目的端的数量,然后再按顺序用独立的 if 去判断,这种“先统计,后判决”的办法彻底消除了逻辑分支间的相互干扰。

四、改进建议

4.1 第一次作业改进建议

建议:引入“入度”概念优化信号传播效率,避免无效遍历

第一次作业中采用的 while(flag) 轮询机制,在每一轮都会尝试计算所有元件,即使某些元件在上一轮已经得出了最终结果。这在电路规模较小时没有影响,但从算法效率角度来看存在大量冗余操作。建议为每个元件维护一个“未就绪输入引脚数量”的计数器(类似图论中的入度)。当一个元件成功计算出结果后,只去更新与其输出引脚直接相连的下游元件的计数器;只有当某个元件的计数器减为 0 时,才将其加入一个待计算的队列中。这种按依赖关系精准触发的改进,可以彻底消除对已稳定元件的重复访问,提升程序在复杂网络下的执行效率。

 

4.2 第二次作业改进建议

建议:分离数据计算与结果展示逻辑,降低实体类的耦合度

第二次作业中,为了应对译码器输出索引、分配器输出带“-”字符串等差异化需求,我在每个具体元件类(如 DecoderDemux)内部都重写了 printResult() 方法。这种做法导致实体类既负责逻辑运算,又包含了具体的控制台输出格式代码,违反了单一职责原则。建议将输出逻辑从元件类中完全抽离,由一个专门的“报表生成类”统一负责。该类根据元件的类型标识,读取其内部状态并决定如何格式化打印。这样如果未来需要将结果输出到文件或图形界面,只需修改报表生成类,而无需改动任何核心逻辑元件的代码。

 

4.3 第三次作业改进建议

建议:封装统一的标识符解析工具类,提升字符串处理的健壮性

第三次作业中,为了识别 A(8)1C1-X1-0 等复杂的引脚名称,主控制类中充斥着大量的 lastIndexOf('-')indexOf('(')substring() 操作。这种“裸奔”的字符串截取方式代码冗长,且一旦题目中的命名规则发生微调(例如允许名称中包含特殊符号),极易引发数组越界等异常。建议将这些零散的截取逻辑剥离出来,封装为一个专门的“标识符解析工具类”。该工具类对外提供清晰的接口方法(如 getType()getId()getPinNumber()),将复杂的语法解析细节隐藏在工具类内部。这不仅能大幅降低主流程代码的圈复杂度,也能显著增强程序应对格式变化的抗风险能力。

五、总结

5.1 综合性总结与收获

回顾这三次迭代作业,我经历了一个从“面向过程模拟硬件”到“用面向对象架构构建仿真系统”的完整蜕变。这个过程带给我的不仅是 Java 技能的精进,更是系统设计思维的脱胎换骨。

第一次作业让我建立了“网络即图,信号即状态”的核心抽象。通过将物理连线转化为 Map 中的键值对映射,我学会了如何用软件数据结构去等价替换现实世界的物理关系。接口与抽象类的引入,让我摆脱了满篇 switch-case 的 procedural 泥潭,初步尝到了多态带来的甜头。

第二次作业是对架构弹性的极限压测。当引脚规则突变、输出格式分化时,我经历了推翻基类重构的阵痛。但正是这种阵痛,逼迫我理解了“开闭原则”的真正含义:好的架构不是一开始就设计得天衣无缝,而是当变化到来时,能够以最小的代价进行重构。将无效状态统一为 null 的决定,让我对系统边界状态的传递有了更深的敬畏。

第三次作业则是思维维度的一次升维。子电路的引入让系统从“平面”变成了“立体”。我放弃了在运行时递归解析子电路的执念,转而使用“预处理展开”的策略,将复杂的树形结构在计算前拍平。这种“空间换时间、预处理换运行时简洁”的工程折中,是我在这系列作业中最自豪的设计。而异常校验的 Context 环境抽象和优先级计数器设计,则让我彻底告别了写“意大利面条式 if-else”的新手阶段。

 

5.2 进一步研究的内容

通过这三次作业,我也清晰地看到了自己当前能力的边界,有几块内容是我接下来必须重点突破的。

首先是编译原理基础与解析器生成工具。本次作业中手撕字符串的逻辑让我倍感痛苦。我打算系统学习正则表达式的进阶用法,并了解 ANTLR 等解析器生成工具。如果未来再遇到类似复杂的文本协议解析,我应该有能力直接定义语法文件(Grammar),自动生成 AST(抽象语法树),而不是在 Java 代码里写一堆易错的 indexOf

其次是单元测试与测试驱动开发(TDD)。整个系列作业我都是靠肉眼比对 PTA 的测试样例输出。对于第三次作业那种包含 5 种优先级异常、十几种边界情况的逻辑,人工构造测试用例几乎不可能覆盖全面。我深刻体会到了“没有经过自动化测试的复杂逻辑,本质上都是不可靠的”。接下来我需要熟练掌握 JUnit,学会为校验逻辑和计算引擎编写参数化测试,用机器代替肉眼去守护代码的正确性。

最后是设计模式的内化。虽然第三次作业使用了组合模式,但我是在写完代码后才发现“哦,原来这就是组合模式”,属于事后诸葛亮。我希望在未来面对需求时,能够前置性地在脑海中调出设计模式工具箱——看到复杂的对象创建,立刻想到工厂模式;看到大量的条件分支算法,立刻想到策略模式。将书本上的模式真正转化为肌肉记忆,是我从“代码搬运工”走向“软件设计师”的必经之路。

 

5.3 结语

数字电路模拟程序的三次迭代作业,对我而言是一场极具硬核色彩的修行。从最初连逻辑门符号都要回想半天的门外汉,到最后能够手搓一个支持子电路嵌套和严密异常校验的微型仿真引擎,这段经历让我真切体会到了软件架构是如何在约束与破局中螺旋上升的。在此过程中,那些折磨我到深夜的 Bug——无论是解析时的数组越界、高阻态的空指针传染,还是异常优先级的逻辑短路——如今看来,都是极其宝贵的反馈。它们精准地指出了我在抽象能力、状态管理和逻辑严密性上的短板,并逼迫我一一补齐。感谢老师设计了这样一套节奏极佳、直击痛点的迭代作业。它没有让我们停留在简单的语法学练上,而是直接将我们推向了真实工程中必须面对的“需求变更”、“复杂状态管理”和“边界防御”的战场。这个系列的结束不是终点,而是我将以更加严谨、更具架构视角的态度去审视每一行代码的全新起点。我会带着这三次作业淬炼出的工程思维,继续向着更深层次的软件世界探索。

posted @ 2026-06-23 20:54  LLL0806  阅读(5)  评论(0)    收藏  举报