OPP第二次作业集总结
前言
第一次作业
目标
设计一个基础的数字逻辑电路仿真模块。系统支持与门、或门、非门、异或门、同或门五种基本逻辑元件,每个元件包含若干输入引脚和一个输出引脚。系统通过解析外部输入信号和元件间的连接关系,从输入信号出发沿连接关系递归计算每个元件的输出值,并处理元件间的依赖关系和循环依赖检测。最终按指定顺序输出所有可计算元件的输出引脚电平,不可计算的元件自动忽略。
设计与分析
本题要求模拟一个由五种基础逻辑门(与门、或门、非门、异或门、同或门)组成的数字电路。输入部分给出外部信号、元件连接关系,程序需要从输入信号出发,沿着连接关系逐级计算每个门电路的输出,最终按指定顺序输出所有可计算的元件输出引脚电平。
算法
1.递归求值:每个门电路的输出依赖于其输入引脚所接的信号源,信号源可能是外部输入,也可能是另一个门电路的输出。程序需要递归地追踪信号来源,直到拿到确定的 0/1 值或判定无法计算。
2.循环依赖检测:电路中可能出现 A 依赖 B、B 依赖 A 的循环结构,求值时需要能检测并避开,避免死递归。
3.忽略不可计算元件:某个门电路的输入引脚如果没有接任何有效信号源,或者依赖的元件因循环依赖无法计算出结果,则该元件被忽略,不参与输出。
实现方法
1.解析 INPUT 行,建立外部信号名到值的映射。
2. 解析所有连接信息 [输出信号 输入引脚1 输入引脚2 ...],建立每个门元件输入引脚 → 信号源的映射。
3. 对每个门元件调用递归求值函数:
(1)如果该元件已计算过,直接返回缓存结果。
(2)如果正在计算中(环),返回 null。
(3)否则递归获取每个输入引脚的值,全部拿到后按门类型计算输出并缓存。
4. 按输出规则排序并打印所有计算成功的元件。
代码规模
第一次作业代码规模如下表
| Source File | Total Lines | Source Code Lines | Source Code Lines [%] | Comm Ent Lines | Comm Ent Lines [%] | Blank Lines | Blank Lines [%] |
|---|---|---|---|---|---|---|---|
| Main.java | 174 | 157 | 90.2% | 6 | 3.4% | 11 | 6.3% |
| Total: | 174 | 157 | 90.2% | 6 | 3.4% | 11 | 6.3% |
类图
- Flight – 航班类
记录航班基本信息(航班号、最大起飞重量、最大业载重量)。 - Cargo – 货物类
记录货物信息(名称、重量)。 - LoadManifest – 装载清单类
管理当前航班的货物列表,提供添加货物、计算总重量、判断是否超载的方法。 - CargoSorter – 货物排序类
提供排序方法,按货物重量从高到低排序(采用选择排序算法,时间复杂度 O(n²))。 - Main – 主类
程序入口,负责读取输入、调用其他类、输出结果。

时序图

复杂度分析
第一次作业的复杂度分析如下表
| 类名 | 总方法数 | 圈复杂度(总和) | 最大方法圈复杂度 | 平均方法圈复杂度 | 最大嵌套深度 | 代码行数(Source Line) |
|---|---|---|---|---|---|---|
| Main | 9 | 48 | 15 | 5.3 | 4 | 157 |
1.可以看出Main的getDoorOutcome方法复杂度较高,因为它本质上是手写了递归求值+五种门逻辑判断,是对Java内置函数式接口或策略模式不熟悉所导致。
2.可以看出readConnections方法复杂度中等偏高,因为它本质上是手写了字符串解析和连接关系构建,是对正则表达式或状态机解析不熟悉所导致
踩坑心得
最初我在输出结果时直接按照元件在Map中存储的顺序输出,但是测试点就是无法通过。后来我仔细阅读题目,发现要按照与门→或门→非门→异或门→同或门的顺序输出,同类按编号升序,而HashMap的遍历顺序是不确定的。改用ORDER常量定义顺序,并结合编号排序后,测试点成功通过了。
改进建议
getDoorOutcome()方法直接修改了Door.outcome字段缓存计算结果。虽然提高了多次查询的效率,但如果电路发生变动或需要重新计算,缓存结果无法自动失效。我认为可以在Door中增加版本号或时间戳机制,或者在每次readConnections时重置所有outcome为null,确保计算结果的正确性。另外,当前的环检测通过calculate集合实现,但检测到环时直接返回null,没有尝试输出已计算的部分结果,可以考虑在环发生时记录环路径用于调试。
第二次作业
目标
在第一次作业五种基础逻辑门的基础上,扩展支持三态门、译码器、数据选择器、数据分配器四种新元件。引脚类型从单纯的输入/输出扩展到控制、输入、输出三类。系统需根据各元件的控制逻辑计算输出,其中三态门需判断控制引脚决定是否输出高阻态,译码器需检测控制引脚组合决定是否有效及输出引脚编号,数据选择器根据控制编码从多路输入中选一路输出,数据分配器根据控制编码将输入分配到指定输出通道。最终按指定顺序输出所有可计算元件的输出电平,无效元件自动忽略。
设计与分析
本题在第一次作业的基础上新增四种元件,元件命名规则与引脚号分配规则更为复杂。与门、或门沿用 A(输入引脚数)编号 / O(输入引脚数)编号 格式;非门、异或门、同或门沿用 N编号 / X编号 / Y编号 格式;三态门用 S编号;译码器用 M(输入引脚数)编号;数据选择器和数据分配器用 Z(控制引脚数)编号 / F(控制引脚数)编号。
引脚分配方面,含控制引脚的元件按控制→输入→输出的顺序排列。以译码器 M(2)1(2-4线译码器)为例:引脚0~2为控制端 S1/S2/S3,引脚3~4为输入端 A0/A1,引脚5~8为输出端 Y0~Y3。数据选择器 Z(2)1(四选一):引脚0~1为控制端 S0/S1,引脚2~5为数据输入端 D0~D3,引脚6为输出端 Y。三态门 S1 固定3个引脚:0为控制端,1为输入端,2为输出端。
算法
1.多类型元件计算:对每种新元件按题目逻辑实现求值,三态门控制为0时返回null(高阻态);译码器控制不满足 S1=1、S2=0、S3=0 时返回null,否则按输入编码将对应输出引脚置0;数据选择器按控制编码从多路输入中选一路输出;数据分配器按控制编码将输入信号分配到一路输出,其余输出为 -。
2. 多输出引脚处理:译码器和数据分配器有多个输出引脚,getInfo 方法需根据引脚号从 Door.output 字符串中取对应字符。译码器输出格式为 元件名:零引脚编号(输出0的引脚在输出组中的相对编号),数据分配器输出格式为 元件名:占位符字符串(由 0/1/- 组成)。
3. 输出引脚号判断:不同元件的输出引脚号不同,普通门为0,三态门为2,数据选择器为 控制引脚数 + 2^控制引脚数。输出阶段需按元件类型分别处理。
实现方法
1.沿用第一次的全局 Main + Door 内部类架构,Door 新增 String output 字段(支持多输出)和 boolean calculated 缓存标志。
2. parseDoorName 方法扩展解析新元件命名格式,计算总引脚数 footCount。
3. isInputFoot 方法判断引脚是否为需要参与计算的输入引脚(普通门为1n,三态门为01,译码器为03+n-1,数据选择器为0n+2^n-1,数据分配器为0~n)。
4. getDoorOutput 按元件类型分别计算,译码器和数据分配器返回字符串而非单个字符。
5. getInfo 方法根据引脚号从多输出元件的 output 字符串中取对应位置字符,遇到 - 返回 null。
6. 输出阶段按 ORDER = "AONXYSMZF" 顺序排序。
代码规模
第二次作业代码规模如下表
| Source File | Total Lines | Source Code Lines | Source Code Lines [%] | Comm Ent Lines | Comm Ent Lines [%] | Blank Lines | Blank Lines [%] |
|---|---|---|---|---|---|---|---|
| Main.java | 340 | 310 | 91.2% | 5 | 1.5% | 25 | 7.3% |
| Total: | 340 | 310 | 91.2% | 5 | 1.5% | 25 | 7.3% |
类图
1.Main – 主类
程序入口,负责输入解析、连接构建、递归求值、输出排序。扩展支持九种元件类型判断和多格式输出。
2. Door – 门元件数据载体
存储元件名、类型、编号、输入引脚数 n、总引脚数 footCount、信号来源数组 footHome、输出结果 output(String 类型,支持多输出)、计算缓存标志 calculated。

时序图!

复杂度分析
| 类名 | 总方法数 | 圈复杂度(总和) | 最大方法圈复杂度 | 平均方法圈复杂度 | 最大嵌套深度 | 代码行数(Source Line) |
|---|---|---|---|---|---|---|
| Main | 10 | 65 | 28 | 6.5 | 4 | 310 |
1.可以看出 Main 的 getDoorOutput 方法复杂度最高(约28),因为它承担了九种元件的计算逻辑,包含三态门高阻态判断、译码器控制组合校验、数据选择器/分配器的控制编码计算,是对策略模式不熟悉所导致。
2. 可以看出 getInfo 方法复杂度中等偏高(约8),因为它需要从多输出元件(译码器/数据分配器)的 output 字符串中按引脚号取字符,同时处理单输出元件的返回值,是对统一输出接口设计不熟悉所导致。
踩坑心得
1.数据选择器控制引脚的编号顺序:题目描述中 S1/S0 的四种组合 00/01/10/11 分别选择 D0~D3,但引脚编号顺序为 S0(引脚0)、S1(引脚1),低位在前。最初我将引脚0当作高位处理,导致控制编码计算错误。通过分析样例9(Z(1)1,控制 C-0 输出 D0=1)确认了引脚编号越小权重越低的规则。
2. 三态门输出引脚号非0:普通门输出引脚为0,但三态门输出引脚固定为2。输出阶段容易忽略此差异,需单独判断 d.type == 'S' 设置 outFoot=2。
3. 译码器输出要求的“输出0的引脚编号”是指输出组内的相对编号而非绝对引脚号。例如 M(2)1 输出引脚 Y0~Y3 对应绝对引脚 5~8,但输出要求为 M(2)1:0 而非 M(2)1:5。
改进建议
getDoorOutput 方法已膨胀至约150行,包含9个分支,圈复杂度接近30。如果后续继续增加元件类型,代码将完全不可维护。建议将每种元件的计算逻辑抽成独立方法(如 calcAndGate、calcTriState、calcDecoder),或用策略模式将元件行为封装为独立类。此外,isInputFoot 和 isOutputFoot 中大量使用硬编码边界判断(如 foot >= 3 + d.n),应将引脚类型定义为枚举或配置表,提高可读性。
第三次作业
目标
在第一次作业五种基础逻辑门的基础上,新增子电路和异常输入检测两大功能。子电路支持将一部分电路封装为独立模块,在主电路中被多次引用,子电路内部元件编号与主电路独立,输出时带子电路编号前缀。异常检测要求对连接信息检查六种非法情况,按优先级输出首个异常信息。
设计与分析
本次作业在第一次的基础上进行扩展,核心变化是引入了 Circuit 类作为电路容器,将原来挂在 Main 上的全局静态数据(input、doors、calculate)封装到每个电路实例中。主电路和每个子电路各自拥有独立的元件表和计算状态,通过 parent 引用和递归的 getValue 方法实现跨电路信号传递。
子电路的输入引脚(INPUT 声明的名称)在外部看来是信号目标,子电路的输出引脚(OUT 声明的名称)在外部看来是信号源。子电路内部通过 inputNames/outputNames 列表记录引脚名,通过 assignedInputSource 映射外部信号到内部输入引脚,通过 outputHome 映射内部信号到输出引脚。
异常检测按优先级从高到低依次检查:多个输入、无输入、无输出、输入输出顺序错误、输入引脚冲突,且只处理排在最前面的异常信息。
算法
- 子电路信号传递:getValue(String source) 需处理四种信号来源——外部输入(input 映射)、子电路输入引脚(查 assignedInputSource 递归到父电路取值)、门元件输出引脚(调用 getDoorOutcome)、子电路输出引脚(调用子电路的 getOutputValue)。
- 异常检测优先级流水线:checkAndReadOneConnection 按顺序检查六种异常,一旦命中立即记录 errorMessage 并返回,不继续解析该连接。isInputSignal 和 isOutputSignal 需根据上下文动态判断引脚身份。
- 输出前缀:子电路内门元件输出时带子电路编号前缀,格式为 子电路编号-元件名-0:值。主电路元件输出格式不变。
实现方法
- 引入 Circuit 类封装电路上下文,包含 input、doors、calculate、inputNames、outputNames、outputHome、assignedInputSource、parent 等字段。
- main 循环中先读取所有子电路定义(以 C编号: 起始,endc 结束),再读取主电路的 INPUT 和连接信息。
- readSubCircuit 方法解析子电路的 INPUT/OUT/连接信息,存入对应 Circuit 实例。
- checkAndReadOneConnection 实现异常检测优先级逻辑,检测通过后才执行数据填充。
- getResults 方法收集子电路和主电路所有可计算元件,输出时根据 isSubCircuit 拼接前缀。
- 异常检测覆盖六种场景:多个输入(inputCount > 1)、无输入(inputCount == 0)、无输出(outputCount == 0)、顺序错误(首 token 非输入信号或后续 token 含输入信号)、引脚冲突(同一输入引脚被多个信号驱动)。
代码规模
第三次作业代码规模如下表
| Source File | Total Lines | Source Code Lines | Source Code Lines [%] | Comm Ent Lines | Comm Ent Lines [%] | Blank Lines | Blank Lines [%] |
|---|---|---|---|---|---|---|---|
| Main.java | 520 | 470 | 90.4% | 15 | 2.9% | 35 | 6.7% |
| Total: | 520 | 470 | 90.4% | 15 | 2.9% | 35 | 6.7% |
类图
- Main – 主类
程序入口,负责管理主电路和子电路实例、调用异常检测、输出结果。包含静态成员 subCircuits、mainCircuit、errorMessage。 - Circuit – 电路类
封装一个电路(主电路或子电路)的完整上下文,包括输入信号映射、元件表、计算状态、输入输出引脚列表、引脚信号映射、父电路引用。提供 readInput、readOut、readConnections、getValue、getDoorOutcome、getResults 等方法。 - Door – 门元件数据载体
与第一次相同,存储元件名、类型、编号、输入引脚数、信号来源数组、计算结果。 - Result – 结果辅助类
存储元件计算结果及输出名称,用于排序。

时序图

复杂度分析
第三次作业的复杂度分析如下表
| 类名 | 总方法数 | 圈复杂度(总和) | 最大方法圈复杂度 | 平均方法圈复杂度 | 最大嵌套深度 | 代码行数(Source Line) |
|---|---|---|---|---|---|---|
| Main | 7 | 32 | 10 | 4.6 | 3 | 160 |
| Circuit | 10 | 72 | 18 | 7.2 | 5 | 310 |
| Total: | 17 | 104 | 18 | 6.1 | 5 | 470 |
1.可以看出 Circuit 的 getValue 方法复杂度较高(约12),因为它需要处理四种信号来源(外部输入、子电路输入引脚、门输出、子电路输出引脚)并支持跨电路递归,是对统一信号源抽象设计不熟悉所导致。
2. 可以看出 Circuit 的 checkAndReadOneConnection 方法复杂度较高(约18),因为它承担了六种异常检测优先级判断 + 连接解析 + 数据填充三块职责,是对单一职责原则掌握不够所导致。
踩坑心得
- 子电路输入引脚在 isOutputSignal 中被误判:[X C1-A] 中 C1-A 是信号目标,应被识别为输出信号。最初只判断了门引脚(pinNo > 0)和 OUT 关键字,漏掉了子电路输入引脚,导致被误判为无输入异常。
- 子电路输出引脚在 isInputSignal 中被误判:[C1-C N1-1] 中 C1-C 是信号源,应被识别为输入信号。最初只判断了门输出引脚(pinNo == 0),漏掉了子电路输出引脚,导致被误判为无输出异常。
- 异常优先级中“多个输入”与“没有输入”的边界:[A(2)1-0 O(2)1-0] 包含两个门输出引脚(信号源),触发“多个输入”异常。关键是要理清“输入”指连接中的信号源(数据流出的地方),“输出”指信号目标(数据流入的地方),与元件引脚类型相反。
改进建议
isInputSignal 和 isOutputSignal 逻辑过于复杂,包含门引脚、子电路引脚、外部输入、OUT 关键字等多种判断,且依赖 Circuit 上下文。建议将引脚类型判断抽象为独立的枚举或访问者模式。异常检测和连接解析耦合过紧,checkAndReadOneConnection 同时承担检测和数据填充双重职责,应拆分为两个阶段:先做纯检测(不修改状态),全部通过后再执行数据填充。子电路实例化方面,当前设计中子电路定义和实例是同一个对象,若同一子电路被多次引用将共享内部状态,对于组合逻辑虽无影响,但若未来扩展时序电路会成为隐患,建议将定义和实例分离。
综合性总结
收获
本次迭代作业,每次都在前一次的基础上增加新需求,使我对需求与代码的联系的理解大大加深了。最开始我习惯把所有业务逻辑全部塞在Main中,现在会把不同的逻辑放在不同的类中,做到职责清晰。从第一次的全局静态数据到第三次引入Circuit类封装电路上下文,逐步体会到了"高内聚、低耦合"在实际开发中的意义。
三次作业都有不同的限制——第一次要求手写选择排序,第二次面对九种元件的引脚号分配规则,第三次处理子电路命名空间和异常优先级——让我更加灵活地运用代码应对不同的设计约束。随着异常检测机制的逐步完善,让我逐步掌握了对于非法数据的处理,并且考虑可能会出现的各种情况。
本次作业集也让我对类图的绘制更加熟练,从最初不知道用什么表示类之间的关系,到现在能清晰地画出继承、组合、依赖关系,对UML的理解有了实质提升。
不足
对算法的运用不熟练,靠基础堆上去,越到后面代码的复杂度越来越高。第一次的getDoorOutcome还只有五种门逻辑,到第二次膨胀到九种,圈复杂度接近30,却始终没有用策略模式或工厂模式来拆分,导致getDoorOutput方法越来越臃肿。对Java内置的工具和设计模式掌握不够,习惯了自己手写递归求值和字符串解析,而没有借助现有类库或更优雅的架构方案。如果能有意识地运用策略模式、组合模式等设计模式来重构,代码的可维护性和可读性会有质的提升。

浙公网安备 33010602011771号