从“写对”到“写好”——作业集1~3迭代开发总结
一、前 言
本阶段围绕“航空器配载与货运管理系统”完成了三次PTA作业集的迭代开发。作业场景从单货舱基础装载扩展到多货舱管理,再升级到旅客管理与载重平衡计算,难度呈阶梯式递进。下面我将对三次作业的知识点覆盖、题量与难度变化进行简要梳理。
1.1 知识点演变
三次作业的知识点呈现清晰的进阶脉络:
维度 作业集1 作业集2 作业集3
基础语法 Java类与对象、基本数据类型 集合框架(ArrayList) 输入输出、精度控制
面向对象 类定义、封装、构造方法 类间关联(聚合/组合/依赖) 单一职责原则强化、private封装
核心算法 冒泡排序/选择排序 稳定性排序(同重保序) 重量-力矩物理计算、重心百分比
业务逻辑 单货舱装载+重量排序 多货舱管理、跨舱分配 旅客/行李管理、重心安全校验
工程实践 基础输入输出 输入校验、模块化拆分 迭代开发、代码重构
从表格可以看出,作业集1侧重于语法入门和基础数据结构,让我初步理解了类与对象的关系;作业集2开始强调类间关系的设计能力,对组合、聚合、依赖有了明确要求;作业集3则要求在严格遵循设计原则的前提下,实现完整的业务闭环。
1.2 题量与难度曲线
三次作业的代码量呈递增趋势。我记录了自己的实际投入时间:
作业集1(约280行):初次接触迭代作业,面对类图不知如何下手,仅在思考整体设计就花费了整整两天,编码又用了一天,合计超过12小时。
作业集2(约480行):熟悉了开发流程,但由于初期设计与题目要求存在不少出入,调试耗费了大量时间,合计约6小时。
作业集3(约800行):业务复杂度陡增,引入旅客管理和物理重心计算后,类间关系异常复杂,仅调试就花了近两天,合计约16小时。
难度方面,个人认为第三次作业的挑战最为突出:一是业务场景复杂度从“管货物”变成“管旅客+货物+重心”,同时处理多项业务逻辑;二是类间关系设计难度显著增大,既要新增旅客和行李类,又要保证不破坏原有代码逻辑;三是重心计算涉及物理公式,一旦计算前数据有错漏,最终结果就会偏离正确答案且难以排查。
二、设计与分析
本章将通过类图演进展示三次作业的架构变化,并借助SourceMonitor的代码度量数据,量化分析代码质量的变化轨迹。
2.1 第一次作业:基础框架搭建
(1)业务需求
实现一个航空器货物装载信息管理系统,主要功能包括:输入航班信息(航班号、最大载重量、货物件数)、输入每件货物的名称和重量、对货物按重量从大到小排序、输出货物清单、总重量和配载状态。
(2)类图设计
本次作业遵循了题目提供的类图指引,共设计5个类:
Cargo:货物实体类,封装名称和重量;
CargoSorter:工具类,负责对链表进行选择排序;
Flight:航班类,管理航班信息和货物清单;
LoadManifest:装载清单类,计算总重量和输出列表;
Main:主控类,负责输入读取、对象构建和流程编排。
(此处请替换为您自己绘制的第一次作业类图截图,可使用PowerDesigner导入代码自动生成,或手绘UML类图)
图2-1 第一次作业类图设计
【类图制作方法】:若使用PowerDesigner,可通过File → Reverse Engineer → Object Language导入Java代码,在Object Language处选择Java,即可将代码逆向工程为类图。
从类图的关联关系来看,Flight与LoadManifest之间是关联关系——航班拥有一个舱单对象;LoadManifest与Cargo之间也是关联关系,通过链表头指针连接所有货物节点;Main类则依赖所有其他类来完成整个流程。
(3)SourceMonitor代码度量分析
(此处请替换为您自己的SourceMonitor报表截图,需包含:语句数、深度、圈复杂度等关键指标)
图2-2 第一次作业SourceMonitor报表
SourceMonitor的核心价值在于通过圈复杂度、嵌套深度等指标,从代码表面层次发现根本性问题。根据报表数据分析,关键指标如下:
指标 数据 (请替换) 解读
总代码行数 (填写实际行数) 类与方法间交互合理,调用层级简单
有效代码占比 (填写实际占比) 说明冗余代码少
最大圈复杂度 v(G) (填写实际数值) 低于10表示逻辑较清晰,但需关注高复杂度方法
最大嵌套深度 (填写实际层数) 过深嵌套会使逻辑晦涩难懂
注释率 (填写实际百分比) 多数同学的第一次作业注释率偏低,需加强
【圈复杂度解读】:圈复杂度是SourceMonitor的核心度量指标,反映了代码中独立路径的数量。高圈复杂度往往意味着更高的出错概率、更低的可测试性和更长的调试时间,是代码审查中的重要预警指标。
从数据可以看出,第一次作业整体逻辑较为简单。主要问题集中在:Main类承担了输入、构建链表、调用排序等多个职责,违背单一职责原则,导致最大圈复杂度集中在Main类的部分方法中。
2.2 第二次作业:多货舱管理与职责拆分
(1)业务需求
在第一版基础上引入多货舱管理,包括:定义多个货舱及其容量、支持按重量降序分配货物到指定货舱、分别判断货舱超载和航班整体超载、输出舱单信息。
(2)类图设计
(此处请替换为您自己绘制的第二次作业类图截图,需能清晰展示类间关系)
图2-3 第二次作业类图设计
本阶段新增了多个类,类间关系变得更为丰富:CargoCompartment组合Position(货舱内嵌位置,生命周期绑定);Flight聚合CargoCompartment(航班包含货舱,但货舱可独立存在);LoadDispatcher依赖Cargo(排序工具类与数据对象解耦);OutPut依赖Flight和CargoCompartment。
关键设计亮点在于严格遵循单一职责原则:Position只管位置编号,CargoCompartment只管货舱内货物管理,LoadDispatcher专门处理排序逻辑,InputValidator统一处理输入校验。
(3)SourceMonitor代码度量分析
(此处请替换为您自己的第二次作业SourceMonitor报表截图)
图2-4 第二次作业SourceMonitor报表
本次作业的代码度量数据发生了显著变化。关键指标的趋势演变如下(请将括号中的对比数据替换为您的实际值):
总代码行数从第一次的约(280行)增长至约(480行),增长约71%;
最大圈复杂度略有上升,但分布更加均匀(LoadDispatcher中的排序方法v(G)=5,Flight中的装载分发方法v(G)=7);
与第一次作业相比,虽然总复杂度绝对值上升,但得益于类的拆分,平均圈复杂度得以维持在较低水平。这正是单一职责原则在度量上的具体体现——复杂度分散到了不同的类中,降低了单点维护风险。
2.3 第三次作业:完整业务闭环
(1)业务需求
在第二次基础上新增旅客与行李管理,实现载重平衡核心算法,包括:旅客基础体重+行李重量计算、重心百分比的物理公式计算、安全区间校验、禁止使用工具类排序的要求。
(2)类图设计
(此处请替换为您自己绘制的第三次作业类图截图)
图2-5 第三次作业类图设计
本次作业新增了Passenger和Luggage两个核心类。与第二次相比,类间关系最大的变化在于:Passenger组合Luggage(行李与乘客的生命周期绑定),同时Flight直接管理Passenger列表。这种设计虽然符合业务逻辑,但也带来了复杂度管理上的挑战。
(3)SourceMonitor代码度量分析
(此处请替换为您自己的第三次作业SourceMonitor报表截图)
图2-6 第三次作业SourceMonitor报表
本次作业是整体代码复杂度的峰值。关键指标分析如下(请替换为您的实际数据):
指标 数据 (请替换) 变化趋势
总代码行数 (填写实际行数) 相对第二次大幅增长
最大圈复杂度 (填写实际数值) 重心计算方法(物理公式+多条件)可能超过15,需重点关注
平均嵌套深度 (填写实际层数) 多重条件判断导致嵌套增加
注释率 (填写实际百分比) 建议不低于15%
相较于前两次作业,本次最大的度量挑战来自两方面:一是重心计算方法融合了物理公式和多条件分支,圈复杂度容易超标;二是数据校验需要覆盖更多输入字段,增加了代码分支数量。
小结:复杂度演进趋势
通过SourceMonitor的三次对比,可以清晰地看到我的代码质量在不断提升。虽然绝对行数和复杂度随着业务扩展而增加,但得益于逐步强化的类拆分和职责划分,复杂度密度(即每行代码的平均复杂度)在第二次作业后开始趋稳——这说明我的设计能力正在追平业务复杂度。
三、采坑心得
写代码的路上,“坑”是避不开的。下面我把三次作业中踩过的最深的坑整理成“避坑指南”,希望能帮到后来者。
3.1 坑一:输入缓冲区残留(通用高频错误)
这个问题在第一次作业输入航班号和货物名称时就已出现。使用Scanner读取int或double后,换行符会残留在缓冲区,紧接着调用nextLine()时可能会读取到这个空字符串,导致名称读取出错。
java
错误示例
int n = scanner.nextInt(); // 读取数字后,换行符留在缓冲区
String name = scanner.nextLine(); // 换行符被读到name中!
// 正确做法
int n = scanner.nextInt();
scanner.nextLine(); // 单独吸收掉换行符
String name = scanner.nextLine(); // 此时能正确读取
【我的教训】 :第一次作业时,货物名称总是读不到正确内容,排查了很久才发现是换行符的问题。后来我在每次nextInt()之后都主动添加一行scanner.nextLine(),成功避开了这个“经典大坑”。
3.2 坑二:排序方向写反(第一次作业)
题目要求按货物重量从大到小排序,但我最开始实现时写成了从小到大,导致输出结果总是与样例不符。
【排查过程】 :我起初以为问题出在排序算法本身,于是反复修改冒泡排序的循环条件,但结果始终不对。后来仔细阅读题目才发现是方向反了。改为降序排序后,测试点才全部通过。
java
// 升序排序(题目要求降序)
if (crgos[j].getWeight() > cargos[j+1].getWeight()) { swap; }
// 降序排序
if (cargos[j].getWeight() < cargos[j+1].getWeight()) { swap; }
3.3 坑三:封装不严导致数据暴露(第二次作业)
第二次作业开始强制要求属性为private并提供getter/setter,但我在编写Cargo类时还是习惯性地使用了public权限:
java
// 错误的封装
public class Cargo {
public String name;
public double weight;
}
这种做法直接违背了面向对象封装原则,修改后如下:
java
// 正确的封装
public class Cargo {
private String name;
private double weight;
public String getName() { return name; }
public double getWeight() { return weight; }
}
【我的教训】 :在此前的基础编程中很少严格要求封装,导致对private的重要性认识不足。作业集2的评测中,系统检测到了非private属性的问题,我才意识到规范的严肃性。
3.4 坑四:精度控制失误(第三次作业)
重心计算要求保留一位小数,使用System.out.printf("%.1f", value)可以满足。但问题出在累加过程:浮点数累加会导致精度累积误差,尽管输出时只保留一位小数,但计算过程需要保证足够的精度。
java
// 直接累加可能导致精度丢失
double sumWeight = 0;
for (Cargo cargo : cargoList) {
sumWeight += cargo.getWeight(); // 浮点数累加可能产生微小误差
}
java
// 使用更高精度的中间计算(或BigDecimal)
double totalMoment = passengers.stream()
.mapToDouble(p -> p.getWeight() * p.getArm())
.sum(); // 流式计算的累加方式更精确
3.5 坑五:PTA格式错误的千层套路(三次作业通用)
PTA评测系统通过字符串匹配来判断答案是否正确,这意味着输出中多一个空格、少一个空格、大小写不一致、多一个换行、少一个换行都会导致“格式错误”(Presentation Error)或“答案错误”。
输出去尾空白:每行末尾不应有多余空格,这是最常见的扣分原因。
末尾换行:最后一行是否要有换行符?需要仔细阅读题目输出格式。
中英文符号:输出的冒号、逗号等必须是英文符号。
数值格式:要求输出1.0时不能输出1或1.00,格式必须严格匹配。
【我的教训】 :第一次作业调试通过后,满心欢喜提交,结果喜提“部分正确”——原因竟然是一个逗号用了中文全角!从那以后,我会直接复制样例输出的格式字符串,确保完全一致。
3.6 坑六:边界条件漏测导致代码脆弱
在三次作业中,我多次吃了边界测试的亏。下面是几个让我记忆犹新的边界场景:
边界场景 我的代码最初表现 正确应对
货物件数n=0 排序时空指针异常 处理前检查列表是否为null/空
货舱列表为空 查找目标货舱时越界 使用isEmpty()做前置校验
旅客列表为空 重心计算报除零错误 判断分母是否为0后再计算
货舱满载后继续装载 静默忽略而非报错 输出明确的拒绝信息
边界测试是一项关键能力。高质量的代码不应该只在正常输入下工作,还应该在空值、空集合、临界值、满容量等场景下保持稳定。
3.7 踩坑总结
问题类型 出现频次 根本原因 解决方案
输入换行符残留 对Scanner工作机制不够熟悉 养成nextLine()清缓冲的习惯
排序方向错误 未仔细审题 编码前先确认排序方向
封装性缺失 基础编程习惯遗留 严格遵循private+getter/setter
浮点精度误 对浮点数特性不了解 使用Math.round或保留足够精度
格式错误 输出格式不匹配 直接复制样例格式,输出时多加调试
边界条件遗漏 测试用例不充分 主动设计边界场景测试
四、改进建议
经过三次作业的历练,我对自己编写的代码有了更清晰的认识。如果现在让我重新设计,我会在以下几个方面进行针对性改进:
4.1 引入常量类消除魔法数字
问题现状:三次作业中,我多次在代码中硬编码了业务常量,例如重心安全范围的上限0.37和下限0.31。
java
// 当前代码中的硬编码(请替换为您的实际代码片段)
if (centerOfGravity > 0.37) {
System.out.println("重心超出范围");
}
if (centerOfGravity < 0.31) {
System.out.println("重心超出范围");
}
改进方案:定义常量类集中管理业务阈值。
java
// 改进建议
public class FlightConstants {
public static final double CG_LOWER_LIMIT = 0.31;
public static final double CG_UPPER_LIMIT = 0.37;
public static final double MAX_TAKEOFF_WEIGHT = 1200.0;
// 未来业务变化时,只需修改一处
}
改进理由:魔法数字散落在代码各处,一旦业务规则变化需要修改N个地方,不仅效率低下,还容易遗漏出错。使用常量类既提高了可维护性,又增强了可读性——看到CG_LOWER_LIMIT就知道业务含义,不需要再查阅文档。
4.2 配置化管理输入输出格式
问题现状:输出字符串的格式硬编码在各个输出方法中,当排版格式需要调整时,改动量很大。
改进方案:可以将输出模板抽象为一个配置类,使用String.format()动态生成。这在实际项目开发中是常见做法,可以有效降低输出模块与业务逻辑的耦合。
4.3 加强单元测试与边界测试
问题现状:我的测试策略还停留在“提交到OJ,根据测试点反馈修正”的阶段,属于典型的“后测试”模式。这种方式存在两个明显问题:一是效率低,多次提交等待评测浪费时间;二是心态容易受影响,看着测试点一个接一个不过难免焦躁。
改进方案:在本地构建充分的测试用例后再提交。
java
// 改进建议:提前构建测试用例
// 正常场景
输入:"CA1201 1200.0 3\nA 100.0\nB 200.0\nC 300.0"
预期输出:(自行对照题目要求)
// 边界场景
输入:"CA1201 1200.0 0" // 货物数为0
预期输出:(应有的输出格式)
// 异常场景
输入:"CA1201 -100.0 3..." // 负重量
预期输出:(系统应输出错误提示或按规则处理)
具体的测试方法论包括:边界值分析(测试最大值、最小值、零值和负数)、空值测试(确保空列表不导致异常)、以及满容量测试(验证超载提示是否正确)。
4.4 持续重构:圈复杂度超标函数拆分
SourceMonitor报表可以精准定位出圈复杂度较高的方法。以第三次作业为例,重心计算方法是复杂度最高的部分。
改进方案:将重心计算方法拆分为职责更单一的小方法:
java
// 改进建议:拆分高复杂度方法
public class CGBalanceCalculator {
// 计算总力矩
private double calcTotalMoment(List
// 计算总重量
private double calcTotalWeight(List
// 计算重心百分比
private double calcCGPercentage(double totalMoment, double totalWeight) { ... }
// 校验重心范围
private boolean isWithinSafeRange(double cg) { ... }
}
重构收益:拆分后的每个方法都聚焦于单一计算任务,不仅圈复杂度显著降低,而且单元测试可以精准覆盖每个计算单元。正如重构的原则所言:“重构不只可以改善既有代码设计,还可以改变组织代码的思路,使程序在设计之初就趋于合理化”。
五、总 结
5.1 本阶段的主要收获
(1)从“面向过程”到“面向对象”的思维转变
三次作业让我对面向对象设计原则有了切身体会。从第一次作业把所有逻辑塞进Main类,到第三次作业能够根据业务实体自然拆分出Flight、CargoCompartment、Cargo、Passenger、Luggage、Position等独立类,并清晰定义它们之间的组合/聚合/依赖关系,思维方式发生了质的转变。
(2)工具赋能,让度量数据成为设计的“第三只眼”
SourceMonitor帮助我打开了一扇新的窗户——让代码质量变成可量化的数据。之前判断代码“好”还是“不好”全凭感觉,现在有了圈复杂度、注释率、嵌套深度等指标作为客观依据。在第三次作业中,当发现重心计算方法的圈复杂度超过15时,我明白了为什么这部分代码总是出错——不是因为能力不够,而是结构本身就不合理。
PowerDesigner的逆向工程能力同样让我受益匪浅:代码写完后自动生成类图,能直观地看到类间关系是否符合最初的设计意图。
(3)问题调试能力的显著提升
从最初的“面对错误不知所措”,到能够根据SourceMonitor指标推断潜在问题区域,再到利用本地测试用例快速定位bug,我的问题排查效率大幅提升。PTA的错误提示(“答案错误”、“格式错误”、“部分正确”)也逐渐从“抽象提示”变成了“有意义的线索”。
5.2 发现的短板与后续研究方向
短板领域 具体表现 学习规划
设计模式 类间关系的设计主要靠直觉,缺乏系统的模式指导 深入学习SOLID原则的具体实践
异常处理 输入校验不够完善,对try-catch的使用较为被动 系统学习Java异常处理机制
单元测试 尚未建立规范的测试习惯 学习JUnit框架和TDD开发方法
代码重构 对“何时重构”的判断力不足 阅读《重构:改善既有代码的设计》
5.3 对课程的建议
客观而言,本次作业设计总体合理,难度梯度和业务场景设置符合教学目标。以下为我个人的几点建议,供参考:
提前演示SourceMonitor和PowerDesigner的使用:很多同学是做完作业才知道需要生成报表,临时摸索效率较低。建议在作业布置之初安排一次课堂演示,展示工具的安装、配置和常见用法。
测试用例的“精”而非“多” :PTA后台测试点数量众多,部分同学反应难以定位具体是哪个边界条件触发了错误。如果可以在提交失败时提供更精确的定位信息(如“第X个测试点预期输出XX,实际输出XX”),将极大地提升调试效率。
保持迭代场景的连续性:航空器配载管理这个场景贯穿三次作业,对于理解需求演进过程非常有帮助。如果可能,后续课程可以引入更多类似的贯穿式案例。
5.4 我的成长曲线
回顾这三周的学习历程,我发现最大的变化不是代码写得有多漂亮,而是:
心态上的变化:从害怕看到“测试点未通过”的红字,到能够平静地分析失败原因;
视野上的拓展:代码不仅是用来跑对的,还要让人看得懂、能维护;
工具链的建立:代码编辑器→SourceMonitor→PowerDesigner→PTA评测,形成了一个完整的开发与反馈闭环。
从首次面对类图的茫然,到能从容设计多类协作系统;从只会盲试错误,到学会用度量数据指导重构——这个从“写对”到“写好”的转变过程,比任何一次满分都更有价值。
浙公网安备 33010602011771号