面向对象——第四五六次PTA作业集总结

前言

本阶段的三次作业围绕“数字电路模拟程序”展开,同1,2,3,依旧是一个典型的迭代式开发项目。
这三次作业都比前三次难,而难主要是在逻辑与细节之中,让大部分人难以达到满分而只能达到高分。
以下是三代数字电路模拟程序的简要分析。涵盖了知识点与难度分析,难度评级

数字电路模拟程序 知识点、重点分析 难度评级

一代

基础门电路(与、或、非、异或、同或)的逻辑模拟,重点在于类的设计信号的单向传播 基础
二代 引入复杂组合逻辑器件(三态门、译码器、数据选择器、数据分配器),重点在于多引脚管理(控制/输入/输出引脚分离)和多值逻辑(高阻态Z)。 较难(复杂度飙升↑↑)
三代 引入子电路(模块化)异常处理,重点在于层次化设计错误检测 难(模块嵌套)

设计与分析

第一次作业

第一次作业共包含三个核心类:Gate(门电路类)、Source(信号源类)、Main(主类)。
本次作业采用面向过程的数据结构单向信号传播机制,结构简单直白。其中Gate类为实体类,存储元件名称、类型、输入引脚数、编号、输出值以及各引脚的信号来源映射;Source类作为辅助记录类,用于存储信号来源的元件名和引脚号;Main类负责数据输入、元件创建、信号传播和结果输出。

类图

image

依赖关系:本次作业类之间依赖关系简单——Main类依赖GateSource以及集合容器、Gate类依赖Source作为引脚信号来源的映射值、Source类无任何依赖,属于独立辅助记录类。

复杂度分析

image

可以看到,复杂度怎么看都是有点爆的,这不仅是题目本身复杂性所致。
而且是代码规范性问题
Main部分用大量 if-else 硬编码实现不同门电路(A/O/N/X/Y)的逻辑,每新增一种门类型都要修改这个方法,违反开闭原则。
每个分支里都重复了 “获取输入值→判空→计算结果” 的模板代码,冗余严重。
直接依赖 getVal 方法获取输入值,和外部状态耦合,难以单独测试。
用正则 + 多分支判断门电路类型,同时处理两种不同的命名格式(A(2)1 和 N1 等),逻辑复杂。包含大量格式校验和异常捕获,嵌套层级多,可读性差。

当然,也必然有可以优化之处:
如拆分main方法遵循, 单一职责重构 calc 方法;用策略模式消除 if-else;优化化 create 方法的逻辑等...
下为遵循单一职责示例:

查看代码
// 1. 读取输入
List<String> conns = readInput(sc);
// 2. 解析连接关系并创建门电路
parseConnections(conns);
// 3. 迭代计算输出值
computeOutputs();
// 4. 按顺序输出结果
printOutputs();

小结(一)
本次作业能拿100分是比较容易的,虽然main复杂度堆起来了,但大体无关紧要。
针对自定义对象存入哈希集合的需求,按照规范重写equals()
hashCode()方法,保证集合能够正确判断对象相等性,是面向对象开发中容器使用的重要实践。程序借助HashMapList等集合类,统一管理大量逻辑门与输入信号,实现对象的批量组织与快速查找。
本次作业不仅完成了逻辑电路模拟的题目要求,也让我扎实掌握了类与对象、封装、集合框架、方法设计等核心知识点,学会用面向对象思维拆解实际问题、搭建程序结构。

第二次作业

第二次作业在第一次的基础上,新增了五类复杂器件:三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F),同时优化了信号传播机制。
本次作业共包含:Device抽象类及其五个子类(AndDeviceOrDeviceNotDeviceXorDeviceXnorDevice),以及专门处理复杂器件的独立类(TriDeviceDecDeviceMuxDeviceDemuxDevice)。此外引入了pinSignals全局信号池,实现引脚级别的信号存储与传播。

设计变化:

  • 从第一次的Gate单一实体类,演变为抽象基类+具体子类的继承体系

  • 引入pinSignals(Map<String, Integer>)作为全局信号池,替代了Gate.output的单一值存储

  • 信号传播从calc方法的递归求值,演变为propagateSignals的迭代传播机制

类图

image

依赖关系:Main类依赖Device体系及pinSignals信号池;Device为抽象基类,五个子类(AndDevice、OrDevice、NotDevice、XorDevice、XnorDevice)各自独立实现calculate方法;复杂器件(TriDevice、DecDevice、MuxDevice、DemuxDevice)的calculate返回null,其逻辑在Main.propagateSignals中硬编码处理,pinSignals作为全局信号池被MainevaluateDevice共同依赖。本次作业类之间依赖关系适中。

复杂度分析

 image

与第一次对比,平均圈复杂度从7.00降至5.58,得益于职责拆分更加细致,方法数从11增至26。但最大圈复杂度从28升至40,说明个别方法承担了过重的职责,成为新的复杂度热点。
第二次作业在复杂度管理上有明显优化:平均圈复杂度从10.18降至5.58,方法数从11增至26,职责拆分更加细致。但复杂器件(S/M/Z/F)未纳入继承体系,导致evaluateDevice方法成为新的"上帝方法",圈复杂度高达40,认知复杂度77,远超第一次的calc方法(v(G)=24)。
此外,printOutputs也因九种器件输出格式各异而达到v(G)=24。两个方法合计贡献了64的圈复杂度,占全文件145的44%。如果继续沿用这种设计,第三次作业新增子电路功能时,这两个方法将进一步膨胀,最终导致架构失控。根本解决方案是将S/M/Z/F也纳入Device继承体系,各子类独立实现calculateoutput方法,消除硬编码分支。

小结(二)

 通过本次作业,我加深了对继承与多态的理解。五种基础门通过继承Device分别实现calculate方法,代码结构比第一次更加清晰。pinSignals全局信号池的引入也有效解决了多引脚器件的信号存储与读取问题。
优化方向可以将S/M/Z/F也纳入Device继承体系,各子类独立实现calculateoutput方法,消除evaluateDeviceprintOutputs中的硬编码分支。同时废弃Gate体系,统一信号存储为pinSignals单一信号池,避免双重路径问题。

第三次作业

第三次作业在前两次的基础上,新增了两大核心功能:子电路(模块化) 和异常输入检测
本次作业共包含:BlockModule(模块类)、LogicUnit抽象类及其五个子类(AndUnit、OrUnit、NotUnit、XorUnit、XnorUnit)、WireLink(线网类),以及Main类中大量的静态解析方法。此外,第一次的Gate体系、第二次的Device体系与第三次新增的LogicUnit体系三套元件表示同时共存。
类图
image

依赖关系:Main类依赖BlockModule、LogicUnit、WireLink以及前两次遗留的Gate/Device体系;BlockModule包含members(模块内元件映射)和internalLinks(内部线网列表),递归解析子电路定义;LogicUnit为抽象基类,五个子类各自实现compute方法;WireLink作为独立线网记录类,存储驱动端和接收端信息。本次作业类之间依赖关系急剧增加,且三套元件体系功能重叠,导致架构混乱。
复杂度分析
image

第三次作业最大圈复杂度虽从40降至22,但复杂方法数量从4个增至5个,parseMainv(G)=22)和runSim(v(G)=19)成为新的复杂度热点。runSim的认知复杂度高达81,是全文件最难理解的方法。根本问题在于三套元件体系(Gate/Device/LogicUnit)共存,导致信号存储存在三重路径,子电路克隆时内部映射丢失。parseMain承担了四个独立职责,严重违反单一职责原则。有时候做题就是图方便难免违反。
改进思路:废弃Gate和Device体系,统一为LogicUnit一套体系;子电路采用命名空间重命名而非克隆;将parseMain拆分为解析、创建、连接、验证四个独立方法。
下为删除 cloneUnit 方法 + 改造 registerUnitByPin

查看代码
static void registerUnitByPin(String token) {
    // 子电路模块:Cxxx-端口/器件
    if (token.startsWith("C") && token.contains("-")) {
        int dash = token.indexOf('-');
        String modTag = token.substring(0, dash);
        String innerName = token.substring(dash + 1);
        BlockModule mod = moduleLib.get(modTag);
        if (mod == null) return;

        // 遍历模块内部所有逻辑单元,拼接 模块名-单元名 作为全局命名空间
        for (Map.Entry<String, LogicUnit> e : mod.members.entrySet()) {
            String globalUnitName = modTag + "-" + e.getKey();
            // 命名空间全局唯一,不存在则新建,不再克隆
            if (!allUnits.containsKey(globalUnitName)) {
                // 直接基于原始定义创建,使用命名空间名称
                LogicUnit newUnit = makeUnit(globalUnitName);
                if (newUnit != null) {
                    allUnits.put(globalUnitName, newUnit);
                }
            }
        }

        // 模块内部连线,同样追加命名空间,加入全局连线表
        for (WireLink wl : mod.internalLinks) {
            boolean exists = false;
            for (WireLink existing : allLinks) {
                if (existing.driver.equals(wl.driver) && existing.receiver.equals(wl.receiver)) {
                    exists = true;
                    break;
                }
            }
            if (!exists) {
                allLinks.add(new WireLink(wl.driver, wl.receiver));
            }
        }
    }
    // 普通独立逻辑门
    else if (token.contains("-")) {
        String unitName = token.substring(0, token.lastIndexOf('-'));
        if (!allUnits.containsKey(unitName)) {
            LogicUnit unit = makeUnit(unitName);
            if (unit != null) allUnits.put(unitName, unit);
        }
    }
}


小结(三)

通过本次作业,我对模块化设计有了更深刻的理解,第三次作业也是这三次中最重量级也最重要的一个。子电路的本质不是简单的类嵌套,而是命名空间隔离——将一组元件封装为独立单元,对外只暴露输入输出端口,内部信号对外部不可见。这让我联想到操作系统中的进程隔离概念,虽然层级不同,但思想相通。异常检测机制的引入也让我认识到,在复杂系统中输入验证不是可有可无的附属品,而是保证系统健壮性的第一道防线,一个设计良好的系统应该在错误发生时给出清晰的提示,而非静默失败
本次作业已完成大部分测试点,能想到的输入基本测试过无问题,但仍没有满分,还是有细节待究。

采坑心得

第一次作业踩坑记录

坑一:忽略引脚0的限制

第一次作业中,getVal方法没有s.pin == 0判断,导致读取信号时可能取到输入引脚而非输出引脚。
当上层元件连接到X1-1(异或门的输入引脚)时,getVal会返回g.output,但此时输出引脚还没计算完,或者返回的是错误的信号值。

查看代码
static int getVal(Source s) {
    if (inputs.containsKey(s.dev)) return inputs.get(s.dev);
    Gate g = gates.get(s.dev);
    if (g != null) return g.output;  // ❌ 没有检查pin是否为0
    return -1;
}
//修正↓
static int getVal(Source s) {
    if (inputs.containsKey(s.dev)) return inputs.get(s.dev);
    Gate g = gates.get(s.dev);
    if (g != null && s.pin == 0) return g.output;  // ✅ 只读输出引脚
    return -1;
}

第二次作业踩坑记录

坑一:第一次的Gate.output和第二次的pinSignals同时存在,读取和写入走不同路径。

可以废弃Gate.output,统一从pinSignals读取:

查看代码
static int getVal(Source s) {
    if (inputs.containsKey(s.dev)) return inputs.get(s.dev);
    Gate g = gates.get(s.dev);
    if (g != null && s.pin == 0) return g.output;  // ❌ 仍从Gate读
    return -1;
}

static void propagateSignals() {
    // ...
    pinSignals.put(name + "-0", result);  // 写到pinSignals
}

//修正↓
static int getSignal(String pinName) {
    return pinSignals.getOrDefault(pinName, -1);
}

坑二:evaluateDevice中过早返回

evaluateDevice在检查控制引脚时,一个引脚缺失就直接return false,但其他引脚可能还没传播到。
控制引脚和数据引脚同时传播时,数据先到控制后到,由于控制缺失直接返回false,导致同一轮次内无法完成求值。
应该所有引脚读取完再统一判断:

查看代码
Integer ctrl = pinSignals.get(name + "-0");
if (ctrl == null) return false;  // ❌ 过早返回
Integer in = pinSignals.get(name + "-1");
if (in == null) return false;
// ...

//修正↓
Integer ctrl = pinSignals.get(name + "-0");
Integer in = pinSignals.get(name + "-1");
if (ctrl == null || in == null) return false;  // ✅ 都读完再判断

第三次作业踩坑记录

坑一:第一次的Gate、第二次的Device、第三次的LogicUnit同时存在,一个元件可能出现在三个Map里。

registerUnitByPin遇到子电路引脚时,先克隆LogicUnit,但runSim中又从Device体系读取信号,导致信号断裂。
可以废弃前两套体系,只用LogicUnit一套。

查看代码
// 第一次
Map<String, Gate> gates = new HashMap<>();
// 第二次
Map<String, Device> devices = new HashMap<>();
// 第三次
Map<String, LogicUnit> allUnits = new HashMap<>();

//修正↓
LogicUnit clone = cloneUnit(e.getValue(), fullName);
allUnits.put(fullName, clone);

坑二:异常检测过于激进

某些正常连接格式(如多个输出引脚连接到同一个输入引脚?题目说一个输入引脚不能连接多个输出引脚,但反过来是可以的)被误判为错误。->原本正确的Case因为异常检测误判而输出错误信息。

要仔细对照题目要求,只检测明确禁止的情况(没办法只能猜,猜也猜不到满分😀)

查看代码
if (outputCount > 1) {
    faultMsg = "ERROR: " + line + " include more than one input";
    hasFault = true;
    return;
}

 

踩坑小结

  • 能跑通的代码不一定对,引脚0的坑就是血的教训

  • 信号源只能有一处,多路径必然混乱

  • 不要打补丁,要重构——欠下的技术债连本带利都要还
  • 以样例为准,题目描述与样例冲突时信样例

  • 一个方法只做一件事,parseMaincalc就是反面教材

(PS.最大的坑是无法理解题目要求。在第一次数字电路作业中,题目看似写的规则十分清晰,实则暗藏玄机,示例中的OUT是什么?0是哪里来的?原来OUT不是很有所谓,0是默认👴等等,全是看示例自己猜,自己摸索,自己去想,自己去试试是不是这样的,后面的一些测试点更是这样,找极端逻辑极端示例,本次作业比上三次作业最强悍之处在于测试点更为阴间,能想到的点都能过,也许是一些答案与出题者想的有所出入?本次作业想做到全对花费的时间无疑是巨大的,我也没有做到满分的地步,做到高分应该算是很好的了)

改进建议

一、代码结构改进

问题: 三次作业的方法圈复杂度持续超标,Main.calc(v(G)=24)、Main.evaluateDevice(v(G)=40)、Main.parseMain(v(G)=22)都是典型的"上帝方法"。
建议: 严格遵循单一职责原则,每个方法只做一件事。以parseMain为例,拆分为parseInputLines、parseConnections、validateConnections、buildNetwork四个方法,各自独立测试。
问题: 三套元件体系(Gate/Device/LogicUnit)共存,信号存储存在多重路径。
建议: 只保留一套元件体系,统一信号源。若从第三次作业重构,应废弃GateDevice,只保留LogicUnit,所有信号通过唯一的sigMap存取。

二、架构设计改进

问题: 子电路采用克隆物理元件的方式实现,导致内部信号映射丢失。
建议: 采用命名空间重命名。子电路实例化时,给其内部所有信号名加上前缀(如C1.And1-0),信号池统一维护,无需克隆。
问题: 复杂器件(S/M/Z/F)未纳入继承体系,在evaluateDevice中硬编码处理。
建议:S/M/Z/F也纳入Device继承体系,各子类独立实现calculateoutput方法,消除硬编码分支。

三、可扩展性改进

问题: 新增器件类型需要修改create、calc、evaluateDevice、printOutputs等多个方法。
建议: 引入工厂模式创建元件,策略模式处理计算,新增器件只需新增一个子类,无需修改已有代码。
问题:引脚号计算分散在各处(数据选择器输出引脚、译码器输出起始引脚等)。
建议:Device基类中定义getOutputPin()getInputPins()等方法,由各子类自行计算,外部只需调用接口。

四、异常处理改进

问题: 异常检测逻辑与解析逻辑混在parseMain中,圈复杂度22。

建议: 独立的InputValidator类负责所有异常检测,解析完成后统一校验,职责清晰。

问题: 错误信息不够明确,难以定位问题。

建议: 异常发生时输出具体的错误位置和期望格式,如ERROR: Pin X1-1 not connected,而非笼统的ERROR: invalid input

总结

三次迭代作业做下来,最大的感受就是:能跑和好改是两码事。

一、代码结构改进
问题: 三次作业的方法圈复杂度持续超标,Main.calc(v(G)=24)、Main.evaluateDevice(v(G)=40)、Main.parseMain(v(G)=22)都是典型的"上帝方法"。

建议: 严格遵循单一职责原则,每个方法只做一件事。以parseMain为例,拆分为parseInputLines、parseConnections、validateConnections、buildNetwork四个方法,各自独立测试。

问题: 三套元件体系(Gate/Device/LogicUnit)共存,信号存储存在多重路径。

建议: 只保留一套元件体系,统一信号源。若从第三次作业重构,应废弃GateDevice,只保留LogicUnit,所有信号通过唯一的sigMap存取。

二、架构设计改进
问题: 子电路采用克隆物理元件的方式实现,导致内部信号映射丢失。

建议: 采用命名空间重命名。子电路实例化时,给其内部所有信号名加上前缀(如C1.And1-0),信号池统一维护,无需克隆。

问题: 复杂器件(S/M/Z/F)未纳入继承体系,在evaluateDevice中硬编码处理。

建议: S/M/Z/F也纳入Device继承体系,各子类独立实现calculateoutput方法,消除硬编码分支。

三、可扩展性改进
问题: 新增器件类型需要修改create、calc、evaluateDevice、printOutputs等多个方法。

建议: 引入工厂模式创建元件,策略模式处理计算,新增器件只需新增一个子类,无需修改已有代码。

问题: 引脚号计算分散在各处(数据选择器输出引脚、译码器输出起始引脚等)。

建议:Device基类中定义getOutputPin()getInputPins()等方法,由各子类自行计算,外部只需调用接口。

四、异常处理改进
问题: 异常检测逻辑与解析逻辑混在parseMain中,圈复杂度22。

建议: 独立的InputValidator类负责所有异常检测,解析完成后统一校验,职责清晰。

问题: 错误信息不够明确,难以定位问题。

建议: 异常发生时输出具体的错误位置和期望格式,如ERROR: Pin X1-1 not connected,而非笼统的ERROR: invalid input

收获层面
这三次作业收获良多。第一次让我真正理解了什么叫"数据和行为封装在一起",第二次加深了对继承和多态的理解,第三次虽然分数不如意,但让我明白了模块化设计的核心是命名空间隔离,不是复制粘贴。三个版本的对比摆在眼前,对我java面向对象的提升无疑是巨大的。锻炼我调试水平,提高了抗压水平,分数也迫使自己有了钻研的耐心与动力,这三次作业还为自己巩固了上课所学的知识点,更是提高了实践能力,可以说三次作业的代码量都不少,题是非常好的题,既有逻辑思考环节,也有细节探寻环节,也有代码构思环节。我还是认为这三次作业主要是理解,然后去编程去实现,对逻辑的体现是非常深刻的,对各个板块的划分和算法的讲究也体现的十分的重要,换输出、转换算法,相当于换了一个思路,很多时候考虑的不全的话算法对测试点的正确率也是天壤之别的,想出所能及的最好的思路,选择最优的算法,是解这种题的最好方法,主要是耐心,冷静,然后逐个击破难点!

 

 

posted @ 2026-06-24 21:57  darlin'  阅读(6)  评论(0)    收藏  举报