数字电路模拟程序迭代开发总结
一、前言
1.1 作业系列概述
在《面向对象程序设计》课程的本阶段学习中,我们完成了一个极具挑战性且高度贴合底层硬件逻辑的“数字电路模拟程序”系列作业。三次作业以敏捷迭代的方式展开:从最基础的五种逻辑门电路模拟,逐步扩展为包含三态门、译码器、数据选择器等复杂组合逻辑元件的系统,最终演进为支持子电路实例化、具备严格输入合法性校验的层级化电路仿真平台。整个过程不仅考察了 Java 语言的高级特性,更对面向对象中的接口抽象、设计模式应用(如组合模式)、以及复杂业务规则的解耦与建模进行了极为深刻的训练。
1.2 题量、难度
第一次作业(数字电路模拟程序-1)需实现与、或、非、异或、同或门,代码量约 150 行。主要考察接口与抽象类的应用,以及基于图的信号传播思想。由于元件单一、引脚规则固定(输入从1开始,输出固定为0),难度适中,重点在于建立“元件-连接-信号”的核心数据模型。
第二次作业(数字电路模拟程序-2)新增了四种复杂元件,代码量激增至 300 行以上。难度陡然上升,核心痛点在于引脚编号规则的突变——引入了“控制引脚、输入引脚、输出引脚”的顺序排序机制,且不同元件的输出引脚号不再固定为0。此外,输出格式出现了分化(如分配器需要输出带“-”的无效状态字符串),极大地考验了代码的扩展性。
第三次作业(数字电路模拟程序-4,跳过了时序电路的模拟程序3直接迭代)达到了本系列的复杂度巅峰,代码量逼近 450 行。引入了“子电路”概念和五大类异常输入校验。需求要求运用组合模式将子电路作为特殊元件嵌套,并要求以极其严苛的优先级顺序解析并报出连接异常。此次作业综合性极强,难度偏难,真正考验了在高压下进行架构重构与防御式编程的能力。
1.3 三次作业所覆盖的核心知识点
- 面向对象设计原则:深度实践单一职责原则(SRP)与开闭原则(OCP),在新增元件时尽量减少对原有计算引擎的侵入。
- 设计模式:第三次作业重点应用了组合模式,将子电路与基本门电路抽象为统一的接口,实现了树形结构的递归处理。
- 图论思想应用:虽然没有显式写出图的数据结构,但电路网络本质上是有向无环图(DAG),信号传播采用了类似拓扑排序的迭代轮询机制。
- 复杂字符串解析与状态判定:通过大量的
String截取、正则匹配以及上下文环境类,实现了对如A(8)1-2、C1-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 作为全局信号总线,巧妙地解耦了元件之间的直接对象引用,使得网络拓扑的构建变得异常灵活。
下面为作业一代码分析图:

代码分析总结:
项目规模与结构: 项目被合理地拆分为了多个独立的 .java 文件(如 AndGate.java、Circuit.java、Gate.java 等),总计包含 8 个类/接口,代码总行数在 150~200 行之间。相比于将所有代码塞进一个主类,这种物理上的拆分完美映射了逻辑上的“接口-抽象类-具体实现类”的继承层次结构,类的颗粒度划分十分清晰。
代码质量与复杂度: 整体代码质量极佳,复杂度处于极优水平。具体逻辑门实现类(如 AndGate、OrGate 等)的语句数极少,基本只包含单层的 if 判断或简单的 for 循环,平均圈复杂度和嵌套深度都极低,阅读起来毫无负担。从报表数据可以推断,项目的最大复杂度和 最高分支比例(% Branches) 大概率集中在 Circuit 类的 checkAndCreate() 方法中。因为该方法需要通过 indexOf 和 substring 对诸如 A(8)1 这样不规则的字符串进行大量的存在性判断和截取操作,产生了较多的条件分支,是整个项目中唯一稍微显得臃肿的地方。
潜在缺陷与改进点: 延续了初版代码的通病,图表中的注释率(% Comments)必然为 0.0%。虽然得益于良好的方法命名(如 process、calculate),代码做到了一定程度的“自解释”,但完全缺乏对于核心设计思想(如利用 Map 模拟全局信号总线、迭代轮询机制的原理)以及复杂字符串解析规则的注释说明。随着后续作业元件类型的增加和解析逻辑的复杂化,这种“零注释”状态将会严重阻碍代码的维护与迭代。
2.1.3 类的设计与分析
下面是第一次作业的类图:

从图中可知:
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(或门类):
属性: 无新增属性。
方法: 构造函数调用 super;calculate 遍历输入数组,任一值为 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 子类实例并注册到allMap和list中。calculate: 循环遍历所有组件调用process,直到没有新结果产生为止(拓扑驱动计算)。printResults: 按门类型权重和编号排序后,输出各组件的输出值。getWeight: 辅助方法,为不同类型门电路赋予排序权重(A=1, O=2, N=3, X=4, Y=5)。
关系: 它与 Component 是关联关系,通过 allMap 和 list 持有组件的引用,但不负责组件的生命周期创建(创建逻辑在 checkAndCreate 中按需进行)。
Main(主类):
属性: 无。
方法: main 静态方法,通过 Scanner 逐行读取输入——INPUT: 行解析外部输入信号,[...] 行解析连接关系,end 结束输入。随后调用 calculate 执行计算,调用 printResults 输出结果。
关系: 它与 Circuit 和 Cargo 是依赖关系,仅在 main 方法中临时使用,不持有其引用。
2.1.4 心得
初次接触这种将“硬件连线”转化为“软件数据结构”的题目,最大的感受是视角的转换。过去写代码是线性的流水线,而这次面对的是一个网状的依赖图。我设计的 Circuit 类中采用了一个极为朴素的 while(flag) 循环:不断遍历所有元件尝试计算,如果有一个元件算出了新结果,就继续下一轮,直到某一轮没有任何新结果产生。这种“暴力轮询”虽然时间复杂度不高(因为节点少),但让我深刻理解了信号传播的时序问题——必须等所有输入就绪,元件才能动作。将具体逻辑运算下放到 AndGate、OrGate 等子类,而将“何时计算”的控制权留在父类和上下文中,这是我第一次真切体会到多态带来的代码整洁感。
2.2 第二次作业:复杂组合逻辑与多态引脚
2.2.1 需求回顾
在原有基础上,新增三态门、译码器、数据选择器、数据分配器。引脚编号规则发生颠覆性变化,需按照“控制-输入-输出”的物理顺序重新映射。不同元件的输出表现差异巨大:三态门可能断开(无效),分配器只有一路有效其他为无效状态,译码器需输出有效输出的引脚索引号。
2.2.2 关键代码分析
本次作业的核心难点集中在各类复杂元件对“引脚偏移量”的动态计算,以及 Demux 类的 printResult() 方法对无效状态的处理上。由于引入了“控制-输入-输出”的连续编号规则,例如译码器 M(3)1 的 0-2 号是控制引脚,3-5 号才是输入引脚。因此,在 Decoder 和 Mux 的 process() 方法中,必须根据构造时传入的引脚数量,动态计算出输入引脚的起始索引(如 startInputPin = 3)和输出引脚的起始索引。在数据分配器的输出阶段,printResult() 方法遍历所有输出引脚,针对 Map 中存为 null 的值(代表高阻态或未选中状态),将其转化为 "-" 字符拼接到结果字符串中。这种将复杂的物理引脚映射规则封装在各个实体类内部的做法,避免了底层信号传播逻辑的混乱;而统一使用 null 代表无效电平,既简化了状态判断,又完美契合了分配器特殊格式的输出需求。
下面为作业二代码分析图:

代码分析总结:
项目规模与模块化演进: 随着三态门、译码器、选择器、分配器四种复杂元件的引入,项目文件数量显著增加。这表明代码严格遵循了“一类一文件”的工程规范,每种具有独立引脚映射和输出格式的元件都被隔离在独立的物理文件中,模块化程度极高,避免了单一文件的过度膨胀。
核心复杂度集中与解析瓶颈: 从图表数据可以精准定位到,项目的核心复杂度高度集中在了 Circuit.java(120行,86条语句,分支比例高达 34.9%)。相比于第一次作业,Circuit 类的分支率出现了明显的跃升。这是因为在第二次作业中,checkAndCreate() 方法必须承担极其繁重的“词法解析”工作:它不仅要区分带括号与不带括号的元件名(如 A(8)1 与 N1),还要根据标识符(M、Z、F)的不同,解析出控制引脚数并动态计算输入/输出引脚的起始偏移量。大量的 if-else 判断和字符串索引查找直接推高了该类的圈复杂度。
实体类的低耦合保持: 尽管管理类的解析逻辑变得复杂,但可以预见,新增的 Decoder.java、Demux.java 等具体元件实现类,其内部的 process() 方法依然保持了较低的复杂度。这说明“复杂规则解析”与“纯粹逻辑计算”的职责分离是成功的:脏活累活(字符串拆解)都被 Circuit 类挡在了外部,门电路类内部依然保持纯粹的数学与逻辑运算。
顽疾与隐患: 延续了前序版本的特征,项目的注释率(% Comments)必然依旧为 0.0%。在 Circuit.java 分支率逼近 35% 的情况下,完全缺乏注释是非常危险的。例如,startInputPin = 3 这种魔法数字,如果没有注释说明“0-2为控制端,3开始为输入端”的物理背景,后续维护时极易引发理解偏差和修改错误。随着第三次作业子电路的加入,这种“零注释”状态将成为巨大的技术债务。
2.2.3 类的设计与分析
下面是第二次作业的类图:

从图中可知:
Component(组件接口):
属性: 无。
方法: 定义了四个方法——getName 获取名称、getId 获取编号、process 根据连接信息和信号映射执行处理逻辑、printResult 根据信号映射输出结果。
关系: 它是所有组件的顶层行为规范,任何电路元件都必须遵循该契约。
BaseComponent(基础组件抽象类):
属性: name(String,受保护)、id(int,受保护)。
方法:
- 构造函数: 初始化名称和编号。
getName/getId: 提供属性的 Getter 方法。
关系: 它实现了 Component 接口,但未实现 process 和 printResult,将这两个方法的实现责任推迟给子类。它与 Component 是实现关系,是整个组件体系的中间层,起到属性复用的作用。
LogicGate(逻辑门抽象类):
属性:
inputCount(int,受保护)——输入引脚数;outputValue(int,受保护,初始值为 -1 表示未计算)。
方法:
- 构造函数: 调用
super初始化名称和编号,同时设置输入引脚数。 process: 实现了通用的信号采集逻辑——遍历所有输入引脚,从connMap查找信号源,从sigMap读取信号值,组装成数组后调用calculate,将结果写回sigMap,若已计算过则返回false避免重复计算。printResult: 从sigMap中读取输出引脚的值并打印,仅当值存在时才输出。calculate: 抽象方法,由具体门电路子类实现各自的逻辑运算。
关系: 继承自 BaseComponent,是泛化关系(继承)。它将 process 和 printResult 的通用逻辑在此层统一实现,子类只需关注 calculate 的具体算法,体现了模板方法模式。
AndGate(与门类):
属性: 无新增属性。
方法: 构造函数调用 super;calculate 遍历输入数组,所有值为 1 才返回 1,否则返回 0。
关系: 继承自 LogicGate,是泛化关系(继承)。
OrGate(或门类):
属性: 无新增属性。
方法: 构造函数调用 super;calculate 遍历输入数组,任一值为 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 类似——译码器的引脚结构(使能端+数据输入+多条输出)与标准逻辑门差异较大,需要完全自定义 process 和 printResult。
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 是关联关系,通过 allMap 和 list 持有所有组件的引用。它是整个系统的编排者,负责组件的创建、连接管理、计算调度和结果输出。
Main(主类):
属性: 无。
方法: main 静态方法,通过 Scanner 逐行读取输入——INPUT: 行解析外部输入信号,[...] 行解析连接关系,end 结束输入。随后依次调用 calculate 执行计算和 printResults 输出结果。
关系: 它与 Circuit 是依赖关系,仅在 main 方法中临时使用,不持有其长期引用。
2.2.4 心得
第二次作业对我第一次建立的类图结构造成了巨大冲击。原本我让所有元件继承自一个统一的 Gate 抽象类,假设输出引脚都是 0。但三态门的输出变成了 2,数据选择器的输出引脚号变成了动态计算的 controlCount + inputCount。为了不让基类变得臃肿不堪,我被迫进行了重构,将原来的 Gate 拆分为 BaseComponent 和 LogicGate,让那些“异类”元件直接继承基类并重写 process() 和 printResult()。在处理分配器的无效状态时,我选择在 signals Map 中存入 null 来表示高阻态/断开,在输出时通过判断 null 来拼接 "-"。这个设计虽然有点取巧,但完美解决了多态输出格式的问题,让我明白了当业务规则出现分化时,及时拆分类层级比勉强塞进同一个模板要明智得多。
2.3 第三次作业:子电路嵌套与异常校验
2.3.1 需求回顾
引入子电路概念,子电路有独立的输入输出端口,可在主电路中像普通元件一样被实例化和连线。同时,增加对连接信息的严格语法和语义校验,要求能精准识别“多个输出短接”、“无输入”、“无输出”、“顺序错误”、“输入冲突”五种异常,并严格按照给定优先级只报第一个错误。
2.3.2 关键代码分析
本次作业的核心设计亮点集中在 CircuitSimulator.validateConnection() 的异常优先级判定机制,以及 expandUsedSubCircuits() 方法的子电路降维策略上。在异常判定中,该方法没有采用容易相互干扰的连续 if-else 分支,而是先遍历一遍连接数组,利用独立的计数器精准统计出真正的源端(sourceCount)和目的端(destCount)数量,随后严格按照题设的优先级顺序进行独立判断并立即返回。在子电路处理方面,expandUsedSubCircuits() 采用了一种极为巧妙的“预处理展开”思想:当检测到主电路使用了某个子电路时,它不去修改底层的计算引擎,而是直接将子电路内部定义的所有连接信息,加上子电路实例的前缀(如 C1-),拍平追加到主连接列表中。这种“先统计后判决”的解耦设计彻底消除了复杂逻辑下的短路陷阱,保证了校验的绝对严密;而“降维展开”策略则完美体现了组合模式的变体应用,它以极小的预处理开销,彻底屏蔽了运行时递归解析嵌套结构的复杂性,让底层引擎实现了零修改兼容。
下面为作业三代码分析图:

代码分析总结:
项目规模与结构:随着新增了四种复杂的组合逻辑元件,代码被拆分成了更多的独立 .java 文件(比如新增了 Decoder.java、Demux.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 类的设计与分析
下面是第三次作业的类图:


浙公网安备 33010602011771号