数字电路模拟程序——面向对象设计阶段总结(作业集四至六)
数字电路模拟程序——面向对象设计阶段总结(作业集四至六)
一、前言
本阶段共完成三次面向对象程序设计作业,主题从"航空器配载与货运管理"切换为"数字电路模拟系统",采用迭代式开发模式,从基础逻辑门电路逐步扩展为支持子电路与异常检测的完整数字电路仿真程序。
知识点覆盖:三次作业循序渐进地覆盖了面向对象程序设计的核心概念,且知识点侧重点与前三次作业有明显差异。作业四重点考察抽象类与继承机制的应用——通过Gate抽象类统一定义五种基本逻辑门(与门、或门、非门、异或门、同或门)的接口,每个子类实现各自的逻辑运算规则。同时引入了HashMap作为信号表、元件表和连接表的数据存储结构。作业五在继承体系基础上新增加四种复合元件,引入控制引脚的概念,将元件的引脚模型从简单的"输入→输出"升级为"控制-输入-输出"三段式结构,译码器、数据分配器还涉及多输出引脚的场景。这一变化对Gate基类的设计提出了更高的抽象要求——需要同时兼容单输出和多输出元件。作业六难度进一步提升,引入子电路概念和输入异常检测机制。将子电路视为一种组合型的电路元件,异常检测需要处理五种不同优先级的输入错误。
题量与难度:每次作业均为一道综合设计题,体量和复杂度逐次递增。作业四的类数量约11个,代码量约210行,主要考察抽象类和继承的基本应用,以及基于信号传播的电路模拟算法。作业五扩展至15个类,代码量约350行,新增4种复合元件类型,Gate基类需要同时提供单输出引脚和多输出引脚两套接口,且引入了isValid()方法来判断元件是否处于有效工作状态。作业六在作业四的基础上迭代替换,类数量约11个,但代码量增至约400行,核心复杂度集中在子电路提取/展开和五种异常检测两大模块。
二、设计与分析
2.1 作业四:基础逻辑门电路模拟
需求概述:设计数字电路模拟程序的基础版本,支持五种基本逻辑门元件(与门A、或门O、非门N、异或门X、同或门Y),通过读取电路连接信息,模拟信号从输入端经过各级门电路传播并最终输出各元件引脚电平的过程。
类设计分析:
本次作业以Gate抽象类为核心,构建了一个清晰的继承体系:
- Gate抽象类:定义了所有逻辑门元件的公共属性和行为。包含元件名、类型标识符、编号、输入引脚数、输入值数组和输出值)六个属性。提供
setInput(int pin, boolean value)方法为指定引脚设置输入信号,isReady()方法检查所有输入引脚是否均已连接有效信号,evalaute()抽象方法由子类实现各自的逻辑运算规则,getOutput()返回计算结果。
五个门电路子类:A(与门)的evalaute()采用"遇假则假"策略——遍历所有输入,一旦发现false立即返回false;O(或门)采用"遇真则真"策略——遍历所有输入,一旦发现true立即返回true;N(非门)仅有一个输入引脚,直接返回其反相值;X(异或门)比较两个输入是否不一致;Y(同或门)比较两个输入是否一致。Circuit类调用gate.evalaute(),运行时自动分发到对应的实现。
- Input类:负责文本解析,将输入字符串转换为内部数据结构。
parseInputLine()解析"INPUT: A-1 B-0"格式的信号定义,parseConnectionLine()解析"[A A(2)1-1]"格式的连接信息,createGate()通过元件名字符串识别类型并实例化对应的Gate子类。createGate()中的if-else分支判断元件名首字符来决定创建哪种门。
- Circuit类:核心模拟引擎。
simulate()方法采用基于队列的信号传播算法:维护一个引脚名列表和对应信号值列表,使用游标cursor遍历。每当一个元件的所有输入就绪(isReady()返回true),立即计算其输出并将输出引脚加入传播列表末尾。
- 三个Table辅助类:SignalTable、GateTable、ConnectionTable各自封装一个
HashMap<String, T>,提供类型安全的增删查接口。
SourceMonitor分析结果:
| 类名 | 方法数 | 平均复杂度 | 最大复杂度 | 代码行数 |
|---|---|---|---|---|
| Gate(抽象) | 7 | 1.4 | 3 | 54 |
| AndGate(与门) | 2 | 2.0 | 3 | 45 |
| Circuit | 3 | 5.3 | 8 | 256 |
| ConnectionTable | 4 | 1.3 | 2 | 28 |
| GateTable | 4 | 1.0 | 1 | 20 |
| Input | 7 | 3.1 | 7 | 78 |
| Main | 1 | 2.0 | 2 | 20 |
| NotGate/OrGate/Xor/Xnor | 2×4 | 1.0~2.0 | 1~3 | 11~45 |
| SignalTable | 4 | 1.0 | 1 | 20 |
| 合计 | 40 | 1.9 | - | 583 |
类图:
类图展示了11个类及其继承/依赖关系。核心结构为Gate抽象类及其五个子类形成的继承树,Input类依赖于三个Table类并通过createGate()方法与Gate建立创建依赖,Circuit类持有三个Table的引用并调用Gate的方法完成模拟,Main类作为入口编排Input和Circuit的协作流程。
2.2 作业五:复合元件与控制引脚
需求概述:在第一次数字电路作业基础上,电路新增四种复合元件:三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F)。这些元件的共同特点是引入了控制引脚——除传统的输入、输出引脚外,增加了决定元件工作状态或路由选择的控制信号。引脚编号规则升级为"控制→输入→输出"三段式顺序排列。
类设计分析:
新增了四个子类:
- Gate基类的双构造器设计:保留了原有的四参数构造器
Gate(name, type, number, inputCount)供基本逻辑门使用,新增六参数构造器Gate(name, type, number, controlCount, inputCount, outputCount)供复合元件使用。同时增加了三个关键方法:isValid()判断元件控制引脚是否处于有效状态(默认返回true),重载的getOutput(int pin)支持多输出引脚元件的按引脚号取值,getOutputPins()返回所有输出引脚号的数组供Circuit遍历。
S(三态门):1个控制端+1个输入端+1个输出端。重写了setInput()将引脚值存入统一的pins[]数组。isReady()要求控制端和输入端均已连接。isValid()当控制端为低电平时三态门处于高阻态,isValid()返回false,Circuit将忽略该元件的输出。evalaute()仅在控制端为高电平时将输入值传递到输出端。
- M(译码器):3个控制端+n个输入端+2^n个输出端。以3-8线译码器为例,包含6个引脚和8个输出引脚。
isValid()的判定条件为S1=1且S2=S3=0,不满足时译码器处于无效状态。evalaute()先将输入引脚编码转为二进制数值val,然后将所有输出引脚初始化为true,仅将第val号输出引脚设为false。
Z(数据选择器/MUX):controlCount个控制端+2^controlCount个输入端+1个输出端。控制端决定选择哪一路输入直接送往输出。evalaute()通过二进制加权求和计算选择索引sel,然后将pins[controlCount+sel]的值赋给唯一的输出引脚。
- F(数据分配器/DEMUX):controlCount个控制端+1个输入端+2^controlCount个输出端。与Z相反,将唯一输入信号分配到由控制端选中的输出引脚,其余输出引脚保持null(无效状态)。printResult中对F的特殊处理——遍历所有输出引脚,null输出"-",非null输出0或1。
SourceMonitor分析结果:
| 类名 | 方法数 | 平均复杂度 | 最大复杂度 | 代码行数 |
|---|---|---|---|---|
| Gate(抽象) | 7 | 1.4 | 3 | 54 |
| AndGate/OrGate/Xor/Xnor | 2×4 | 1.0~2.0 | 1~3 | 11~45 |
| NotGate | 2 | 1.0 | 1 | 11 |
| S(三态门) | 7 | 1.3 | 2 | 56 |
| M(译码器) | 6 | 2.2 | 4 | 58 |
| Z(数据选择器) | 6 | 1.8 | 3 | 35 |
| F(数据分配器) | 5 | 2.4 | 4 | 42 |
| Input | 7 | 3.1 | 7 | 78 |
| Circuit | 3 | 5.3 | 8 | 256 |
| 三个Table类 | 4×3 | 1.0~1.3 | 1~2 | 68 |
| Main | 1 | 2.0 | 2 | 20 |
| 合计 | 64 | 1.9 | - | 766 |
类图:
2.3 作业六:子电路与异常检测
需求概述:在作业四的基础上(不含作业五的复合元件),新增两大功能模块:一是子电路机制——允许将一部分电路定义为可复用的子模块,主电路通过"子电路编号-引脚名"的格式引用子电路的输入输出;二是输入异常检测,题目明确建议采用组合模式,将子电路和基本元件作为抽象元件的子类统一处理。
类设计分析:
本次作业的Gate体系退回到作业四的五种基本门(A/O/N/X/Y),但在Input类中新增了三大核心模块:
- 子电路提取模块:当遇到"C数字:"格式的行时进入子电路读取状态,依次读取INPUT行、OUT行和连接信息行,将子电路编号、输入列表、输出列表、连接列表存入四个Map中。
- 异常检测模块:按题目规定的五种异常类型及其优先级顺序逐一检查每条连接信息。
子电路展开模块(expandSC):将子电路内部的连接信息"注入"主电路的Table中。核心挑战是命名空间隔离,子电路内部的元件名(如A(2)1)可能与主电路或其他子电路中的元件重名。解决方案是对连接信息中的引脚名也做相应前缀处理。对于子电路的输入引脚,作为信号源直接映射到主电路的连接中;对于子电路的输出引脚,作为连接目标。
- Circuit类的适配:simulate()方法增加对非元件目标引脚的传播支持。当目标引脚不属于任何已注册的Gate时,将其作为中间信号继续传播。printResult()在排序时增加三级比较,先按元件类型排序,再按编号排序,最后按名称排序。
SourceMonitor分析结果:
| 类名 | 方法数 | 平均复杂度 | 最大复杂度 | 代码行数 |
|---|---|---|---|---|
| Gate(抽象) | 7 | 1.4 | 3 | 54 |
| AndGate/OrGate/Xor/Xnor | 2×4 | 1.0~2.0 | 1~3 | 11~45 |
| NotGate | 2 | 1.0 | 1 | 11 |
| Input(增强版) | 12 | 6.3 | 26 | 232 |
| Circuit | 3 | 5.3 | 8 | 256 |
| 三个Table类 | 4×3 | 1.0~1.3 | 1~2 | 70 |
| Main | 1 | 2.0 | 2 | 20 |
| SignalTable | 4 | 1.0 | 1 | 20 |
| 合计 | 45 | 2.9 | - | 739 |
类图:
2.4 三次作业迭代演进总结
从作业四到作业六,数字电路模拟系统经历了三个迭代周期,呈现以下演进趋势:
- 引脚模型的演化:作业四的"输入引脚1..n + 输出引脚0"简单模型 → 作业五的"控制0..c-1 + 输入c..c+i-1 + 输出c+i..c+i+o-1"三段式模型 → 作业六保留简单模型但通过子电路引入外部引脚。引脚模型的变化是整个系列的核心设计挑战,直接影响了Gate基类的接口设计。
- 代码复杂度的迁移:作业四的核心复杂度在Circuit.simulate(),作业五的核心复杂度分散在各个复合元件的evalaute()中,作业六的核心复杂度高度集中在Input类的checkErrors()和expandSC()中。
- 设计模式的渐进引入:作业四隐含了模板方法模式,作业五扩展了多态的应用范围,作业六引入了组合模式的雏形。
三、采坑心得
3.1 引脚编号体系的"0号陷阱"
作业四的引脚编号体系中,输出引脚固定为0号,输入引脚从1开始编号。这个看似简单的约定在代码实现中引入了大量的边界条件判断。在Input.parseConnectionLine()中,判断一个引脚是输入还是输出的逻辑为:如果字符串不含"-"则为外部输入(如"A"),如果包含"-"且最后一个"-"后面的数字>0则为输入引脚,如果数字==0则为输出引脚。这套判断逻辑分散在多个方法中,稍有不慎就会出现将输入引脚误判为输出引脚的bug。
在作业五中,情况进一步复杂化——控制引脚的编号也从0开始(如三态门的0号控制端)。这意味着引脚号0不再能唯一标识"输出引脚",必须结合元件类型来判断。我的解决方案是在Gate基类中保留getOutputPins()方法,由每个子类明确声明哪些引脚号是输出,而不是依赖"0=输出"。
3.2 三态门的"隐身"与isValid()的设计
作业五中三态门(S)的引入带来了一个有趣的设计问题:当控制端为低电平时,三态门输入输出之间断开(高阻态),其输出应被视为无效,程序应忽略该元件。这与"输入引脚未全部连接"导致的无效有所不同——前者是元件主动进入高阻态,后者是缺少必要的输入数据。
我的初始设计是在evalaute()中直接不设置输出值,但这样会导致Circuit认为该元件尚未完成计算(因为getOutput()返回null),从而阻塞后续的信号传播。经过调试后,我引入了isValid()方法作为独立的有效性判断通道:isReady()检查输入数据完整性,isValid()检查控制信号的有效性。Circuit在元件计算完成后额外检查isValid(),只有两者都通过才将输出信号加入传播队列。
3.3 译码器输出格式的特殊化处理
作业五的译码器(M)和普通门电路的输出格式完全不同——普通门输出"元件名-0:电平",译码器输出"元件名:激活引脚编号"。在printResult()中,我采用instanceof式的类型判断(实际上是通过getType()=='M'来判断),对不同类型元件采用不同输出逻辑。虽然功能上满足了需求,但这种做法违反了开闭原则,如果将来新增一种输出格式不同的元件,就需要修改printResult()。
3.4 子电路引脚命名的命名空间问题
作业六的子电路实现中,最棘手的问题是子电路内部元件与主电路元件的命名冲突。例如,子电路C1中有一个A(2)1,主电路中也有一个A(2)1,如果直接存入GateTable会导致后者覆盖前者。
我的解决方案是在展开阶段为子电路内所有元件名加上"C编号-"前缀。这看似简单,但实际实现时需要仔细追踪哪些地方使用了元件名——GateTable的key、ConnectionTable中连接信息的源和目标、以及最终输出时的元件名显示。特别是ConnectionTable中的目标引脚,需要同时修改gateName部分(加前缀)而保持pinNum部分不变。我的初始实现遗漏了expandSC中连接信息目标引脚的完整前缀处理,导致子电路内部连接断裂,调试了近两小时才定位到问题。这个经验说明了任何一个环节的遗漏都会导致引用断裂。
四、改进建议
4.1 引脚模型的统一与类型安全
当前设计中,作业五的Gate基类同时维护inputs[](基本门使用)和pins[](复合元件使用)两套数组,子类通过重写setInput()来区分使用哪一套。
改进方案:统一使用pins[]数组存储所有引脚值,废弃inputs[]。在Gate基类的构造器中,根据controlCount+inputCount+outputCount统一分配pins数组大小。基本门的构造器自动设置controlCount=0, outputCount=1,从而无缝兼容简单场景。同时为引脚号引入语义包装类(如PinIndex),区分PinIndex.control(n)、PinIndex.input(n)、PinIndex.output(n),避免裸int带来的语义歧义。
4.2 异常检测的状态机化
作业六的checkErrors()方法复杂度高。该方法混合了引脚角色判定、异常类型判定、优先级排序和usedTargets集合管理四重职责。
改进方案:引入状态机模式重构异常检测。将每条连接信息的解析过程建模为状态转换——初始状态(等待输出引脚)→有输出状态(等待输入引脚或结束)→异常状态(记录错误类型)→结束状态。每种异常类型作为独立的状态转换条件,通过配置表而非硬编码的if-else链来管理优先级。
五、总结
5.1 本阶段学习收获
通过数字电路模拟程序的三次迭代式作业,我在面向对象设计方面获得了以下核心收获:
- 抽象类与继承的实战应用:不同于作业一至三中"禁止使用继承"的约束,本阶段作业将继承作为核心设计手段。Gate抽象类作为五种(后扩展至九种)元件的统一接口,子类通过重写evalaute()实现各自的逻辑运算规则。我深刻体会到继承不是"为了复用代码",而是"为了统一接口以便多态调用",Circuit类不需要知道正在操作的是哪种门电路,只需要调用evalaute()。
- 引脚模型演化的设计教训:从简单的"输入1..n+输出0"到"控制-输入-输出"三段式,引脚编号体系的每一次变化都在代码中留下了痕迹。我认识到:当底层数据模型需要根据需求变化时,依赖数据模型的每一层代码都会受到影响。如果在设计初期就对引脚模型进行更抽象的定义,后续扩展的代价会小得多。
5.2 需要进一步学习的方向
- 设计模式的系统学习:本阶段三次作业自然地"遇见"了模板方法模式、组合模式、工厂模式的应用场景,但我的实现大多是形似神不似。系统地学习GoF 23种设计模式,理解每种模式的意图、结构和适用场景,能够在未来的项目中有意识地选择合适的模式,是下一阶段的重要目标。

浙公网安备 33010602011771号