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

前言

面向对象程序设计课程开学以来,一共完成了三次 PTA 作业。这三次题目并不是彼此独立的,而是围绕“航空器配载与货运管理系统”这个主题进行一个迭代。第一次只需要处理最基础的航班装货,第二次开始细化到前舱、后舱和具体装载位置,第三次则把旅客、行李和载重平衡也纳入了系统。这三步其实可以看为:先把骨架搭起来,再往里面填业务规则,最后补上真正决定系统专业性的计算部分。

从代码量上看,三次作业的体量也是逐步增加的。第一次大约 100-200 行,第二次提升到 200-300 行,第三次已经接近 300-400 行。刚开始写第一题时,我还是比较习惯“先把功能做出来再说”的思路,很多逻辑都想往一个类里塞;到了后面,随着类越来越多、关系越来越复杂,如果还按这种写法硬撑,代码会很快变得难改、难查、难测。所以这三次作业对我来说,不只是把题做完,更像是把“面向对象应该怎么写”这件事真正走了一遍。

作业 主要内容 代码规模 设计重点
第一次 单航班、单一装货逻辑、超载判断 100-200 行 单一职责原则
第二次 多货舱、位置网格、按目标货舱分配 200-300 行 组合与聚合关系
第三次 旅客、行李、载重平衡与重心计算 300-400 行 依赖关系与工具类设计

设计与分析

第一次作业

【本次作业需求说明】
设计一个基础的航班货运配载模块。系统需要记录航班的基本信息(航班号、最大起飞重量、最大业载重量)。地勤人员可以按照货物重量从高到低向该航班添加货物(货物名称、重量)。系统需要实时计算当前已装载的总重量,并判断是否超载。

1. 代码规模分析

第一次作业的规模相对较小,类的数量也不多,主要围绕 Flight类Cargo类CargoSorter类LoadManifest类 展开。虽然代码行数不高,但它其实已经把后面两次作业最核心的几个问题提前抛出来了:数据应该放在哪个类里、计算应该放在哪个类里、排序和统计应不应该混在一起。

第一次代码规模如下图所示:
image

从 SourceMonitor 的统计结果来看,这一版代码整体还比较紧凑,方法长度普遍不长,说明当时的功能点确实比较集中,没有出现特别臃肿的模块。

2. 类图分析

类图如下:
image

这一版的设计重点,其实就是单一职责原则有没有落到实处。

  • Flight类 负责保存航班的基础信息,以及当前航班关联的货物数据;
  • Cargo类 只表示货物本身,记录名称和重量;
  • CargoSorter类 负责排序和装载顺序控制;
  • LoadManifest类 负责统计已装载总重量,并判断是否超载。

现在回头看,这种拆分类的方式虽然简单,但意义很大。因为如果把排序、统计、判断超载这些逻辑都塞进 Flight类,那 Flight类 很快就会变成一个什么都管的大类。第一次作业刚好让我意识到,类不是越少越好,关键是职责边界要清楚。

3. 复杂度分析

复杂度分析如下:
image

从复杂度图上可以看出,第一题的复杂度主要集中在装载和统计相关的方法上,这也很正常,因为分支判断基本都在“能不能装”“装完是否超载”这两件事上。整体来看,这一版的复杂度并不高,调试时也主要是边界判断问题,比如:

  • 货物按重量排序后是否真的按从大到小输出;
  • 当前重量是否实时更新;
  • 当总重量刚好等于阈值时,系统应该判定为可装载还是超载。

第一次作业给我的最大感受是:题目不难,真正难的是从一开始就把结构搭对。因为这一步如果偷懒,后面只会越补越乱。


第二次作业

【本次作业需求说明】
设计一个扩展的航班货运配载模块,实现以下功能:
航班信息管理:记录航班号、最大起飞重量、最大业载重量。
货舱管理:每个货舱有唯一标识、最大载重、行数、列数(组成位置网格)。
货舱与其位置是组合关系,货舱与装载货物是聚合关系。
货物装载时,系统先按重量从高到低排序,再依次尝试将货物装入指定货舱。
最终输出每件货物的装载结果、每个货舱的装载情况以及航班整体是否超载。

1. 代码规模分析

第二次代码规模如下图所示:
image

和第一次相比,第二次的代码量增长得比较明显。原因不只是类变多了,更关键的是系统开始真正地处理对象关系了。第一次只需要考虑货物能不能往飞机上放,第二次则需要考虑货物应该放进哪个货舱、货舱内部位置怎么组织、某个货舱超载了应该怎么办。也就是说,这时候程序已经不再是单纯的线性流程,而是开始有明显的结构层次。

2. 类图分析

类图如下:
image

这一次我觉得最值得分析的是类与类之间的关系,而不是只单看某一个类本身。

  1. CargoCompartment类Position类 是组合关系。
    位置对象不是独立存在的,它依附于货舱。货舱创建时内部生成位置列表,货舱没了,这些位置也没有单独存在的意义。

  2. CargoCompartment类Cargo类 是聚合关系。
    货物并不是为某个货舱“量身定做”的,它本身可以先存在,再被分配到某个舱位中,所以这里更适合用聚合理解。

  3. LoadDispatcher类 负责调度。
    这个类的作用很像系统里的“中间人”:先排序,再按目标货舱逐个尝试分配。这样写的好处是,货舱只关心自己能不能装,航班只关心整体信息,调度过程单独封装,逻辑不会缠在一起。

  4. InputValidator类 负责输入校验。
    这个类虽然不显眼,但对主流程帮助很大。把异常处理和输入转换单独抽出来之后,主函数读起来会清楚很多。

第二次作业让我对“组合”和“聚合”的区别有了更具体的理解。以前看概念时总觉得很抽象,真正把类图画出来,再对应代码去写,才会发现它们并不是为了考试而分出来的名词,而是真的会影响对象创建方式和职责划分。

3. 复杂度分析

复杂度分析如下:
image

这一版复杂度上升最明显的地方,主要是装载分发逻辑。因为系统要连续处理多种判断:

  • 目标货舱是否存在;
  • 当前货舱剩余载重是否足够;
  • 位置是否还有空位;
  • 装载失败后该如何输出结果;
  • 最后如何统计单舱和整机状态。

如果把这些判断全部堆在主函数里,代码会非常难看,所以第二次作业实际上是在考我我如何让类各自干各自的事。

我在这次作业里做自测时,重点测了下面几类数据:

测试点 预期结果
目标货舱不存在 装载失败,并给出对应提示
货物重量刚好等于货舱剩余载重 装载成功
货舱位置已满但重量仍未超限 仍应装载失败
航班总业载未超,但某一货舱超限 单舱报错,整机状态单独统计

这些测试让我发现一个问题:写代码时最容易漏掉的,不是正常情况,而是看起来不常见的边界情况。


第三次作业

【设计要求】
本次迭代新增约 4 个类,系统总类数达到 10 个左右。严禁使用继承和多态,请严格遵循关系设计与单一职责设计。新增 Passenger类Luggage类WeightBalanceCalculator类InputValidator类 等类,原有 Flight类 需增加 List<Passenger类> 属性。本次生成报告时需内部调用 LoadDispatcher类,且排序算法不允许使用 lambda 表达式及 Collections.sort(),必须使用循环完成排序过程(冒泡排序)。

1. 代码规模分析

第三次代码规模如下图所示:
image

第三次作业的代码量增长是最明显的,但它的难点其实不在“多写几个类”,而在于要把前两次已经做出的结构继续保持住,不要因为功能变复杂就把设计写崩。到了这一阶段,系统已经不只是货运系统,而是一个同时处理货物、旅客、行李和配平计算的综合模型。

2. 类图分析

类图如下:
image

这次最关键的几个类关系如下:

  • Passenger类Luggage类 是组合关系。
    行李对象在旅客构造时直接创建,旅客离开系统,行李也没有单独存在的必要。这种设计既符合题目要求,也能避免外部随意修改行李对象。

  • WeightBalanceCalculator类Flight类 是依赖关系。
    计算器不保存 Flight类 的成员变量,而是在方法参数中接收航班对象。这一点看似只是写法不同,实际上是在控制耦合度。计算类只负责“算”,不负责“存”。

  • Flight类Passenger类 是关联关系。
    航班需要维护旅客列表,因为最终的总重量和重心计算都要把旅客及其行李算进去。

3. 复杂度分析

复杂度分析如下:
image

第三题的复杂度主要集中在两个位置:

  1. 数据汇总变多了。
    现在不仅要统计货物重量,还要把旅客标准体重、行李重量、货舱装载情况全部纳入总重量和总力矩计算。

  2. 业务规则更专业了。
    前两次更多是在做“管理”,第三次则开始碰真正的航空业务概念,比如力矩、重心、CG% MAC。这部分一旦公式写错,程序即使能运行,结果也是错的。

这次作业也让我真正体会到工具类设计的价值。像 WeightBalanceCalculator类 ,如果直接写进 Flight类,表面上看少了一个类,实际上会让 Flight类 同时承担“保存数据”和“完成专业计算”两种职责,后期非常难维护。拆出来以后,主流程反而更清晰:

输入数据 -> 校验 -> 排序分配 -> 汇总数据 -> 配平计算 -> 生成结果

我在第三次作业里重点做了几组自测:

测试点 检查目的
旅客行李为 0kg 验证组合关系存在,但数据允许为 0
总重量正常但重心超范围 验证“能装下”不等于“能飞”
多名旅客与多舱货物同时存在 检查总重量、总力矩和 CG% 是否一致

这一步做完以后,我对程序正确这件事的理解也比以前具体了一点。以前我更多关注有没有输出、会不会报错;现在会开始去想:输出的这个结果,在业务上到底合不合理。


采坑心得

这三次作业里,我踩过的坑主要集中在下面几个方面。

1. 一开始总想把各种方法都塞进一个类

第一次作业刚上手时,我最自然的想法就是把各种像排序、统计、超载判断都写进 Flight类 里。这样写短期内确实省事,但代码一多就会开始乱。尤其到了第二次、第三次,功能越来越复杂,如果还坚持这种写法,改一个地方往往会牵出别的 bug。后来把排序方法交给 LoadDispatcher类,统计方法交给 LoadManifest类 之后,代码结构明显顺了很多。

这件事让我意识到,面向对象不是“多定义几个类”这么简单,关键在于要实现单一职责原则。

2. 对组合、聚合、依赖等关系概念了解有误

第二次和第三次作业里,比如:

  • Position类 应该由 CargoCompartment类 内部创建,如果从外部随意塞进来,就不够像组合;
  • Cargo类 可以先存在,再被放进舱里,所以更适合聚合;
  • WeightBalanceCalculator类 不应该持有 Flight类 成员变量,否则依赖关系就写成了强耦合。

所以这部分我最后采取的办法是:每改一次排序逻辑,就马上用一组很小的数据测试,比如 3 件货、4 件货等等,先手动检查一遍验证顺序,再拿去跑完整流程。

3. 边界值的选取题目有误

image
一开始我看题目中写着货物的范围是>=0,所以我自然而然的认为前舱货物数量最小值>=0,最大值<=货物数量,可当我这么写了之后,却发现答案始终不正确。我在这之后又调试了无数次,以为是数值或是格式的问题,最后发现都不是。后来通过与同学间的交流发现,当最小值为1而不是0时,这个测试点就能够通过,我认为这是我采的最大的一个坑。

4. 第三次作业真正考验的是“建模”而不是“算公式”

第三次表面上最难的是“载重平衡”公式,但我觉得更难的是怎么把这些计算合理地放进系统里。公式本身查清楚之后可以慢慢写,真正麻烦的是:哪些数据该由 Flight类 管,哪些该交给 Passenger类,哪些应该统一由 WeightBalanceCalculator类 汇总。换句话说,代码写到后面,难点已经不再是某一行公式,而是“这个系统该怎么组织”。

5. 不让用库排序,只能使用冒泡排序

image

一开始我并没有注意到公告里老师给的消息,公告中写着:提交源码只能使用已经学习过的内容,不能使用课程未讲授的内容,例如不能使用list.sort()方法,但可以是使用ArrayList或者LinkedList取代数组和链表。
于是我想当然的在LoadManigest类里用库排序将货物的顺序按重量从高到底排好序,最终导致一个测试点死活过不去。后来在同学的帮助下我注意到了公告内容,将排序改为了冒泡排序,这才过了。


改进建议

如果以后继续迭代这个题目,我觉得可以从下面几个方向继续改。

1. 进一步减少主流程中的细节代码

虽然第三次已经把输入校验和配平计算拆出来了,但主流程里仍然会有不少“读取数据后立刻判断、立刻分派、立刻输出”的细节。如果继续优化,可以把“生成装载报告”“生成异常提示”这类功能再单独封装,让 main 更像调度者,而不是执行者。

2. 用统一的数据结构管理装载结果

目前每次装载成功或失败,很多时候都是直接打印结果。如果后续要扩展成图形界面,或者要导出报表,直接输出到控制台就不太够了。更合适的做法是定义统一的结果对象,例如记录货物名、目标货舱、是否成功、失败原因等信息,最后再决定是打印、保存还是展示。

3. 把业务常量集中管理

像旅客标准体重、舱位力臂、MAC 相关参数等,如果直接散落在各个类里,后面修改会很麻烦。比较好的办法是把这类常量集中起来管理,后续不管是改参数还是查问题,都更方便。

4. 补上更系统的测试

目前我的测试还是偏“人工输入 + 观察输出”,能发现一部分问题,但效率不高。后续如果有时间,可以把典型测试数据整理成固定样例,按“正常情况、边界情况、非法输入”分类,这样每次改代码后都能快速回归。


总结

这三次作业做下来,我最大的收获不是“会写几个类图”或者“会套几个设计原则”,而是开始理解面向对象到底解决了什么问题。第一次作业让我意识到单一职责的重要性;第二次让我真正分清了组合、聚合这些以前只停留在概念层面的关系;第三次则让我明白,依赖关系和工具类设计并不是形式上的拆分,而是为了让系统在功能变复杂之后还能继续维护下去。

当然,我自己还有不少地方需要继续补。比如在动手写代码之前,我对类之间的协作关系想得还不够细;再比如测试意识虽然比以前强了一些,但还没有形成稳定的方法。后面如果继续学习相关内容,我希望能把 UML 设计、边界测试和代码重构这几块再练扎实一点。

对课程本身,我觉得这组三次作业的安排其实很有代表性。它不是一上来就给一个很大的系统,而是通过迭代的方式,让我们在前一题的基础上继续扩展,这样更能体会到“代码是会生长的”。如果后续还能增加一点中间过程的讲评,比如挑几份有代表性的设计方案做课堂分析,或者增加同学之间的简单互评,我觉得会更有助于大家理解为什么有些设计更合理,而不只是知道结果对不对。

总的来说,这三次作业让我从“先写出来再说”慢慢走向“先想清楚类该怎么分”。这个变化不算特别快,但确实是真实发生了。对我来说,这比单纯做对几道题更有价值。