pta两次大作业

PTA 两次大作业总结:详细分析与实践经验

前言

回顾这次的家具强电电路模拟程序大作业,它无疑是一次极具挑战的编程与设计经历。从最初简单的电路组件模拟,到后期复杂的多设备连接和精准的控制反馈,这个过程不仅让我掌握了许多技术技能,还在思维方式、问题解决能力以及系统设计方面得到了显著提升。

初期阶段:从基础模拟到逐步扩展
作业的前期阶段主要集中在基本电路元件的模拟。最初,我的任务是将开关、灯具、电动设备等元素抽象成程序中的类,并让它们在代码中能够模拟实际电路的运行。虽然这些设备功能简单,但如何有效地管理和控制它们的状态,却并非易事。尤其是如何模拟开关的不同状态变化,以及如何精确地控制灯具的亮灭、电动设备的启停,都让我经历了不小的困难。

随着作业的进行,我逐步掌握了如何通过面向对象的方式,结合继承、封装和多态等基本概念,来设计和管理这些设备。通过将不同设备抽象成类,并通过方法来控制它们的行为,我得以逐步构建起一个较为简易的电路模拟程序。这个过程不仅锻炼了我的编程基础,还让我更加熟悉了面向对象编程的核心原则。

中期阶段:电路连接与动态状态管理
然而,随着作业逐渐深入,第二阶段的任务变得复杂得多。如何将多个设备之间的相互连接和状态同步起来,成为了我面临的最大挑战。尤其是在模拟电路的并联与串联时,如何确保设备之间的电压、电流传递符合实际物理规律,并能动态更新每个设备的状态,成为了难点。此时,我不仅需要考虑每个设备的独立行为,还需要确保它们在电路中的交互符合预期。

这时候,命令解析和输入格式的严格要求让我陷入了困境。每一次设备的状态变化,都必须精准反馈给其他连接的设备,稍有偏差就会导致整个电路的崩溃。我记得当我初次处理这些电路连接时,因为忽略了某些关键的边界条件,导致程序的输出结果完全不符合预期,甚至连电压值和电流的计算都会出错。每当遇到这种情形,我的心情都会变得极为焦虑,甚至有些想放弃,但每一次调试解决问题后,那种从困境中走出的成就感,又让我重新燃起了动力。

后期阶段:精细的异常处理与模块化设计
进入到第三阶段,我的挑战逐渐集中在如何优化程序的异常处理和如何设计更加模块化的结构上。面对越来越复杂的电路连接,我意识到如果将所有的功能集中在一个类中,代码不仅容易变得冗长且难以维护,而且调试时也会变得异常困难。因此,我开始将不同的功能进行拆分,尝试将设备的控制、命令的解析以及电路状态的更新分别封装在不同的类和方法中。通过这种模块化的设计,我不仅提高了代码的可读性,也极大地提升了代码的可扩展性。即使在最后的调试阶段,我也能够轻松定位到问题并进行相应的修改和优化。
在此过程中,我也深刻理解了异常处理的重要性。面对不合法的输入、设备状态的异常变化,或者电路连接中的突发错误,我都需要通过合适的异常捕获机制来确保系统能够稳定运行。刚开始时,我觉得加入过多的异常处理会使代码显得冗余,但随着调试过程的深入,我渐渐明白了“防御性编程”对于系统健壮性的重要性。通过对每一个潜在问题的预防,程序变得更加稳健,也让我在调试时少了很多焦虑。
编程思维的提升:从错误到经验的积累
当然,在整个过程中也充满了挑战。特别是在处理电路设备连接时,我曾一度卡住,无法理解如何同步多个设备的状态。最初,我误解了设备连接的逻辑,导致电压、电流的计算频繁出错,进而引发了多次调试失败。每一次面对困境时,我都感到深深的沮丧,但也正是通过这些错误的积累,我才逐渐对电路原理和程序结构有了更加深入的理解。每当调试成功,程序终于能够正常运行时,那种从困境中迎来突破的喜悦,常常让我感到无比满足。
这些错误和困难,虽然让我在编程过程中感受到了挫败,虽然最终我的pta第二次作业并没有及格,但它们也推动我不断深入思考编程的本质,促使我不断精进自己的技术。从最初的“误打误撞”,到现在能够有条理地拆解问题,设计合理的程序架构,整个过程中我逐渐意识到,编程不仅仅是写出能运行的代码,更是一个不断优化与改进的过程。

未来展望:不断提升的编程能力
回顾整个作业过程,我深刻体会到,编程不仅是技术层面的积累,它更是一种思维方式的锤炼。这三次作业让我从最基础的电路组件模拟,逐步走向多设备管理、电路状态控制等更复杂的功能设计。这些技能的掌握,使我不仅能够独立完成相对复杂的编程任务,还能够更加自信地面对未来更高难度的项目。

未来,我希望能够进一步提升自己的代码优化能力,学习并应用更为高级的设计模式,以便在面对更加复杂的系统时,能够设计出更高效、易于维护的代码。同时,我也希望在测试方法的应用上不断探索,力求让每一段代码都经得起多场景、多输入的考验。

这次的作业让我经历了无数次的“痛并快乐着”,但每一次的突破,都让我更加热爱编程,并让我相信编程是一个可以不断挑战自己、提升自己的过程。正如我在这篇总结中所提到的,编程不仅是一个技能的学习,它更是一个自我探索与自我提升的旅程,而我,才刚刚开始。

设计与分析

家居强电电路模拟程序-1

任务描述

本项目的任务是模拟家居强电电路,包括对多种家居设备(如开关、调速器、电灯等)的电压控制和状态管理。通过设备间的连接与控制命令,模拟家居电路的实际工作流程,实现各种设备的状态变化并展示给用户。具体要求包括:
支持多个设备,能够根据输入的电压进行状态计算。
支持设备间的连接(如开关与灯、调速器与风扇等)。
支持控制命令来改变设备的状态(如开关开关、调速器增减速度等)。
输出设备的当前状态。

类设计

1. Device

特色代码:

abstract class Device {
    protected String id;
    protected double inputVoltage;  // 输入电压
    protected double outputVoltage; // 输出电压

    public Device(String id) {
        this.id = id;
        this.inputVoltage = 0;
        this.outputVoltage = 0;
    }

    public abstract void process(); // 处理设备的电压输出
    public abstract String getStatus(); // 获取设备状态

    public void setInputVoltage(double input) {
        this.inputVoltage = input;
    }

    public double getOutputVoltage() {
        return outputVoltage;
    }

    public void setOutputVoltage(double output) {
        this.outputVoltage = output;
    }

    public String getId() {
        return id;
    }
}

分析:
Device 类是所有设备的父类,它定义了设备的基本属性(如 idinputVoltageoutputVoltage)以及通用的行为(如设置电压、获取电压和获取设备状态)。通过抽象方法 process()getStatus(),每个设备都可以根据自身的特性来实现具体的行为。这是一个典型的面向对象设计中的抽象类,它保证了不同设备类的统一接口,同时又能提供各自的具体实现。

2. Switch

特色代码:

class Switch extends Device {
    private boolean isClosed;

    public Switch(String id) {
        super(id);
        this.isClosed = false;
    }

    @Override
    public void process() {
        this.outputVoltage = isClosed ? inputVoltage : 0;
    }

    @Override
    public String getStatus() {
        return isClosed ? "closed" : "turned on";
    }

    public void toggle() {
        isClosed = !isClosed;
    }
}

分析:
Switch 类继承了 Device 类,模拟了一个简单的开关设备。通过 toggle() 方法改变 isClosed 状态,从而控制电流的通断。process() 方法根据开关的状态计算输出电压,getStatus() 方法返回开关的当前状态(“closed” 或 “turned on”)。这种设计体现了开关设备的典型功能:开关设备的电压输出由其自身的开关状态决定。

3. StepSpeedController

特色代码:

class StepSpeedController extends Device {
    private int level;

    public StepSpeedController(String id) {
        super(id);
        this.level = 0;
    }

    @Override
    public void process() {
        double multiplier = 0;
        switch (level) {
            case 1:
                multiplier = 0.3;
                break;
            case 2:
                multiplier = 0.6;
                break;
            case 3:
                multiplier = 0.9;
                break;
            default:
                multiplier = 0;
                break;
        }
        this.outputVoltage = inputVoltage * multiplier;
    }

    @Override
    public String getStatus() {
        return String.valueOf(level);
    }

    public void increaseLevel() {
        if (level < 3) level++;
    }

    public void decreaseLevel() {
        if (level > 0) level--;
    }
}

分析:
StepSpeedController 类模拟了一个分档调速器设备,通过不同的档位(level)调节电压输出。process() 方法根据档位 level 来设置输出电压的倍率,getStatus() 返回当前档位。通过 increaseLevel()decreaseLevel() 方法调整档位,这样就能动态地改变电压输出。这种设计适合应用在控制速度、亮度等需要调节的设备。

4. IncandescentLamp

特色代码:

class IncandescentLamp extends Device {
    private static final double MAX_VOLTAGE = 220;
    double value = 0;

    public IncandescentLamp(String id) {
        super(id);
    }

    @Override
    public void process() {
        double voltageDiff = Math.abs(inputVoltage);

        if(inputVoltage <= 9 && inputVoltage >= 0)
            value = 0;
        else if(inputVoltage == 10)
            value = 50;
        else if(inputVoltage == 220)
            value = 200;
        else if(inputVoltage > 10 && inputVoltage < 220) {
            double k = (200.0 - 50.0) / (220.0 - 10.0);
            double b = 50.0 - k * 10.0;
            value = inputVoltage * k + b;
        }
        outputVoltage = 0;
    }

    @Override
    public String getStatus() {
        return String.valueOf((int) value);
    }
}

分析:
IncandescentLamp 类模拟了一个白炽灯设备。白炽灯的亮度与输入电压相关,这里通过输入电压来计算亮度的数值(value)。当电压在 10V 到 220V 之间时,亮度是一个线性函数,通过求解该函数计算亮度。process() 方法根据输入电压计算亮度并更新状态,getStatus() 返回当前的亮度值。这一设计反映了设备的物理特性,并且将状态更新的逻辑封装在 process() 方法中。

5. CircuitSimulator

特色代码:

class CircuitSimulator {
    private Map<String, Device> devices = new HashMap<>();
    private List<String> connections = new ArrayList<>();
    private List<String> commands = new ArrayList<>();

    public void parseConnections() {
        for (String connection : connections) {
            String[] parts = connection.replaceAll("[\\[\\]]", "").split(" ");
            if (parts.length == 2) {
                String dev1Id = parts[0];
                String dev2Id = parts[1];

                if (dev1Id.equals("VCC")) {
                    String dev2BaseId = dev2Id.split("-")[0];
                    Device dev2 = devices.get(dev2BaseId);
                    if (dev2 != null) {
                        dev2.setInputVoltage(220);  // 设置 VCC 电压为 220V
                        dev2.process();
                    }
                } else if (dev2Id.equals("GND")) {
                    String dev1BaseId = dev1Id.split("-")[0];
                    Device dev1 = devices.get(dev1BaseId);
                    if (dev1 != null) {
                        dev1.setOutputVoltage(0);  // 设置 GND 电压为 0V
                        dev1.process();
                    }
                } else {
                    String dev1BaseId = dev1Id.split("-")[0];
                    String dev2BaseId = dev2Id.split("-")[0];

                    Device dev1 = devices.get(dev1BaseId);
                    Device dev2 = devices.get(dev2BaseId);

                    if (dev1 != null && dev2 != null) {
                        dev1.process();
                        dev2.setInputVoltage(dev1.getOutputVoltage());
                        dev2.process();
                    }
                }
            }
        }
    }
}

分析:
CircuitSimulator 类是整个模拟器的核心,负责设备之间的连接、命令的执行以及设备状态的更新。它通过 connections 列表管理设备之间的连接,通过 commands 列表执行用户输入的控制命令。在解析连接时,判断设备是否连接到电源(VCC)或地(GND),并根据连接的情况调整设备的输入和输出电压。parseConnections() 方法根据设备的连接关系动态地调整设备之间的电压流动,确保模拟的电路是符合实际的。

6. Main

特色代码:

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        CircuitSimulator simulator = new CircuitSimulator();

        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            if (line.equals("end")) break;

            if (line.startsWith("#")) {
                simulator.addCommand(line);
            } else if (line.startsWith("[") && line.endsWith("]")) {
                simulator.addConnection(line);
            }
        }

        simulator.parseCommands();
        simulator.parseConnections();
        simulator.showAllDevicesStatus();
    }
}

分析:
Main 类中,使用 Scanner 读取用户输入,

直到用户输入 end。输入的每一行都可能是一个命令或者连接。连接用 [] 括起来,命令以 # 开头。通过这些输入,模拟器动态地解析连接和命令,最终根据设备之间的连接关系和命令来更新每个设备的状态。

代码复杂度与质量分析(基于 SourceMonitor)

在第一次大作业中,我使用了 SourceMonitor 对代码进行了详细的复杂度分析,这里给出了项目的整体质量指标:

分析图表:

从 SourceMonitor 提供的报告和两个图表中,我们可以分析代码的复杂度和结构特性,并进一步思考如何优化代码质量。

1. 复杂度雷达图

复杂度雷达图展示了代码的核心统计数据:

平均复杂度(Avg Complexity):代码的平均复杂度为 2.73,属于较低水平。说明整体代码的逻辑不复杂,没有深层次的嵌套逻辑,符合简洁性的要求。
最大复杂度(Max Complexity):最高复杂度为 10,出现在 Main.main() 方法中。这表明 main() 方法的逻辑较为集中,可能需要进一步拆分和优化。
每类方法数(Methods/Class):每个类平均有 8.4 个方法,说明代码功能较为细化,但仍存在进一步解耦的优化空间。
每个方法平均语句数(Avg Stmts/Method):每个方法平均有 8.36 条语句,表明方法逻辑适中,较为简洁,但某些方法可能需要进一步简化。
注释覆盖率(% Comments):注释覆盖率仅为 13.4%,这一指标偏低,表明代码的可读性和文档化不足,可能对团队协作和长期维护造成不利影响。
嵌套深度(Avg Depth & Max Depth):代码的平均块深度为 1.5,最大深度为 6。平均深度较浅,表明代码逻辑结构较为扁平,最大深度集中在复杂方法中,需要特别关注。

从SourceMonitor的分析来看,代码在复杂度控制方面表现良好,但是也有注释覆盖率不足(下次会着重增加注释量的)和部分复杂方法需要拆分的改进点。


2. 块深度直方图

块深度直方图展示了代码块的嵌套深度分布:

块深度为 0 到 2:绝大部分代码块(284 个语句)的嵌套深度集中在 0 至 2。这表明代码的逻辑较为简单直观,避免了过多的层级嵌套,从而提高了代码的可读性和维护性。
块深度为 3 到 5:少部分代码块(72 个语句)的深度为 3 到 5,这些地方可能是控制逻辑集中或复杂的地方。
块深度为 6 及以上:只有极少数代码块达到深度 6。这些深度较大的代码块可能引入了复杂的逻辑嵌套,需要重点检查是否存在进一步优化的可能性。

这表明代码的逻辑设计偏向扁平化,整体结构简洁。但需要注意深度为 3 及以上的代码块是否引入了难以维护的复杂性,尤其是出现在关键方法中的复杂块。


3. 复杂度较高的方法分析

从 SourceMonitor 报告中提取的复杂方法数据:

方法名称 复杂度 语句数 最大深度 方法调用数
FloorFan.updateState() 6 11 3 0
FluorescentLamp.updateState() 3 5 3 0
IncandescentLamp.updateState() 4 8 3 0
Main.main() 10 36 6 28

Main.main() 方法
复杂度为 10,是代码中复杂度最高的方法。
语句数达到 36,且最大深度为 6,有 28 次方法调用。这说明 main() 方法中包含了大量的逻辑操作和设备初始化、命令解析等任务。
日后的改进建议Main.main() 方法职责过重,可以考虑将初始化和命令解析逻辑拆分成独立的方法或类。例如:
1. 提取设备初始化到专门的 DeviceInitializer 类。
2. 将命令解析和执行的逻辑封装到一个 CommandHandler 类中。
3. 保持 main() 方法仅负责高层的流程控制。

FloorFan.updateState() 方法
复杂度为 6,语句数为 11,嵌套深度为 3。这可能是因为该方法对输入电压进行多条件判断并更新状态。
改进建议:可以尝试将复杂的逻辑分解为多个辅助方法,例如单独处理电压条件的判断逻辑。


4. 代码复杂度总结

优点

  1. 整体复杂度较低:平均复杂度 2.73,最大复杂度 10,说明代码逻辑简单,避免了深度嵌套和复杂操作。
  2. 模块化设计较好:每个类封装了设备的独立逻辑,类与类之间通过输入电压和连接关系交互,体现了良好的面向对象设计。
  3. 块深度控制良好:绝大多数代码块的深度集中在 0 到 2,表明代码结构清晰,易于阅读和维护。

改进方向

  1. 注释覆盖率偏低:注释覆盖率仅为 13.4%,需要增加对方法、类和复杂逻辑的注释,帮助开发者更好地理解代码。
  2. 复杂方法的职责分离Main.main() 方法复杂度高,逻辑集中,建议将其拆分成更小、更独立的模块。
  3. 提高代码复用性:对于设备之间类似的逻辑(如状态更新和输入电压处理),可以考虑提取公共父类或工具方法,减少代码重复。

说实话,第一次作业看上去很简单,但涉及到准确解析输入的时候,我还是遇到了一些麻烦。特别是解析输入,原来的逻辑常常崩溃,调试的过程比我预想的要痛苦得多。

类图分析(基于 PowerDesigner)


类及其结构

1. Device

属性
protected String id:设备的唯一标识符。
protected double inputVoltage:设备的输入电压。
protected double outputVoltage:设备的输出电压。

构造方法
public Device(String id):初始化设备,设置 idinputVoltageoutputVoltage 默认为 0。

方法
public abstract void process():处理电压输出的抽象方法。
public abstract String getStatus():返回设备的状态信息的抽象方法。
public void setInputVoltage(double input):设置设备的输入电压。
public double getOutputVoltage():获取设备的输出电压。
public void setOutputVoltage(double output):设置设备的输出电压。
public String getId():获取设备的 ID。


2. Switch

继承自 Device

属性
private boolean isClosed:开关是否闭合。

构造方法
public Switch(String id):初始化开关设备,设置 isClosedfalse

方法
public void process():处理电压输出,若开关闭合,输出电压与输入电压相同,否则输出 0。
public String getStatus():返回开关的状态(closedturned on)。
public void toggle():切换开关的状态。


3. StepSpeedController

继承自 Device

属性
private int level:调速器的档位(1-3)。

构造方法
public StepSpeedController(String id):初始化调速器,设置档位为 0。

方法
public void process():根据档位调整电压输出。
public String getStatus():返回当前档位的状态。
public void increaseLevel():增加档位。
public void decreaseLevel():减少档位。


4. ContinuousSpeedController

继承自 Device

属性
private double position:调速器的位置(0.0 到 1.0)。

构造方法
public ContinuousSpeedController(String id):初始化调速器,设置位置为 0.0。

方法
public void process():根据位置调整电压输出。
public String getStatus():返回当前的位置(0.00 到 1.00)。
public void setPosition(double position):设置调速器的位置。


5. IncandescentLamp

继承自 Device

属性
private static final double MAX_VOLTAGE = 220:最大电压。
private double value:光强度。

构造方法
public IncandescentLamp(String id):初始化白炽灯。

方法
public void process():根据输入电压计算光强度。
public String getStatus():返回当前的光强度。


6. FluorescentLamp

继承自 Device

属性
private double value:光强度。

构造方法
public FluorescentLamp(String id):初始化日光灯。

方法
public void process():根据输入电压是否大于 0 来决定输出的光强度。
public String getStatus():返回当前的光强度。


7. Fan

继承自 Device

属性
private static final double MIN_VOLTAGE = 80:最小电压。
private static final double MAX_VOLTAGE = 150:最大电压。
private double value:风扇的输出电压。

构造方法
public Fan(String id):初始化风扇设备。

方法
public void process():根据输入电压计算风扇输出电压。
public String getStatus():返回当前的风扇输出电压。


8. CircuitSimulator

属性
private Map<String, Device> devices:存储所有设备的映射。
private List<String> connections:存储设备间连接的列表。
private List<String> commands:存储执行的命令列表。

构造方法
public CircuitSimulator():初始化设备集合、连接信息和命令列表。

方法
public void addDevice(String deviceId):根据设备 ID 动态添加设备。
public void addConnection(String connection):添加设备之间的连接。
public void addCommand(String command):添加设备命令。
public void parseConnections():解析设备连接并设置电压。
public void parseCommands():执行设备命令。
public void showAllDevicesStatus():显示所有设备的状态。


关系示意

继承关系
SwitchStepSpeedControllerContinuousSpeedControllerIncandescentLampFluorescentLampFan 都继承自 Device

聚合关系
CircuitSimulator 聚合 Device,管理多个设备对象。

依赖关系
CircuitSimulator 依赖 Device 类,使用它的 process()getStatus() 方法来模拟电路。

设计心得

在开发 家居强电电路模拟程序 的过程中,我感受到了一次完整的设计、实现和优化过程所带来的启发与成长。这不仅仅是对代码逻辑的深入探索,更是对设计原则与编码习惯的实践和反思。以下是我的一些心得体会:


1. 模块化设计的重要性

在程序中,不同类型的设备(如开关、调速器、灯具、风扇等)通过继承一个共同的抽象类 Device 实现了功能的模块化。这种设计让我深刻认识到:
抽象是复杂系统的简化器:通过将设备的通用功能抽象为父类,具体的实现逻辑被分散到各个子类中,不仅增强了代码的可读性,也降低了系统的耦合度。
职责单一原则(SRP):每个类只处理自己的职责,比如 Switch 负责控制通断,StepSpeedController 负责调速,这种明确的分工使得代码在逻辑上更容易维护和扩展。

启发:模块化设计帮助我更好地组织代码,尤其是当项目规模扩大时,清晰的类划分和职责分离将显得尤为重要。这种思路让我对面向对象设计原则有了更直观的理解。


2. 平衡灵活性与复杂性

在实现 CircuitSimulator 时,我一开始尝试将所有设备连接的逻辑写死,但发现这样会导致扩展性极差。例如,当需要支持新设备类型时,我不得不修改核心代码。后来通过动态地将设备类型与具体类关联,我实现了更灵活的设计:

char type = baseId.charAt(0);
switch (type) {
    case 'K': device = new Switch(baseId); break;
    case 'F': device = new StepSpeedController(baseId); break;
    ...
}

通过这种设计,当需要引入新设备类型时,只需新增一个设备类并修改少量代码即可。

启发:灵活性通常需要付出复杂性的代价,但在系统架构中,适度的灵活性是必要的。通过抽象工厂模式或配置文件初始化等方式,可以进一步降低硬编码的复杂性,提升代码的适应性。


3. 复杂性管理

在程序中,Main.main() 方法一度成为复杂性聚集点。由于它负责设备初始化、连接解析、命令执行等多项任务,导致方法长度和复杂度急剧增加。这让我认识到:
代码复杂性不应集中于单一方法或类:当一个方法负责过多的职责时,不仅难以阅读和维护,还会使代码更容易出错。
分解复杂性,拥抱分层架构:通过将复杂的逻辑拆分到独立的类中(如 CommandHandlerConnectionParser),主流程逻辑得以保持清晰。虽然增加了类的数量,但代码的结构性得到了显著改善。

启发:复杂性是程序设计的天敌,而解耦是对抗复杂性的重要武器。在未来的设计中,我将更加注重代码的分层与职责分离,避免复杂逻辑的集中。


4. 注释与文档的价值

在实现过程中,我注意到注释覆盖率仅为 13.4%,这一指标直接影响了代码的可读性。尤其是当代码中包含复杂的逻辑或数学计算(如白炽灯亮度的线性插值计算)时,缺少注释会让后来者难以快速理解其含义。

通过后期的补充注释,我发现:
注释不仅是对他人的帮助,更是对自己的提醒:注释可以记录设计思路和关键逻辑,从而帮助未来的维护者理解代码的设计意图。
好的注释是解释“为什么”,而不是“是什么”:注释不应仅描述代码表面的含义,而是需要解释背后的设计思路和决策依据。

启发:良好的注释是高质量代码的重要组成部分。未来在编码时,我将养成及时添加注释的习惯,特别是在处理复杂逻辑时,确保每一段代码都有清晰的解释。


家居强电电路模拟程序-2

任务描述

家居强电电路模拟程序的第二版,总算是让我体验了一把“上天入地”的感觉。比起第一版,这次的要求明显更复杂,啥串联电路、并联电路,设备之间还开始搞电阻、搞电压差,甚至还要计算灯的亮度和风扇的转速。别的不说,光是看完题目,我就差点觉得“要疯了”。

不过仔细想想,也挺带劲。这次设计不仅要搞定复杂的电路拓扑,还得保证每个设备都按它的“个性”来工作。我一边设计类结构,一边脑补设备的实际运行逻辑,感觉就像是在给一套智能家居系统写操作系统似的,满脑子都是电路和代码。


1. ElectricDevice 类:设备的灵魂基石

所有设备的共同点都藏在这个基类里。它像是一个设备的模板,把输入、输出、电阻等共性抽象出来。这个基类的存在,可以让我优雅地对各种设备“一视同仁”。

特色代码
public abstract class ElectricDevice {
    protected String id;
    protected double inputVoltage;
    protected double outputVoltage;
    protected double resistance;

    public ElectricDevice(String id) {
        this.id = id;
        this.inputVoltage = 0.0;
        this.outputVoltage = 0.0;
        this.resistance = 0.0;
    }

    public abstract void updateState();
    public abstract String getStatus();

    public void setInputVoltage(double voltage) {
        this.inputVoltage = voltage;
        updateState();
    }

    public double getOutputVoltage() {
        return outputVoltage;
    }
}
设计思路
  1. 统一输入与输出:通过 setInputVoltage() 传递电压,输出通过 getOutputVoltage() 获取,逻辑清晰。
  2. 核心逻辑抽象updateState() 是设备的灵魂方法,设备的所有行为都通过它实现。
  3. 扩展能力:这个基类让我只需要关心“设备要做什么”,而不是“设备怎么连接”。每新增一种设备,只要继承 ElectricDevice 并实现抽象方法即可。

2. 控制设备:电路的指挥家

控制设备的任务很明确——它们用来调节电路的行为,包括开关、分档调速器和连续调速器。这类设备的重点是“灵活性”:调节幅度、控制逻辑都要够直观。

2.1 开关设备 SwitchDevice

开关的逻辑就像它的名字:开或关。虽然它是最简单的设备,但在整个系统中却是最关键的“守门员”。

特色代码
@Override
public void updateState() {
    this.outputVoltage = isClosed ? this.inputVoltage : 0.0;
}

@Override
public String getStatus() {
    return "@" + id + ":" + (isClosed ? "closed" : "turned on");
}
设计亮点

开关的逻辑就是“二元判断”,没有花哨的逻辑。
状态输出直观明了,便于调试和追踪。
作为所有设备的“开关门员”,它在实现简洁性的同时,也让电路行为变得可控。


2.2 分档调速器 StepDimmer

分档调速器有 4 个档位,分别对应 0%、30%、60%、90% 的输出电压。它像是电路中的“齿轮箱”,让我可以手动调整输出。

特色代码
@Override
public void updateState() {
    double[] multipliers = {0.0, 0.3, 0.6, 0.9};
    this.outputVoltage = this.inputVoltage * multipliers[level];
}

@Override
public String getStatus() {
    return "@" + id + ":" + level;
}
设计亮点

通过一个简单的 multipliers 数组实现档位倍率映射,不需要写一堆 if-else
每次切换档位后自动更新输出状态,逻辑流畅,使用方便。
输出状态以档位为核心,让状态变得清晰可见。


2.3 连续调速器 ContinuousDimmer

连续调速器比分档调速器“高大上”了不少,它可以以任意比例输出电压。为了保证输出精确到小数点两位,我在代码里稍微用了一点小技巧。

特色代码
public void setLevel(double level) {
    this.level = Math.floor(level * 100) / 100.0; // 保留两位小数
    updateState();
}

@Override
public void updateState() {
    this.outputVoltage = this.inputVoltage * this.level;
}
设计亮点

通过 Math.floor() 精确控制档位比例,输出结果优雅且规整。
支持任意比例的调节,非常灵活,几乎可以满足任何电路需求。
更新逻辑简单,调用 setLevel() 后一切自动完成。


3. 受控设备:电路的被动成员

受控设备是电路的“执行者”,它们根据输入电压做出反应,比如灯光亮度、风扇转速。这些设备的逻辑稍微复杂一些,因为它们需要考虑实际的物理规律。

3.1 白炽灯 IncandescentLamp

白炽灯的亮度是线性变化的,但亮度范围被限定在 50lux 到 200lux 之间。这段代码写得有点像高中物理题,但最终结果让我非常满意。

特色代码
@Override
public void updateState() {
    if (inputVoltage <= 9.0) {
        this.outputVoltage = 0.0;
    } else {
        double lux = 50.0 + (inputVoltage - 10.0) * 150.0 / 210.0;
        this.outputVoltage = Math.min(lux, 200.0);
    }
}
设计亮点

利用线性公式精确计算亮度,符合真实物理规律。
通过 Math.min() 限制亮度最大值,防止灯“爆表”。
输出结果稳定可靠,尤其在电压边界值时,表现尤为出色。


3.2 吊扇 CeilingFan

吊扇的转速与电压差线性相关,但工作范围是 80V 到 150V。超过范围时,转速保持固定值。

特色代码
@Override
public void updateState() {
    if (inputVoltage < 80.0) {
        this.outputVoltage = 0.0;
    } else if (inputVoltage >= 150.0) {
        this.outputVoltage = 360.0;
    } else {
        this.outputVoltage = 80.0 + (inputVoltage - 80.0) * 280.0 / 70.0;
    }
}
设计亮点

电压与转速的线性关系体现了吊扇的物理特性,逻辑非常直观。
通过分段逻辑处理电压范围外的情况,确保输出转速符合预期。


4. 串联与并联电路:连接的艺术

4.1 串联电路

串联电路将设备按顺序连接,电压逐一传递。实现时,我直接用了一个设备顺序列表,让电压从头传到尾,逻辑非常直观。

特色代码
for (String conn : sc.getConnections()) {
    String devId = conn.split("-")[0];
    ElectricDevice dev = devices.get(devId);
    dev.setInputVoltage(currentVoltage);
    currentVoltage = dev.getOutputVoltage();
}
设计亮点

设备按顺序传递电压,模拟了真实的串联电路。
代码结构简单,扩展性强,能够轻松适配更多设备。


4.2 并联电路

并联电路通过等效电阻公式计算总电阻,并按比例分配电压。这段逻辑的实现让我回忆起高中物理课上的电路计算公式。

特色代码
double reciprocal = 0.0;
for (double r : branchResistances) {
    if (r != 0.0) reciprocal += 1.0 / r;
}
double parallelResistance = reciprocal != 0.0 ? 1.0 / reciprocal : 0.0;
设计亮点

等效电阻公式完美体现了并联电路的物理规律。
为每个并联分支动态分配电压,模拟效果逼真。


5. 输出设计:程序的最后一公里

我按设备类型和编号排序输出状态,既要美观也要实用。最终输出的格式让我觉得清晰又规范,调试起来格外舒心。

特色代码
String[] order = {"K", "F

", "L", "B", "R", "D", "A"};
for (String type : order) {
    for (ElectricDevice device : categorizedDevices.get(type)) {
        System.out.println(device.getStatus());
    }
}
设计亮点

按设备类型和编号排序,让输出结果逻辑性更强。
每行状态信息都清晰直观,方便排查问题。


总结

这次的设计让我深刻感受到复杂系统的魅力。每个类都有自己的职责,设备逻辑与电路关系相辅相成。虽然中途也有迷茫,但每次看着代码变得更加简洁、更加稳定时,我都忍不住感叹:编程还真是一件既烧脑又让人上瘾的事!

代码复杂度与质量分析(基于 SourceMonitor)


代码复杂度与质量分析(基于 SourceMonitor)

在第二次大作业中,我使用了 SourceMonitor 对代码进行了详细的复杂度分析,这里给出了项目的整体质量指标:

分析图表:

从 SourceMonitor 提供的报告和两个图表中,我们可以分析代码的复杂度和结构特性,并进一步思考如何优化代码质量。

主要指标与结果

通过 SourceMonitor 工具分析后,我得到了以下一些数据:
代码行数:597 行
语句数:303 条
分支语句占比:23.8%
方法调用语句:110 条
注释行百分比:18.6%
类和接口数量:6 个
每个类的方法数量:平均 7.17
每个方法的平均语句数:6.98 条
最复杂的方法Main.main(),复杂度为 6
最大代码块深度:5
平均代码块深度:2.11
平均复杂度:1.86

看到这些指标后,我意识到代码的整体复杂度虽然没有非常高,但有几个关键方法的复杂性值得关注,尤其是 Main.main() 方法,它的复杂度和块深度在代码中属于最高的部分。


复杂度雷达图与块深度直方图

复杂度雷达图(Kiviat 图):
从复杂度雷达图中,我可以清楚地看到代码在“最大复杂度”和“最大块深度”上的表现尤为突出。这表明某些方法内部逻辑较为复杂,特别是像 Main.main() 这样的方法,承担了较多的功能职责。

块深度直方图:
块深度分布显示,大多数代码的嵌套深度在 2 到 4 之间,这还算在可接受范围内。但最大块深度为 5,这部分代码主要集中在一些逻辑较为复杂的方法中,比如 Fan1.process()IncandescentLamp.process()。嵌套深度过大让我意识到,需要对这些部分进行优化。


复杂度较高的方法

1. Main.main() 方法
复杂度:6
语句数:11
最大块深度:4
方法调用次数:10

Main.main() 方法作为主入口,它包含了输入解析、设备初始化和主逻辑控制等功能。虽然它的复杂度不是特别高,但多层嵌套和多分支处理让代码看起来有些臃肿。我认为,将这一方法的逻辑拆分成多个辅助方法会更加清晰。

2. IncandescentLamp.process() 方法
复杂度:6
语句数:12
最大块深度:3
方法调用次数:1

这个方法负责模拟白炽灯的状态更新。由于逻辑中涉及亮度计算和多层条件判断,复杂度相对较高。我认为可以通过优化分支结构,进一步降低它的复杂性。

3. Fan1.process() 方法
复杂度:6
语句数:11
最大块深度:3
方法调用次数:0

这个方法的逻辑主要集中在风扇的速度模拟和状态更新上。虽然块深度没有超过 4,但语句密集且涉及多个逻辑条件。我觉得这里可以提取公共逻辑,进一步优化代码的结构。


改进计划

1. 提高注释覆盖率
分析结果显示,我的注释覆盖率仅为 18.6%。这让我意识到,尽管我在编写代码时可能加了一些注释,但还是有许多关键部分的逻辑没有被清晰标注。特别是 Main.main() 这样的重要方法,详细的注释可以帮助后续阅读代码的同学或自己更快地理解其中的逻辑。

2. 优化复杂方法
针对 Main.main() 和其他复杂度较高的方法,我打算通过以下方式优化:
将方法内部的分支逻辑拆分为多个辅助方法。例如,可以把“初始化设备”和“解析输入”的功能分开处理。
简化多层嵌套,避免块深度过大。

3. 减少嵌套深度
嵌套深度大的代码块非常影响阅读和维护。我准备通过以下措施改进:
提取重复逻辑,将其封装为独立的方法。
使用状态模式来处理条件逻辑,减少嵌套。

4. 改进模块化设计
目前,有些类的职责较多,比如 Main 类和设备相关的类。我计划通过模块化设计,把不同设备的逻辑拆分到更小的类中,这样每个模块都可以专注于完成单一功能。


代码评价总结

通过这次复杂度分析,我对自己的代码有了更深入的了解。虽然整体代码的复杂度尚可,但某些关键方法如 Main.main() 的设计需要进一步优化。我意识到注释的重要性,同时也认识到代码结构的模块化和清晰性对后续维护的价值。

接下来,我会根据 SourceMonitor 的分析结果,逐步提高代码的注释覆盖率,优化复杂方法,并减少代码块的嵌套深度。这不仅能让代码更加易读和易维护,也能让我在编程能力上更进一步。

类图分析(基于 PowerDesigner)

类图详细构成

1. ElectricDevice

属性
protected String id:设备ID。
protected double inputVoltage:输入电压。
protected double outputVoltage:输出电压。
protected double resistance:电阻。

构造方法
public ElectricDevice(String id):初始化设备ID,输入电压、输出电压、和电阻的默认值为 0.

方法
public abstract void updateState():抽象方法,用于更新设备状态。
public void setInputVoltage(double voltage):设置输入电压并调用 updateState() 更新设备状态。
public double getOutputVoltage():返回输出电压。
public String getId():返回设备ID。
public abstract String getStatus():抽象方法,用于获取设备的状态字符串。


2. ControlDevice(继承 ElectricDevice

构造方法
public ControlDevice(String id):继承自 ElectricDevice,初始化设备ID。

方法
继承自 ElectricDevice 的所有方法。


3. SwitchDevice(继承 ControlDevice

属性
private boolean isClosed:设备开关状态(关闭/打开)。

构造方法
public SwitchDevice(String id):初始化开关状态为 "turned on"(即 false)。

方法
public void toggle():切换开关状态(打开或关闭)。
public void updateState():根据开关状态更新输出电压(打开时输出电压等于输入电压,关闭时输出电压为0)。
public String getStatus():返回开关的状态字符串。


4. StepDimmer(继承 ControlDevice

属性
private int level:调光器的档位(0-3)。

构造方法
public StepDimmer(String id):初始化档位为 0。

方法
public void increaseLevel():增加档位。
public void decreaseLevel():减少档位。
public void updateState():根据档位更新输出电压(不同档位有不同的电压倍数)。
public String getStatus():返回调光器的状态字符串(显示档位)。


5. ContinuousDimmer(继承 ControlDevice

属性
private double level:调光器的档位(0.00-1.00)。

构造方法
public ContinuousDimmer(String id):初始化档位为 0.0。

方法
public void setLevel(double level):设置档位,范围为 0.00 到 1.00。
public void updateState():根据档位更新输出电压。
public String getStatus():返回调光器的状态字符串(显示档位)。


6. ControlledDevice(继承 ElectricDevice

构造方法
public ControlledDevice(String id):初始化设备ID。

方法
继承自 ElectricDevice 的所有方法。


7. IncandescentLamp(继承 ControlledDevice

构造方法
public IncandescentLamp(String id):初始化电阻为 10.0。

方法
public void updateState():根据输入电压计算输出电压。电压差小于 9V 时,输出 0V。
public String getStatus():返回灯的状态(电压值)。


8. FluorescentLamp(继承 ControlledDevice

构造方法
public FluorescentLamp(String id):初始化电阻为 5.0。

方法
public void updateState():根据输入电压计算输出电压(0V 或 180V)。
public String getStatus():返回灯的状态(电压值)。


9. CeilingFan(继承 ControlledDevice

构造方法
public CeilingFan(String id):初始化电阻为 20.0。

方法
public void updateState():根据输入电压计算输出电压(电压在 80V 到 360V 之间)。
public String getStatus():返回风扇的状态(电压值)。


10. FloorFan(继承 ControlledDevice

构造方法
public FloorFan(String id):初始化电阻为 20.0。

方法
public void updateState():根据输入电压计算输出电压(从 80V 到 360V 之间的多个档位)。
public String getStatus():返回风扇的状态(电压值)。


11. SerialCircuit

属性
private String id:串联电路ID。
private List<String> connections:设备连接列表。

构造方法
public SerialCircuit(String id):初始化电路ID和连接列表。

方法
public void addConnection(String connection):添加连接。
public String getId():返回电路ID。
public List<String> getConnections():返回连接列表。


12. ParallelCircuit

属性
private String id:并联电路ID。
private List<String> serialCircuitIds:串联电路ID列表。

构造方法
public ParallelCircuit(String id):初始化电路ID和串联电路ID列表。

方法
public void addSerialCircuit(String serialId):添加串联电路。
public String getId():返回并联电路ID。
public List<String> getSerialCircuitIds():返回串联电路ID列表。


关系示意

继承关系
ControlDeviceControlledDevice 都继承自 ElectricDevice,形成 继承关系
SwitchDevice, StepDimmer, ContinuousDimmer, IncandescentLamp, FluorescentLamp, CeilingFan, FloorFan 都继承自 ControlDeviceControlledDevice,形成 继承关系

聚合关系
SerialCircuitParallelCircuit 各自包含多个设备ID,形成 聚合关系SerialCircuitParallelCircuitElectricDevice)。



设计心得

  1. 面向对象与继承的应用
    本次设计采用了面向对象的思想,主要通过继承和多态实现设备类型的扩展与状态的管理。通过抽象类 ElectricDevice 和具体子类(如 SwitchDeviceFanDevice 等),实现了不同设备的统一管理与个性化行为。继承使得代码结构更加清晰,同时避免了重复代码,方便后续扩展和维护。

  2. 简化模拟,专注核心功能
    电气设备的模拟遵循了简化原则,没有考虑过于复杂的电气原理(如具体的电压、电流计算),主要模拟设备的开关、状态更新和电路连接等功能。这既保证了模拟的简洁性,也让系统易于理解和操作。虽然如此,设计时仍尽量保留了现实电路中串联与并联的基本概念。

  3. 命令设计与状态管理
    设备的控制采用简短的命令(如 #K#F),这简化了用户输入的复杂度,使得操作更为直观。状态更新功能通过多态机制让不同类型的设备能够自行处理状态变化,避免了冗余的代码逻辑。

  4. 扩展性与可维护性
    设计时考虑到未来的扩展需求,特别是在设备类型和电路模型上的扩展。通过合理的类结构和接口定义,新设备可以轻松地加入到系统中,且不会影响现有功能。设计时尽量保持了低耦合,高内聚的原则,确保系统具有较好的可维护性。

  5. 潜在的改进方向
    虽然目前的设计已经能满足基本需求,但在错误处理和用户输入验证方面还有待完善。比如,在命令输入错误时,系统应提供友好的错误提示或容错机制。此外,如果未来需要更加精确的电气模拟(如功率计算、电流分配等),则需要进一步加强底层计算模块的设计。


PTA 家居强电电路模拟程序系列作业总结


踩坑心得

1. 正则表达式解析问题

第一次作业中,输入格式相对简单,但由于格式中包含特殊字符(如 []、空格),我初次使用的正则表达式对其处理不够灵活,导致解析失败,程序直接崩溃。
问题原因:未正确转义特殊字符,或未考虑某些输入的边界情况。
解决方法:优化正则表达式规则,将复杂格式逐步拆解,同时加入异常捕获机制。例如在解析连接信息时,先用正则表达式提取 [] 中的内容,再逐一解析每个部分,降低了正则表达式的复杂性。


2. 混合输入顺序管理

第二次作业中需要同时解析串联电路、并联电路和设备信息。这些数据类型顺序混杂,不同类型的输入相互依赖,而一开始的解析逻辑试图在一次遍历中完成所有解析,结果由于输入顺序问题导致部分数据解析失败。
问题原因:未考虑输入的依赖关系,例如并联电路需要串联电路的完整信息,设备连接需要电路拓扑完成后才能解析。
解决方法:采用多轮解析方案:

  1. 第一轮解析设备信息和简单的串联电路;
  2. 第二轮解析并联电路信息,逐条构建子电路;
  3. 最后一轮将所有电路拼接成完整的主电路。
    通过多轮解析,我将依赖关系按顺序拆解,保证了每一部分的数据在下一轮解析前完整无误。

3. 混合电路的电压计算

第二次作业中最大难点在于电路计算,尤其是串联与并联电路的混合情况。并联电路中需要计算等效电阻,再结合串联关系分配电压。最初代码直接按设备遍历电路,忽略了电路分支的存在,导致结果不正确。
问题原因:没有正确处理并联电路的等效电阻,导致电压分配错误。
解决方法

  1. 并联电阻计算:对并联电路的所有分支计算等效电阻:
  2. 电压分配:并联分支电压相等,电流按分支电阻反比分配。
  3. 串联关系:按照电流恒定的原则,从电源逐步传播电压,并更新设备状态。
    通过这种分段计算的方式,最终实现了混合电路的正确电压分配。

4. 输入解析与设备连接

输入解析中包含设备的动态添加和连接关系的解析。例如 [K1-1 K2-2] 表示将设备 K1 的 1 引脚与设备 K2 的 2 引脚相连。一开始的实现中,解析直接将连接关系添加到设备,而没有检查连接合法性,导致部分设备重复连接或漏连接。
问题原因:未对设备连接的合法性和顺序进行校验。
解决方法:引入 pinToDevice 映射,将所有引脚与设备进行绑定,确保每个引脚对应唯一设备,同时检查连接点是否存在未定义的设备。解析时,优先建立引脚到设备的映射,再按顺序解析连接信息。


改进建议

1. 模块化设计

目前的输入解析、设备状态更新、电路计算和输出逻辑都集中在主方法和核心类中,耦合度较高,后续扩展较难。
改进方案:将主要逻辑模块化:
输入模块:专门负责解析不同格式的输入。
电路模块:处理电路拓扑和电压计算。
设备模块:定义设备行为及其独立状态更新逻辑。
输出模块:将设备状态格式化并统一输出。

这样每个模块的功能更加单一,代码维护成本降低。


2. 严格输入验证

现有解析逻辑对输入的边界情况考虑不足,例如重复连接、未定义设备、无效引脚等。
改进方案:增加输入验证逻辑:
确保设备编号唯一。
校验引脚连接的合法性,避免未定义设备或引脚重复。
对无效输入及时提示并停止解析。


3. 优化数据结构

当前大部分数据存储在 HashMapList 中,虽然实现简单,但在按顺序访问或复杂查找时效率不高。
改进方案
使用 TreeMap 按编号排序设备。
引入邻接表结构存储电路连接关系,便于拓扑遍历。
对并联电路用独立类封装其子电路,减少对主电路的干扰。


学到的内容

1. 面向对象编程

两次作业让我深刻理解了面向对象编程的三大核心原则:
封装:通过 ElectricDevice 和其子类,将设备逻辑与电路逻辑分离,设备只需要关注自身行为。
继承:将设备的共性抽象在基类中,子类只需实现特定功能,简化了代码扩展。
多态:通过统一接口调用不同设备的状态更新方法,降低了代码耦合。


2. 电路物理特性的模拟

通过代码实现串联与并联电路,我深入理解了物理规律在编程中的应用:
串联电路中,电流相同,电压逐步分配;
并联电路中,电压相等,电流按分支电阻反比分配;
等效电阻公式简化了电路的计算逻辑,为模拟复杂电路提供了理论支持。


3. 数据结构应用

两次作业让我认识到数据结构的重要性:
使用 Map 存储设备信息,快速查找设备状态;
List 表示串联关系,按顺序更新电压;
用邻接表存储复杂的并联关系,提高了数据组织的灵活性。


4. 输入解析与错误处理

解析复杂输入让我学会了分步处理的技巧:
通过正则表达式逐层提取信息,降低解析复杂度;
增加异常处理逻辑,保证解析过程稳健。


最终反思

  1. 细节决定成败
    从正则表达式到并联电阻公式,每个细节都可能影响程序的稳定性和正确性。这次作业让我认识到,在复杂系统中,任何疏忽都可能引发连锁问题。

  2. 代码设计的前瞻性
    第二次作业的混合电路让我意识到,初始设计时必须考虑系统的扩展性。如果在第一次作业中就将电路关系抽象为模块化结构,第二次作业的实现难度会降低很多。

  3. 调试的重要性
    两次作业中,调试时间远超编码时间。通过不断分析测试点失败的原因,我学会了从错误中寻找问题根源,并逐步完善程序。


对课程与作业的建议

  1. 增加案例分析
    希望课程中能讲解更多实际开发中的案例,比如如何优化电路模拟的效率、如何处理复杂输入等,帮助学生将理论与实践更好地结合。

  2. 强化代码审查
    建议引入代码审查环节,通过同学间的互相点评,学习不同的实现思路和优化方法。

  3. 提供更高层次的挑战
    在掌握基础知识后,可以尝试设计更复杂的任务,比如动态电路拓扑调整、并联电路嵌套等。


通过这两次作业,我不仅加深了对电路和编程的理解,还学会了如何在复杂任务中梳理思路、优化设计。这些经验让我对未来的编程学习充满信心,同时也认识到,写出“好代码”是一个不断学习和进步的过程。

posted @ 2024-11-19 20:37  NCHU-徐小磊  阅读(3)  评论(0编辑  收藏  举报