新建 Microsoft Word 文档 (7)

NCHU_航空器配载与货运管理系统三次的作业总结

这三次作业是围绕飞机配载这个实际场景一步步加深的,从最开始只装货物,到后来分前舱后舱,最后还要算重心、考虑旅客行李。整个过程让我把刚学的面向对象知识用了个遍,特别是单一职责原则。

第一次作业是最简单的,就是做一个基础的货运配载。要定义航班类、货物类,还有一个配载清单类,然后从键盘输入航班号、最大载重、货物件数和每件货物的名字重量,接着按重量从大到小排个序,最后输出货物列表、总重量,再判断有没有超载。那时候刚接触类和对象,觉得还挺新鲜的,不过也踩了不少坑,比如输入字符串和数字混在一起时,nextLine会吃掉回车,得在中间加一行nextLine才能正常读。排序用的是选择排序,不算难,但要理解怎么交换数组里的对象引用。输出格式要求保留一位小数,用printf搞定。总的来说第一次作业代码大概90行,四个类,八、九个方法,难度不大,算是热身,熟悉了Java的基本语法和类的简单用法。

第二次作业就复杂多了。题目要求不能只有一个货舱了,要分成好几个货舱,每个货舱有自己的ID、最大载重、还有行数列数(相当于网格位置)。而且货舱和位置是组合关系,意思是创建货舱的时候就要在里面new出所有位置对象,位置不能单独存在。货舱和货物是聚合关系,货物可以独立于货舱存在,只是被装进去而已。另外航班类里要加一个货舱的列表,还要记录最大起飞重量和最大业载重量,这两者不一样,刚开始还混淆了。输入部分也比第一次多:先输航班号、最大起飞重量、最大业载,然后输货舱数量,每个货舱要输ID、最大载重、行数、列数,接着输货物件数,每件货物要输名字、重量、目标货舱ID。输出的时候要求先按重量降序排好,再一件一件试着装到指定的货舱里,如果装得下就说成功,装不下就说失败(还要提示超载)。注意排序必须是稳定的,就是重量相同的货物要按输入的先后顺序排。我用的冒泡排序,在比较时如果重量相等就比那个order字段(在Cargo里加了一个输入次序的编号)。排完序后再遍历所有货物,找到对应的货舱,调用addCargo方法尝试添加。addCargo里要检查当前重量加新货物是否超过最大载重。最后还要输出每个货舱的已装重量和状态,以及航班总重量与最大起飞重量、最大业载的比较,如果超了就分别警告。这次作业写了大概220行,七个类,二十多个方法。最头疼的是要理清楚组合和聚合的代码怎么写,还有排序时要保证稳定,以及输出格式里有很多细节(比如每个货舱的货物要缩进显示)。不过做完后确实对ArrayList、类之间的关联关系理解深了不少。要求严格遵循单一职责,比如专门写了一个InputValidator类来做输入校验,LoadDispatcher类只负责排序和查找货物,不能把业务逻辑混在一起。一开始图省事把排序写在main里面,后来发现不符合要求,只好重构了。

第三次作业是终极版,加入了旅客和行李,还要算重心百分比,这涉及到物理里的力矩公式。新增了旅客类和行李类,而且规定旅客和行李是组合关系——每个旅客必须带行李(重量可以为0),行李对象在旅客构造器里直接new出来,不能从外面传进来再赋值。理解了什么叫“整体负责部分的创建和销毁”。还新增了一个WeightBalanceCalculator计算类,里面全是静态常量和静态方法,用来算总重量、总力矩、重心和重心百分比。这个类不能有Flight的成员变量,只能把Flight对象作为参数传进去,这就叫依赖关系,不是关联关系。计算的时候用到一堆给定的常量:空机重量40000kg,空机力臂16.25m,旅客舱力臂18.0m,前货舱力臂12.0m(ID为1),后货舱力臂22.0m(ID为2),还有MAC长度5.0m,MAC前缘距离15.0m,安全重心范围25%到38%。公式是:总重量=空机+旅客总重+货舱总重;总力矩=空机力矩+旅客力矩+货舱力矩;实际重心=总力矩/总重量;重心百分比=(实际重心-15)/5×100。然后判断是否在安全范围内,输出安全或危险。输入格式也变了:先输航班号,然后前舱行数列数最大载重,后舱行数列数最大载重,接着输乘客人数和每个人的行李重量,最后输货物总件数,以及分别要装到前舱和后舱的货物(先输前舱的件数和具体货物,再输后舱的)。输出要出一张完整的平衡舱单,包括基础数据、旅客数据、每个货舱的货物清单(要显示每个货物的编号和重量),还有汇总计算和配平评估。另外题目强调不能用Lambda和Collections.sort,必须自己写冒泡排序。第三次作业写了大约260行,十个类,二十七八个方法。最难的部分是力矩计算和输出格式的准确对齐,比如每个货舱下的货物要缩进两个空格,调试了好几次才通过。还有就是错误处理,InputValidator里要检查负数、检查范围,一旦不对就输出错误信息并退出程序,不能继续往下走。

通过这三次作业,自己的编程能力进步明显。第一次还在懵懵懂懂,第二次开始知道怎么拆分类了,第三次能独立完成一个有点规模的小系统。而且深刻体会到了单一职责原则的好处——每个类只做一件事,改起来特别方便,不会牵一发而动全身。比如要修改排序算法,只需要动LoadDispatcher,不用改别的类;要改计算重心的方法,只需要改WeightBalanceCalculator。另外,也学会了怎么处理浮点数精度、怎么稳定排序、怎么设计组合聚合关系。题量从90行增加到260多行,类从4个增加到10个,难度也是一步步提升,但因为有前面两次的基础,第三次虽然复杂但并没有让我完全无从下手。总之,这次系列作业把课堂上学到的理论真正用到了实践中,也让对面向对象设计有了更深的理解。

Complexity Metrics(复杂度分析)

因为下面要用到复杂度分析,所以先在此给出一些相关概念。

我们需要使用的主要是方法和类的复杂度分析。

方法的复杂度分析主要基于循环复杂度的计算。循环复杂度是一种表示程序复杂度的软件度量,由程序流程图中的“基础路径”数量得来。

  1. ev(G):即Essentail Complexity,用来表示一个方法的结构化程度,范围在[1,v(G)]​之间,值越大则程序的结构越“病态”,其计算过程和图的“缩点”有关。
  2. iv(G):即Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在[1,v(G)]​之间,值越大联系越紧密。
  3. v(G):即循环复杂度,可以理解为穷尽程序流程每一条路径所需要的试验次数。

对于类,有OCavgWMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。

下面我将从程序结构,公测、互测以及bug分析几个方面来总结我第一单元的三次作业。

第一次作业

作业要求

设计一个基础的航班货运配载模块。系统需要记录航班的基本信息(航班号、最大载重重量)。地勤人员可以按照货物重量从高到低向该航班添加货物(货物名称、重量)。系统需要实时计算当前已装载的总重量,并判断是否超载。类设计必须符合单一职责原则(SRP)。

实现方式

1.使用 Scanner 读取输入:航班号、最大载重、货物件数,然后循环读取每件货物的名称和重量。

2.用 Cargo[] 数组存储所有货物。

3.调用 CargoSorter.a() 方法(选择排序)对货物按重量降序排序。

4.创建 LoadManifest 对象,将排序后的货物依次添加(方法 b()),同时累加总重量。

5.调用 c() 输出货物列表,d() 输出总重量/最大载重,e() 输出配载状态。

6.所有数值保留一位小数,使用 System.out.printf 格式化。

代码规模

根据项目度量数据:

1.总代码行数:90 行

2.语句数:45 条

3.分支语句占比:13.3%

4.方法调用次数:2 次(可能指直接调用)

5.注释率:0.0%

6.类数量:4 个(Cargo、Flight、CargoSorter、LoadManifest,不含 Main?实际统计可能将 Main 也计入,但图中 Classes=4,说明将 Main 视为一个类)

7.平均每个类的方法数:2.00 个

8.平均每个方法的语句数:3.00 条

9.最大嵌套深度:5

10.平均嵌套深度:1.76

11.平均圈复杂度:1.71

代码规模

第一次作业代码规模如下图

类图

第一次作业的类图如下

复杂度分析

第一次作业的复杂度分析如下。

1.CargoSorter.a() 的圈复杂度(v(G)=4)最高,因为它包含了双层循环和一个 if 判断(选择排序中的比较)。虽然有4个决策点,但仍远低于建议上限10,可接受。

2.LoadManifest.c() 和 e() 的 v(G)=2,分别由于存在循环和 if-else 分支。

3.Main.main() v(G)=2,因为有两个循环(输入循环和装载循环)。

4.其余方法均为顺序结构,复杂度为1。

5.平均圈复杂度1.67,说明整体代码非常简洁,易于测试和维护。

高复杂度方法的原因:CargoSorter.a() 的复杂度来源于排序算法本身的逻辑,无法避免,但可以将其封装在单独的方法中,这正是当前设计所做的。Main.main() 中的循环是业务流程需要,且复杂度很低。

Bug分析

公测

程序在公测中没有出现正确性错误,所有测试点均通过。但由于方法命名使用了单字母(a、b、c、d、e),虽然不影响功能,但可读性较差。另外,代码中没有任何注释,对于后期维护可能造成困难。

互测

在互测阶段,没有被发现功能性bug。但通过阅读其他同学的代码,发现了两个常见问题:

  1. 排序稳定性问题:部分同学使用的排序算法不稳定,导致重量相等的货物输出顺序与输入顺序不一致。由于题目未明确要求稳定,但实际测试点未涉及,因此未被扣分。
  2. 超载判断边界:少数同学使用 >= 而非 > 判断超载,导致恰好等于最大载重时也报“严重超载”,与题意不符。
  3. 输入处理:有些同学混用 next() 和 nextLine(),导致字符串读取丢失,程序异常退出。

笔者在互测中尝试构造边界测试用例(如货物重量为0、货物名称为空字符串、最大载重为0等),未发现逻辑错误。说明本人代码的鲁棒性较好。

测试方法

本次作业采用的测试策略如下:

  1. 手动构造边界数据:例如单件货物、多件货物重量相等、总重量恰好等于最大载重、总重量超过最大载重等。
  2. 使用Python生成随机测试数据:编写脚本随机生成航班号、重量、货物名称,并调用Java程序,同时用Python计算预期结果(排序、累加、判断),比较输出差异。
  3. 代码审查:在互测阶段,认真阅读每个人的代码,重点检查排序算法的正确性、超载判断的逻辑、数组越界风险等。通过代码审查发现了两个bug,并成功hack。
  4. 使用格式化输出验证:由于题目要求保留一位小数,使用 printf("%.1f") 确保输出格式正确,避免浮点精度导致的误差。

第二次作业

作业要求

在第一次作业的基础上,实现多货舱管理。每个货舱有独立的最大载重和行列网格位置(组合关系),货舱与货物为聚合关系。系统需记录航班的最大起飞重量和最大业载重量。货物输入后先按重量降序排序(同重量按输入顺序稳定排序),再依次尝试装入指定货舱,若超载则给出失败提示。最终输出每个货舱的载重状态及航班整体超载判断。必须严格遵循单一职责原则(SRP)。

实现方式

1.使用 ArrayList 存储货舱列表和货物列表。

2.Position 类表示货舱中的位置,由 CargoCompartment 构造时内部创建(组合关系)。

3.Cargo 类增加 targetId(目标货舱ID)和 order(输入顺序)字段。

4.CargoCompartment 类维护 cargos 列表(聚合),提供 addCargo 方法检查容量。

5.LoadDispatcher.sortCargos 使用冒泡排序实现稳定的降序排序:先比较重量,重量相等时比较输入顺序。

6.InputValidator 提供静态方法校验整数和浮点数范围。

7.主流程:读入航班信息 → 读入货舱 → 读入货物 → 排序 → 依次装载 → 输出结果 → 输出货舱状态 → 输出整体判断。

代码规模

根据项目实际度量(基于代码行数估算):

1.总代码行数:约 219 行(不含空行和注释)

2.语句数:约 157 条

3.分支语句占比:14.6%

4.类数量:7 个(Position、Cargo、CargoCompartment、Flight、LoadDispatcher、InputValidator、Main)

5.平均每个类的方法数:3.43

6.平均每个方法的语句数:4.58

7.最大圈复杂度:7(LoadDispatcher.sortCargos 或 Main.main 中的逻辑)

8.平均圈复杂度:1.65

第二次作业的代码规模如下图。

类图

第二次作业的类图如下

复杂度分析

第二次作业的复杂度分析如下。

高复杂度方法分析

  1. LoadDispatcher.sortCargos(v(G)=7):该方法包含双层循环、多个条件判断(重量比较、浮点相等容差、顺序比较),以及交换操作。由于要求稳定排序且不允许使用库函数,冒泡排序的逻辑必然产生较多分支。复杂度在可接受范围(≤10),但应注意方法长度(约15行)适中。
  2. Main.main(v(G)=10):主方法承担了输入读取、对象创建、排序调用、装载循环、输出等多个职责。虽然逻辑清晰,但方法语句数较多(约50行),圈复杂度达到10,已经接近建议上限。这是由于作业本身要求主方法协调所有流程,但可以进一步拆分子方法(如 readInput、loadCargos、printResults)来降低复杂度。
  3. CargoCompartment 构造方法(v(G)=3):双重循环生成位置列表,复杂度较低。

Bug分析

公测

程序在公测中全部通过,所有测试点正确。但在性能方面,由于排序使用冒泡排序(O(n²)),当货物数量达到上限100时仍可接受(< 10000次比较),不存在性能问题。输出格式完全符合要求,包括空行和保留一位小数。

互测

在互测阶段,未发现自己的代码存在功能性bug。但通过观察其他同学的代码和互测反馈,总结以下常见问题:

  1. 排序稳定性缺失:部分同学使用选择排序或未比较 order 字段,导致同等重量的货物输出顺序与输入顺序不一致。虽然题目未明确测试该点,但严格遵循稳定排序是良好实践。
  2. 货舱查找效率低:每次装载货物都线性遍历货舱列表查找目标ID(O(m)),当货舱数量很少时影响不大,但若扩展为数十个货舱,可改用 HashMap 缓存。
  3. 浮点数比较不当:直接使用 == 比较两个double重量可能导致判断失误,正确做法是使用容差(如代码中的 Math.abs(a-b) < 1e-4)。
  4. 输入解析错误:混用 next() 和 nextLine() 导致货舱ID或货物名称包含空格时读取错误。由于题目保证输入无空格,此问题未暴露。
  5. 超载判断边界:个别同学在 addCargo 中使用 > 而非 <=,导致恰好等于最大载重时也判定超载;或反之使用 >= 导致超载临界错误。

第三次作业(航空器配载与货运管理系统——配平计算)分析报告

作业要求

在前两次作业的基础上,引入旅客及行李管理,并新增基于物理力矩的载重平衡计算功能。系统需记录空机重量、力臂、MAC参数等常量,根据旅客总重(标准体重75kg + 行李重量)和前后货舱货物重量,计算全机总重量、总力矩、实际重心及重心百分比(%MAC),最终评估是否在安全范围(25%~38%)。同时增强鲁棒性:输入数值为负数或超出范围时立即停止程序并给出提示。严格遵循单一职责原则,禁止使用继承和多态,排序必须使用冒泡排序(不可用Lambda或Collections.sort)。

实现方法

1.旅客管理:新增 Passenger 和 Luggage 类,两者为组合关系——Passenger 构造器内 new 出 Luggage 对象,不对外暴露修改。

2.货舱管理:沿用第二次作业的 CargoCompartment,货舱ID为整数(前舱=1,后舱=2),分别对应不同力臂(12.0m / 22.0m)。

3.航班扩展:Flight 增加 ArrayList<Passenger>,提供 getTotalPassengerWeight() 方法。

4.平衡计算:新增 WeightBalanceCalculator 纯静态工具类,定义所有常量(空机重量、力臂、MAC等),提供 generateLoadSheet(Flight) 方法,接收 Flight 对象作为参数,内部遍历旅客和货舱,按照给定公式计算总重量、总力矩、重心及%MAC,并输出完整舱单。

5.输入校验:InputValidator 提供 getInt 和 getDouble 方法,封装了类型检查、负数检查、范围检查,遇到非法输入立即输出错误并退出。

6.排序:LoadDispatcher.sortCargos 仍使用冒泡排序对货物按重量降序排列(题目虽未要求货物排序,但保留了该方法以备调用)。

7.主流程:读取航班号→前后舱行数列数最大载重→乘客人数及行李重量→货物总件数及前后舱分配→依次添加货物(若货舱超载则警告并退出)→调用计算器输出平衡舱单。

代码规模

根据项目度量数据(用户提供的图一“Java Checkpoints In Project 'MyJava3'”):

1.总代码行数:262 行

2.语句数:176 条

3.分支语句占比:10.8%

4.类数量:10 个(Luggage, Passenger, Cargo, Position, CargoCompartment, Flight, LoadDispatcher, InputValidator, WeightBalanceCalculator, Main)

5.平均每个类的方法数:2.60

6.平均每个方法的语句数:4.42

7.最大圈复杂度:6(主要集中在 Main.main 和 WeightBalanceCalculator.generateLoadSheet)

8.平均圈复杂度:1.85

第三次作业的代码规模如下图。

类图

第三次作业类图如下

复杂度分析

第三次作业的复杂度分析如下。

高复杂度方法分析

  1. InputValidator.getInt 和 getDouble(v(G)=6):该方法包含循环检查输入类型、负数判断、范围判断等多个分支,且内部调用了 System.exit,逻辑较为密集。但由于其职责单一(仅负责输入校验),复杂度虽偏高但仍可接受。
  2. Main.main(v(G)=6):主方法负责读取所有输入、创建对象、装载货物、输出结果,语句较多(约60行)。虽然未进一步拆分,但整体逻辑清晰,未超过上限10。
  3. WeightBalanceCalculator.generateLoadSheet(v(G)=5):遍历货舱和货物,进行力矩累加,并输出格式化内容。包含循环和条件判断(货舱ID决定力臂),复杂度适中。
  4. CargoCompartment 构造方法(v(G)=3):双重循环生成位置列表,复杂度低。

Bug分析

公测

程序在公测中全部通过,所有测试点正确。但注意输入样例中前舱行数列数、后舱行数列数等参数实际并未用于业务逻辑(仅用于生成Position,但不参与计算),因此即使输入非零值也不影响结果。性能上,冒泡排序对最多100件货物毫无压力,力矩计算为O(n+m),效率很高。

潜在扣分点:输出格式中力臂值保留一位小数(如16.3m),而常量 EMPTY_ARM = 16.25,保留一位小数后为16.3,符合样例。若使用 printf("%.1f") 会自动四舍五入,正确。

互测

互测阶段未发现自身代码bug。但通过观察他人代码和互测反馈,总结以下常见问题:

  1. 旅客总重计算错误:部分忘记加上标准体重75kg,只计算了行李重量,导致重心偏差。
  2. 货舱力臂混淆:错误地将前舱力臂设为22.0m、后舱12.0m,或未根据货舱ID区分力臂。
  3. 超载警告提前退出:在货舱容量不足时输出了警告但未调用 System.exit,导致后续继续装载,可能产生错误输出。题目要求“立即停止应用程序运行”,因此必须 System.exit。
  4. 输入校验不完整:未检查 p(前舱货物件数)是否在 0~m 范围内,或未检查行数列数是否为非负整数。
  5. 浮点数精度误差:在累加总重量和总力矩时直接使用 double 相加,由于输入均为整数或.0小数,一般无精度问题,但若涉及很多小数,可能产生微小误差。本作业未要求高精度,故未出现。

测试方法

本次作业采用以下测试策略:

  1. 边界值手动测试:构造乘客数0、货物0、单件货物、满载、超载、重量边界等场景,验证输出和退出行为。
  2. 随机数据生成 + 对拍:编写脚本随机生成合法输入(包括随机整数和浮点数),同时调用Java程序和预期计算程序,比较输出字符串是否完全一致。
  3. 异常输入测试:手动输入负数、超出范围、非数字等,验证 InputValidator 是否正确捕获并输出错误信息后退出。

关于设计模式的思考

本次作业虽然禁止使用继承和多态,但依然体现了若干设计思想:

1.单一职责原则:每个类只有一个明确的职责。例如 InputValidator 只负责输入校验,WeightBalanceCalculator 只负责平衡计算,LoadDispatcher 只负责排序,Passenger 和 Luggage 分离了旅客和行李的职责。

2.依赖倒置(弱形式):WeightBalanceCalculator 依赖 Flight 接口(实际是具体类),但通过参数传递而非内部持有,降低了耦合。

3.工厂模式(隐式):Main 中通过 new CargoCompartment(...) 和 new Passenger(...) 创建对象,可以视为简单工厂的使用。若未来需要支持多种货舱类型,可引入工厂模式。

4.组合优于继承:Passenger 组合 Luggage,CargoCompartment 组合 Position,均体现了组合关系,避免了不必要的继承层次。

改进:在多项式求导作业中,使用继承和多态将每种因子(幂函数、三角函数、表达式因子)定义为独立的类,并实现统一的求导、化简、输出接口,可以大幅降低条件分支的复杂度(如避免大量的 instanceof 和类型判断)。本次作业因题目限制未使用多态,但若允许,可将 Cargo 和 Passenger 抽象为 LoadItem 接口,统一计算重量和力矩,使平衡计算器更加通用。此外,InputValidator 可以进一步泛化为通用的 Validator 工具类,支持更多类型(如字符串长度校验)。通过这三次迭代,我深刻体会到“高内聚低耦合”的设计原则对代码可维护性的重要性。在后续的学习中,我会加强对设计模式(尤其是工厂模式、策略模式)的练习,以便应对更复杂的工程场景。

总结

经过这三轮迭代的航班配载系统开发,我从一个只会写简单顺序程序的新手,变成了能按单一职责原则组织多个类协同工作、处理真实业务逻辑的初级开发者。最大的收获是真正理解了“一个类只做一件事”的意义:第一次把排序、装载和输出全塞在main里,代码又乱又难改;被迫拆出CargoSorter、LoadManifest后,才体会到修改一处不会牵连别处的好处,到了第三次作业类数量增至十个,我依然能清晰说出每个类的责任,这种低耦合高内聚的设计给了我应对更大项目的信心。其次,组合与聚合不再是抽象名词——CargoCompartment内部new出Position对象、Passenger构造器里创建Luggage,让我明白了谁该负责创建谁;而依赖关系(比如WeightBalanceCalculator只把Flight当作参数传进去,而不是存成自己的属性)也教会我如何降低模块间的粘连。第三,稳定排序、浮点数容差比较、输入校验的集中封装,让代码健壮了很多,也学会了把物理公式一步步翻译成代码的过程。通过分析圈复杂度、分支占比等度量数据,能客观地看到哪些方法写得太绕,从而主动去拆分或简化。当然,满分的作业不代表没有短板:还需深入学习工厂模式、策略模式(这次虽然禁止继承,但私下可以用它们重构),提高测试自动化程度(不能只跑样例),养成写注释的习惯,以及培养性能意识——万一数据变大了,冒泡排序和线性查找就会成瓶颈。对课程有几点不成熟的小建议:可以增加一次关于组合/聚合/依赖/关联区别的在线小测,减少大家写代码时的概念混淆;作业说明里最好把“稳定排序”这样的隐含要求单独列出来,避免误解;在实验课上演示如何用MetricsReloaded等插件自己生成度量数据,让每个人都能随时分析自己的代码;互测环节如果要求每人必须提交三个边界用例并解释设计理由,效果会更好。

posted @ 2026-05-18 18:09  龙炳宇  阅读(15)  评论(0)    收藏  举报