航空器配载与货运管理系统迭代开发总结
一、前言
1.1 作业系列概述
在《面向对象程序设计》课程的本阶段学习中,我们完成了一个循序渐进、高度贴近真实业务场景的“航空器配载与货运管理系统”系列作业。三次作业以迭代方式展开:从最基础的航班货运配载模块,逐步扩展为多货舱管理与重量排序装载,最终演进为包含旅客、行李以及载重平衡计算的完整航空配载系统。整个过程不仅考察了 Java 语言的基本功,更对面向对象设计原则、类间关系建模以及工程化思维进行了系统训练。
1.2 题量、难度
第一次作业仅需实现 Cargo、CargoManifest、Flight 三个核心类和一个主类,代码量约 100 行。主要考察单一职责原则(SRP)的初步应用,以及集合的遍历、简单排序和格式化输出。由于功能单一,难度较低,适合作为迭代的起点。
第二次作业增加到 6 个类,引入了货舱(CargoCompartment)、位置网格(Position)、调度工具(LoadDispatcher)和输入校验工具(InputValidator)。题量接近 200 行,开始明确要求组合/聚合关系的正确实现,以及禁止使用 Collections.sort()、必须手写排序算法。整体难度明显提升,重点在于类间关系的设计与算法细节。
第三次作业达到 10 个类左右,新增旅客(Passenger)、行李(Luggage)实体和重心计算类(WeightBalanceCalculator)。需求融合了航空力学公式,并要求严格遵循 SRP、禁止继承与多态、必须使用冒泡排序,代码量超过 300 行。此次作业综合性极强,难度偏难,真正考验了既能用代码实现物理计算,又能应对用户输入的各种意外,保证程序不崩掉的能力。
1.3三次作业所覆盖的核心知识点
-
面向对象设计原则:重点实践单一职责原则(SRP),每个类仅承担一项清晰职责,避免一个类担任多个功能。
-
类间关系:组合(Composition)、聚合(Aggregation)、依赖(Dependency)的识别与代码实现。
-
集合与算法:使用
ArrayList管理对象,手写选择排序与冒泡排序,实现按重量降序排列并理解排序稳定性。 -
输入验证:统一的校验工具类
InputValidator,处理非法输入、负数及范围错误,异常时立即终止程序。
1.4核心学习目标
通过本系列作业,期望达到以下目标:能够独立从需求文档中提炼出合理的类结构;熟练运用 SRP 原则控制类的大小和职责;能够正确区分并实现组合、聚合和依赖关系,并且能充分理解类间关系的含义;具备手写基本排序算法的能力;养成防御式编程和代码健壮性意识。
二、设计与分析
2.1 第一次作业:基础航班货运配载模块
2.1.1需求回顾
系统需要记录航班号与最大载重重量,地勤人员按货物重量从高到低的顺序添加货物,系统实时计算总重量并判断是否超载。输出要求按重量降序列出货物,并显示总重量与最大载重的对比以及配载状态。
2.1.2关键代码分析
本次作业的核心逻辑集中在 CargoManifest 类的排序方法和 Flight 类的超载判断上。
sortCargosByWeight() 方法采用选择排序算法,每次从未排序部分中选出重量最大的货物,与当前头部元素交换,实现降序排列。该实现直接操作 List<Cargo> 的内部元素,未借助任何 Java 集合框架的排序工具,符合题目对手写算法的要求。选择排序是不稳定排序,若存在相同重量的货物,其原始输入顺序可能被打乱,但本次作业并未对顺序稳定性提出要求,因此可以接受。
Flight 类的 isOverload() 方法通过调用 manifest.getTotalWeight() 获取当前总重,再与 maxWeight 比较。这种委托设计使航班类只需关注“是否超载”这一业务判断,而无需了解重量是如何累加和管理的,初步体现了职责分离。
下面为作业一代码分析图:

代码分析总结:
- 项目规模极小: 整个项目仅包含 1个文件,代码总行数为 107行。这是一个非常轻量级的代码片段或微型项目。
- 结构细碎: 在百行左右的代码中包含了 4个类,说明类的颗粒度很细。
- 逻辑简单清晰:
- 低复杂度: 平均复杂度仅为 1.86,最大复杂度为 7(通常认为10以下为良好),说明代码逻辑非常直接,没有难以理解的复杂算法或嵌套。
- 方法短小: 平均每个方法只有约 3.7条语句,代码的颗粒度控制得很好,符合单一职责原则。
- 嵌套较浅: 平均嵌套深度不到2层,最大深度为5层,阅读起来没有负担。
- 缺乏文档:
- 注释率为 0.0%:这是代码中最显著的问题。虽然代码简单易懂,但完全没有注释意味着缺乏必要的说明,不利于后期的维护或他人接手。
2.1.3类的设计与分析
下面是第一次作业的类图

从图中可知:
-
Cargo (货物类):
- 属性:
name(String) 和weight(double),都是私有的 。 - 方法: 提供了构造函数以及获取名称和重量的 Getter 方法。
- 属性:
-
CargoManifest (货物清单类):
- 属性: 持有一个
List<Cargo>集合 (cargos)。 - 方法:
addCargo: 添加货物。getTotalWeight: 计算总重量。sortCargosByWeight: 实现了按重量排序的逻辑(代码中使用的是选择排序)。getCargos: 返回货物列表。
- 关系: 它是
Flight的一部分。
- 属性: 持有一个
-
Flight (航班类):
- 属性: 包含航班号 (
flightNo)、最大载重 (maxWeight) 和一个CargoManifest对象 (manifest)。 - 方法:
addCargo: 将货物委托给 manifest 处理。isOverload: 检查当前总重量是否超过最大载重。getManifest/getMaxWeight: 获取相关属性。
- 关系: 它与
CargoManifest是组合关系 ,因为一个航班必须有一个清单,且通常清单随航班创建而创建,随航班销毁而销毁。
- 属性: 包含航班号 (
-
Main (主类):
- 包含
main方法,作为程序的入口,负责读取输入、创建对象并调用逻辑。它与Flight和Cargo是依赖关系 。
- 包含
2.1.4心得:
初次接触这种“先设计类图再编码”的方式,最大的感受是代码结构变得异常清晰。过去我常常把所有逻辑塞进一个 Main 类里,改一处就会牵动全身。这次将排序逻辑单独放在 CargoManifest 中,航班类只调用结果,如果未来排序规则变化,只需要修改清单类而不会影响航班类,这就是单一职责原则带来的好处。不过,选择排序的不稳定性当时并未意识到,直到第二次作业才察觉顺序会乱,算是埋下了一个小伏笔。这个小细节让我明白,编程不能只关注逻辑。
2.2 第二次作业:多货舱管理与重量排序装载
2.2.1需求回顾
飞机被划分为多个货舱,每个货舱拥有独立的最大载重和行列网格构成的装载位置。货物需按重量降序排序后,依次尝试装入指定的目标货舱,若加入后超出该货舱最大载重则装载失败并给出提示。系统还需记录航班整体的最大起飞重量和最大业载重量,并对整体超载情况进行判断。
2.2.2关键代码分析
本次作业的装载逻辑集中在 CargoCompartment.addCargo() 方法中。与第一次作业不同,这里采用了预判断机制:该方法在货物真正加入集合之前,先计算加入后的总重是否超出上限,若未超则添加并返回 true,否则返回 false。将超载判断与装载动作封装在一起,避免了外部先检查再装载可能产生的逻辑不一致。货舱本身不直接输出信息,而是将结果返回给主流程处理,维护了单一职责。
下面为作业二代码分析图:

代码分析总结:
- 项目规模小幅增长: 项目仍为单文件(1个文件),但代码量增加至 203行,相比之前的版本规模有所扩大,但仍属于小型项目。
- 结构更为细碎: 在203行代码中包含了 7个类,平均每个类只有约 2.29个方法。说明代码被拆解得非常细致,可能包含大量简单的数据结构或工具类。
- 代码质量极佳(优点):
- 超低复杂度: 平均复杂度仅为 2.00,最大复杂度为 5(处于极优水平),几乎没有复杂的控制流。
- 方法体量小: 平均每个方法约 5.75条语句,方法非常短小,易于理解和测试。
- 逻辑层级浅: 平均嵌套深度仅 2.12,代码阅读非常顺畅。
- 缺乏文档(顽疾):
-
- 注释率仍为 0.0%:尽管代码逻辑简单,但完全没有注释。随着代码行数增加(从107行增至203行),缺乏注释的问题会逐渐放大。
-
2.2.3类的设计与分析
下面是第二次作业的类图:

从图中可知:
Position (位置类):
- 属性:
row(int) 和col(int),表示行号和列号。 - 方法: 提供了构造函数以及
getPosName方法,用于获取格式化的位置名称(如“1行2列”)。 - 角色: 表示货舱内的具体物理存储位置。
Cargo (货物类):
- 属性:
id(String),weight(double),targetCompartmentId(String)。 - 方法: 提供了构造函数用于初始化货物ID、重量和目标货舱ID。
- 角色: 表示需要被装载的货物实体。
CargoCompartment (货舱类):
- 属性: 包含货舱ID (
id)、最大载重 (maxWeight)、位置列表 (positions) 和货物列表 (cargos)。 - 方法:
addCargo: 尝试添加货物,若不超过最大载重则添加成功。getCurrentWeight: 计算当前货舱内货物的总重量。isOverloaded: 判断当前货舱是否处于超载状态。
- 关系: 它与
Position是组合关系 ,因为货舱在构造函数中直接创建了具体的位置对象,位置是货舱不可分割的一部分;它与Cargo是聚合关系 ,因为货舱负责管理货物,但货物对象是在外部创建的。
Flight (航班类):
- 属性: 包含航班号 (
flightNumber)、最大起飞重量 (maxTakeoffWeight)、最大业载 (maxPayloadWeight) 和货舱列表 (compartments)。 - 方法:
addCompartment: 向航班中添加一个货舱。getTotalWeight: 计算所有货舱的载重总和。findCompartment: 根据ID查找特定的货舱。
- 关系: 它与
CargoCompartment是聚合关系 ,因为航班由多个货舱组成,但货舱对象是在外部创建后传入的。
LoadDispatcher (装载调度类):
- 属性: 无属性。
- 方法:
sortCargos: 静态方法,按重量对货物列表进行降序排序(选择排序)。FindCargo: 静态方法,根据ID在列表中查找货物。
- 关系: 它与
Cargo是依赖关系 ,因为其方法的参数和返回值涉及Cargo对象,属于工具类性质。
InputValidator (输入验证类):
- 属性: 无属性。
- 方法:
validateInteger和validateDouble,均为静态方法,用于验证输入数值是否在指定范围内。 - 角色: 工具类,虽然在
Main中未实际调用,但设计用于保证数据合法性。
Main (主类):
- 包含
main方法,作为程序的入口。负责读取用户输入、创建Flight、CargoCompartment和Cargo对象,并调用LoadDispatcher进行排序和装载逻辑。它与上述业务类均为依赖关系。
2.2.4心得:
第二次作业让我深刻体会到类关系的精妙。起初我把货舱和位置设计成聚合关系,让位置可以脱离货舱存在,但需求明确指出位置是货舱固有的属性,应该用组合关系。将位置集合的生成写进货舱的构造函数后,外部无法随意修改位置,数据一致性得到了保障。另一个难点是冒泡排序的稳定性问题——作业明确要求同等重量货物保持输入顺序,这逼着我去查了两种排序算法的区别。最终发现冒泡排序正好满足要求,而直接复制第一次作业的选择排序就会翻车。输入校验工具类的设计也让我学到了“防御式编程”。整个作业下来,我最深的感悟是:好的类结构不是为了应付作业,而是真的能减少后续改动的工作量。比如后来第三次作业加旅客时,货舱部分的代码几乎不用修改,这就验证了开闭原则的思想。
2.3 第三次作业:配平计算与旅客管理
2.3.1需求回顾
在已有货舱管理的基础上,引入旅客实体(标准体重75kg + 行李重量),并要求结合前后货舱的货物分布,进行全机载重平衡计算。计算依据空机重量、各装载区域的固定力臂、MAC参数等常量,得出总重量、总力矩、实际重心及CG%MAC,最终评估配平状态是否在安全范围内。输入校验要求更加严格,任何负数或格式错误均会导致程序终止。
2.3.2关键代码分析
本次作业的核心计算全部集中在 WeightBalanceCalculator.generateLoadSheet() 方法中。方法内部定义了若干静态常量,包括空机重量40000.0kg、空机力臂16.25m、旅客舱力臂18.0m、前货舱力臂12.0m、后货舱力臂22.0m等。该方法将所有的航空物理公式完全封装在工具类内部,Flight、Passenger、CargoCompartment 等实体无需了解任何计算细节,仅通过getter提供数据。这种依赖关系明确且耦合度极低,正是单一职责原则和依赖倒置思想的体现——高层计算逻辑依赖于底层实体的抽象数据,而不依赖具体实现。
输入验证方面,InputValidator 进行了参数化重构,getNextInt 和 getNextDouble 方法接收最小值和最大值作为参数,动态生成错误提示信息,完美匹配了题目对各种范围检查的输出要求。
下面为作业三的代码分析图:

代码分析总结:
- 代码规模快速膨胀: 项目仍保持在 1个文件 中,但代码总行数已增长至 325行。相比上一个检查点(203行),代码量增加了约60%,项目处于快速开发或功能扩展阶段。
- 数据缺失限制详细分析: 由于提供的图片描述中截断了关键指标(如Statements、Classes、Complexity、% Comments等),无法评估当前的类结构分布、代码复杂度以及注释覆盖情况。
- 潜在风险提示: 随着单文件代码量突破300行,若没有良好的模块拆分(Classes数量)和合理的复杂度控制,代码的可读性和维护性可能会开始下降。同时,延续前两个版本的趋势,需警惕注释率可能依然为0的问题。
2.3.3类的设计与分析
下面是第三次作业的类图:

从图中可知:
InputValidator (输入验证工具类):
- 属性: 无实例属性。
- 方法:
getNextInt和getNextDouble,均为静态方法。用于从Scanner读取输入,并进行非空、非负及范围验证,若不合法则退出程序。 - 角色: 纯粹的静态工具类,用于保证输入数据的安全性。
Luggage (行李类):
- 属性:
weight(double),表示行李重量。 - 方法: 提供了构造函数和获取重量的方法。
- 关系: 它是
Passenger的一部分,被组合在乘客对象中。
Passenger (乘客类):
- 属性:
STANDARD_WEIGHT(静态常量,标准体重) 和luggage(行李对象)。 - 方法:
Passenger: 构造函数,接收行李重量并实例化Luggage对象。getTotalWeight: 计算乘客总重量(标准体重 + 行李重量)。
- 关系: 它与
Luggage是组合关系 ,因为乘客对象在创建时直接创建了属于该乘客的行李对象,生命周期紧密绑定。
Cargo (货物类):
- 属性:
id(int) 和weight(double)。 - 方法: 提供构造函数和相应的 Getter 方法。
- 角色: 代表装载在货舱中的独立货物实体。
Position (位置类):
- 属性:
row(int) 和col(int),表示货舱内的行列位置。 - 方法: 提供构造函数和获取位置名称的方法。
- 关系: 它是
CargoCompartment的一部分。
CargoCompartment (货舱类):
- 属性: 包含货舱ID (
id)、最大载重 (maxWeight)、位置列表 (positions) 和货物列表 (cargos)。 - 方法:
CargoCompartment: 构造函数,根据行列数循环创建Position对象。addCargo: 尝试添加货物,检查是否超重。getCurrentWeight/isOverloaded: 计算当前载重及状态。
- 关系:
- 与
Position是组合关系 ,位置在货舱构造时生成,属于货舱内部结构。 - 与
Cargo是聚合关系 ,货物在Main中创建后被添加进来。
- 与
Flight (航班类):
- 属性: 包含航班号 (
flightNumber)、货舱列表 (compartments) 和乘客列表 (passengers)。 - 方法: 提供添加货舱、添加乘客以及获取这些列表的方法。
- 关系:
- 与
CargoCompartment是聚合关系 。 - 与
Passenger是聚合关系 。 - 航班作为聚合根,管理着货舱和乘客,但这两者可以在外部创建后传入。
- 与
WeightBalanceCalculator (载重平衡计算器):
- 属性: 定义了一系列静态常量,如空机重量 (
EMPTY_WEIGHT)、力臂 (EMPTY_ARM等)、重心限制 (MIN_CG_PERCENT等)。 - 方法:
generateLoadSheet,根据传入的Flight对象,计算总重量、总力矩、重心位置,并打印载重平衡舱单。 - 关系: 它与
Flight、Passenger、CargoCompartment和Cargo均为依赖关系 ,因为它仅读取这些对象的数据进行计算,不负责管理它们的创建或销毁。
Main (主类):
- 包含
main方法,是程序的入口。它负责整个流程的控制:使用InputValidator读取输入,创建Flight、CargoCompartment、Passenger和Cargo对象,并组装它们,最后调用WeightBalanceCalculator生成报表。它与上述所有类均为依赖关系。
2.3.4心得:
第三次作业让我最有成就感,因为它把之前看似零散的功能全部串了起来。重心计算公式一开始看得我头疼,但把它们拆解成“数据从实体拿,计算在工具类做”的步骤后,逻辑就非常清晰了。特别值得一提的是,我把所有航空常量都设为 static final,不仅避免了魔法数字,还让维护和未来参数调整变得极其方便。在测试时,我特意输入了几组会导致重心超出范围的极端数据(比如前舱装得过重),程序准确地报出了“危险”警告,这让我感受到代码的物理计算模型确实是正确的。回顾整个迭代过程,从第一次连类都分不好的新手,到最终能搭出一个有物理意义的系统,所依靠的正是老师反复强调的那些看似平淡的设计原则。没有这些原则,代码量上来后维护难度会指数级增长,我算是真正体验到了“好的设计是最佳实践”的含义。
三、踩坑心得
3.1 第一次作业遇到的坑
3.1.1坑一:nextInt() 与 nextLine() 混用导致数据读取错乱
在第一次作业的主程序输入处理中,我按照直觉先用 scanner.nextInt() 读取最大载重重量,紧接着用 scanner.nextLine() 读取航班号。结果程序运行后,航班号读到的却是空字符串,后续货物名称的读取也全乱套了。
问题定位
经过调试发现,nextInt() 方法在读取整数时,只把数字部分取走了,用户在输入数字后敲下的那个回车符仍然留在输入缓冲区里。当程序立即执行 nextLine() 时,它读到的就是这个残留的回车符,直接返回空字符串,根本没有给我输入航班号的机会。这就是经典的“回车陷阱”。
具体数据示例
假如输入如下:
CA1201
2000
3
我期望的读取顺序是:航班号 CA1201,最大载重 2000.0,货物件数 3。但如果代码写成 flightNo = scanner.nextLine() 然后 maxWeight = scanner.nextDouble(),实际效果会变成:航班号读到空字符串,最大载重尝试读取 CA1201 直接抛出 InputMismatchException。即便调整顺序先读 nextDouble() 再读 nextLine(),后面的 nextLine() 仍会吞掉数字后的回车,导致第一个货物名被跳过。
解决方案
题目给出的提示是最直接的:在 nextInt() 或 nextDouble() 之后,多写一个 scanner.nextLine() 把残留的回车符吃掉。
3.1.2坑二:选择排序导致货物顺序与预期不一致
第一次作业要求“按照货物重量从高到低向航班添加货物”,我实现了 CargoManifest.sortCargosByWeight() 方法,用的是选择排序。基本思路是:每一轮从未排序部分找出重量最大的货物,和当前头部元素交换位置。
问题定位
当输入多件重量相同的货物时,输出结果和样例里“同等重量按输入顺序排列”的隐含规则对不上。比如输入:
水果 500
电子产品 500
按照输入顺序,水果在前、电子产品在后。但选择排序后,两件货物可能互换了顺序。
3.1.3第一次作业踩坑心得:
第一次作业的两个坑都源于对底层机制和算法特性认识不足。输入缓冲区问题让我意识到,不能想当然地认为高级语言的 API 会替我们处理一切细节,需要清楚每个方法到底做了什么、没做什么。排序稳定性问题则提醒我,选择算法时不能只看功能“能用就行”,还得考虑业务上的隐性约束。这两个教训为后续作业打下了重要的基础,让我在写代码时更注重“预判”而不是“事后修补”。
3.2第二次作业遇到的坑
3.2.1坑一:货舱货物列表缺乏封装导致数据被越权修改
在 CargoCompartment 的初版设计中,我为方便外部遍历装载的货物,将货物列表声明为 public List<Cargo> cargos,并直接通过 getCargos() 返回该引用。我以为这可以简化输出逻辑,却埋下了严重隐患。
问题暴露
在某次调试时,我无意间在 Main 类中写了一句:
compartment.getCargos().add(new Cargo("测试货物", 10000.0));
这条语句直接绕过了 addCargo 方法中的超载校验,把一个 10000.0kg 的货物硬塞进了最大载重只有 5000.0kg 的货舱。程序继续运行,后续的货舱状态输出显示“已装重量:14500.0kg / 最大载重:5000.0kg”,但超载状态判断却仍然为“正常”,因为那个判断是在添加货物时触发的,而这次添加根本没经过那个入口。数据不一致导致整个配载结果完全失真。
3.2.2第二次作业踩坑心得:
第二次作业的坑触及类对外接口的封装性。货物列表的封装失败则让我刻骨铭心地认识到,封装绝不仅仅是加个 private 关键字就完事了,它意味着要精心控制每一个内部数据的访问路径,确保业务规则不会被任何“后门”绕过。这个教训使我在第三次作业设计 Passenger 与 Luggage 的关系时格外谨慎,坚决杜绝了内部对象引用的外泄。
3.3 第三次作业遇到的坑
3.3.1坑一:旅客总重计算中行李对象暴露导致的外部修改风险
第三次作业新增了 Passenger 类,需求要求“行李对象必须在 Passenger 构造器内部 new 出来,不对外暴露修改”。
问题分析
虽然行李对象确实在构造器内部创建了,但我写了一个 getLuggage() 方法把行李对象的引用直接返回给外部。即便 Luggage 类没有提供 setter,返回引用本身也破坏了封装性——外部获取到了内部组合对象的控制权。这和需求中“不对外暴露修改”的意图是违背的。
3.3.2坑二:浮点数计算精度导致重心百分比出现微小偏差
问题发现
使用样例数据测试时,总重量为 45305.0kg,总力矩为 754290.0kg·m,实际重心应为 754290.0 / 45305.0 = 16.65...,约等于 16.6m(保留一位小数)。CG% MAC 应为 ((16.65 - 15.0) / 5.0) * 100.0 = 33.0%。但在某些含有大量小数运算的极端测试数据下(例如旅客行李重量含多位小数),最终 cgPercent 的结果可能是 32.9999999 或 33.0000001,如果直接用 System.out.println 输出,可能显示为 33.0%,但在后续版本中若将 cgPercent 与边界值 38.0 进行比较时,33.0000001 <= 38.0 仍然为 true,不会造成判断错误。但如果恰好计算结果是 37.9999999,而安全上限是 38.0,判断结果为 true,符合安全标准。但如果结果是 38.0000001,判断结果为 false,会误报危险,而理论值本应是 38.0,属于安全边界。
3.3.3第三次作业踩坑心得:
第三次作业的坑都跟“封装性”有关,一个是数据封装,一个是计算精度的封装。行李对象的隐藏是对组合关系严谨性的检验,它说明即使代码能跑通,如果不注意访问控制的粒度,封装就可能形同虚设。浮点精度问题则让我接触到了计算机表示实数时的固有缺陷,虽然本次作业没有酿成实际错误,但让我建立了“浮点数比较不能简单用等号”的警戒心。这两个问题都促使我向更严谨的工程化思维靠拢,不再满足于“输出结果对了”,而是追求“设计上经得起推敲”。
四、改进建议
4.1 第一次作业改进建议
建议:用冒泡排序替换选择排序,保证输出顺序稳定
第一次作业的 CargoManifest.sortCargosByWeight() 采用了选择排序,其不稳定性导致相同重量货物的输入顺序在排序后无法保持。虽然本次作业未强制要求稳定性,但从实际输出样例来看,系统隐含期待按重量降序的同时保持同重量货物的原始输入顺序。将排序算法改为冒泡排序是成本最低且收益明确的改进方案。冒泡排序通过相邻元素的两两比较与交换,天然保证相等元素不交换顺序,实现稳定排序。代码只需将选择排序的“找最大值+跳跃交换”替换为双重循环的相邻比较即可,算法复杂度相同,却消除了顺序不确定的隐患。这一改进能提升程序输出的一致性,也为后续迭代(第二次作业明确要求稳定排序)提前铺平道路。
4.2 第二次作业改进建议
建议:将货舱状态信息封装为专门的数据对象,避免暴露内部集合
第二次作业中,CargoCompartment 的 getCargos() 方法返回了内部货物列表的引用。这虽然方便了报表输出,却给外部代码留下了直接修改货物集合的通道,绕过了 addCargo 中的超载校验,破坏了封装性。建议将装载状态信息的输出职责进一步分离:可以让货舱类提供一个 getLoadInfo() 方法,返回一个只包含当前重量、最大载重、货物数量等摘要信息的不可变数据对象;或者提供 getCargoListCopy() 返回一个集合的浅拷贝,确保外部无法修改原集合。这样既满足了报表对货物数据的读取需求,又封死了状态篡改的可能,真正实现“内部状态的变化只能通过受控的方法完成”这一封装原则。
4.3 第三次作业改进建议
建议:在重心百分比比较中引入浮点容差,提高安全判断的可靠性
目前的 cgPercent 计算使用 double 类型,在与安全边界 38.0 比较时直接使用 <= 运算符。由于浮点运算存在微小误差,当理论值恰好在边界附近时,可能产生误判(例如理论值应为 38.0,实际计算值为 38.0000001,被判为危险)。虽然本次作业的数据规模和物理常量导致边界误判概率极低,但从工程严谨性出发,建议引入一个极小的容差值(例如 EPSILON = 1e-9),将安全判断修改为 cgPercent <= MAX_CG_PERCENT + EPSILON。这样可以在不改变整体判定逻辑的前提下,容忍无害的浮点误差,避免因数值精度问题导致的假阳性警告。这一改进体现了对数值计算复杂性的尊重,也是从“作业代码”迈向“工业级代码”的良好习惯。
五、总结
5.1 综合性总结与收获
回顾这三次迭代作业,我从一个只会把所有代码塞进 Main 方法的新手,逐步成长为一个能够有意识地运用面向对象原则去构建系统的初学者。这个过程带给我的不仅仅是 Java 语法层面的熟练度提升,更是编程思维上的一次重要转变。
第一次作业让我迈出了从“面向过程”到“面向对象”的第一步。在实现基础货运配载模块时,我开始学会把不同的职责分配给不同的类:Cargo 只管货物数据,CargoManifest 管理集合和排序,Flight 负责航班信息和超载判断。这种拆分看似增加了类的数量,却让每个类的代码都变得很短、很聚焦,修改起来不再担心牵一发而动全身。我也第一次体会到,原来好的设计真的能让代码更“好改”,而不仅仅是为了好看。
第二次作业则让我真正感受到了“迭代开发”的魅力与挑战。当需求从单一货舱扩展为多货舱时,如果第一次作业的类设计糟糕,这次改动会非常痛苦。但因为我已经将货物管理和航班信息分离,只需新增 CargoCompartment 和 Position 类,再对 Flight 做有限修改就能适配。这个过程中,我深刻理解了组合与聚合的区别——位置是货舱不可分割的组成部分,必须在货舱创建时一同生成;而货物可以独立存在,被添加到货舱中。这两种关系在代码中的体现截然不同,一旦选错,后续维护就会埋下隐患。同时,手写冒泡排序和输入校验工具类的设计,也让我从“能跑就行”开始转向思考“如何写得更稳、更安全”。
第三次作业是整个系列的高潮。当旅客、行李、力臂、力矩、重心、MAC 百分比这些概念一起涌来时,一开始我是发懵的。但静下心来把公式拆解开,发现核心逻辑无非是“从各个实体那里拿数据,按公式累加计算,最后判断”。我把所有物理常量定义在 WeightBalanceCalculator 中,让计算逻辑与业务实体完全解耦,这个设计决策让我很有成就感。更让我警醒的是对封装性的反复审视——行李对象必须完全隐藏在 Passenger 内部,货舱的货物列表不能随意暴露引用,这些细节在第三次作业中被放大了。我开始意识到,面向对象中的“封装”不是加个 private 就完事了,它要求我们严格控制每一个数据访问的路径,确保业务规则不会被任何后门绕过。
总的来说,这三次作业像三级台阶,每一级都踩得很实。我学会了如何从需求中提取类、如何定义类之间的关系、如何手写基础算法、如何处理输入异常、如何将领域知识转化为代码逻辑,更重要的是,学会了在写每一行代码时多问自己一句:“这个类真的应该负责这件事吗?”
5.2 进一步研究的内容
通过这三次作业,我也清晰地看到了自己当前能力的边界,有几块内容是我接下来需要重点深入学习的。
首先是设计模式。本次作业明确禁止使用继承和多态,全部依靠组合、聚合和依赖来构建系统。但在真实项目中,当货舱类型变多(比如温控舱、散货舱、活体动物舱),它们有不同的装载规则和计算方式时,单纯靠组合会导致大量的条件分支。这时就需要工厂模式创建不同类型的货舱,或者用策略模式封装不同的装载策略。学会在合适的场景引入合适的设计模式,是我从“作业代码”走向“工程代码”必须迈过的一道坎。
其次是单元测试。整个系列作业我都是靠手动输入样例数据、肉眼比对输出来验证正确性的。这种方式效率低,而且一旦修改代码,回归测试几乎不可能做全。我打算系统学习 JUnit 框架,练习为 addCargo、getTotalWeight、generateLoadSheet 这些核心方法编写自动化测试用例,特别是针对边界条件(如重量刚好等于上限、旅客行李为 0、重心恰在安全边界)的测试。测试驱动开发(TDD)虽然短期会多花时间,但从长远看能极大降低 bug 率,这个习惯越早养成越好。
最后是代码质量管理工具的使用。老师在博客要求中提到了 SourceMonitor 和 PowerDesigner,我在本次作业中只是简单体验了一下,还没有形成常态化的使用习惯。后续我希望能够把这些工具融入自己的开发流程:每次写完代码后,用 SourceMonitor 检查圈复杂度和方法长度,用 Checkstyle 规范代码风格,通过 PowerDesigner 反向工程生成类图来审视自己的设计是否合理。让代码质量从“我感觉还行”变成“数据告诉我还行”,这是一名专业开发者应该具备的素养。
5.3 结语
航空器配载与货运管理系统的三次迭代作业,对我而言不仅是一项课程任务,更像是一个微型项目的完整生命周期。从最初只管理几件货物的简陋程序,到最后能够结合物理公式评估飞行安全的系统,这段经历让我真切体会到了软件是如何在不断的迭代中生长、完善的。
在此过程中,失败和踩坑是常态,但每一次调试和修正都让我对某个知识点有了更深一层的理解。无论是输入缓冲区的回车陷阱、排序算法的稳定性差异、超载判断的时机选择,还是封装不严导致的越权修改风险,这些坑都成了我成长路上最宝贵的路标。
感谢老师的精心设计,将真实业务场景拆解为适合新手的迭代步骤,并在作业要求中反复强调单一职责、组合聚合、手写算法等关键原则。这些看似严格的约束,恰恰是帮助我们建立正确编程思维的最佳指引。我也希望课程未来能加入更多代码评审和测试相关的教学环节,让同学们不仅会写代码,更会审视和改进代码。
这个系列的结束不是终点,而是我将面向对象思维真正内化为编程习惯的起点。我会带着这三次作业积累下来的设计意识、防御式编程理念和对代码质量的追求,继续向更广阔的程序设计世界探索

浙公网安备 33010602011771号