面向对象程序设计作业题目集1-3总结
一、前言
这三次PTA作业挺有意思的,都是围绕“航空器配载与货运管理”这个场景,每次都在上次的基础上加新功能,像滚雪球一样越滚越大。从最简单的货物装货判断,到最后算重心、评估安全,难度一阶一阶往上走。我把自己三次作业的知识点、代码量、踩坑情况整理了一下,先列个表让大家一眼看清楚。
| 作业次序 | 核心知识点 | 代码行数 | 难度评估 | 主要挑战 |
| 第一次 | 类封装、选择排序、ArrayList、格式化输出 | 58 | 较简单 | 第一次试着用两个类协作,排序逻辑得自己敲,不能直接调方法 |
| 第二次 | 多类协作、冒泡排序、货舱容量管理、输入次序保证、基础校验 | 202 | 中等 | 类突然变多了,责任怎么分?货物分舱的时候差点搞乱 |
| 第三次 | 旅客与行李管理、重心及力矩计算、输入边界校验、静态常量设计、手工排序 | 373 | 较难 | 公式理解错一点全盘错,还得自己写排序,debug到头大 |
二、设计与分析
第一次作业
第一次任务不复杂:读入航班号、最大载重、货物列表,对货物按重量降序排个序,然后输出,最后判断总重有没有超限。
类图结构:

Main里面把输入、排序、输出全包了,Cargo就是个纯数据袋子,只有两个字段和getter。这种写法在功能简单的时候没啥毛病,代码一眼看穿。但我当时已经隐隐觉得不对——要是以后再加点需求,Main不得炸了?果然后面两次作业我基本是推倒重来。
SourceMonitor 指标分析

| 指标 | 数值 | 解读 |
| 语句数 | 40 | 量不大,逻辑一眼望到头 |
| 分支语句占比 | 17.5% | 主要就是if判断超不超载,还有循环里的分支 |
| 最复杂方法 | Main() | 复杂度8,对第一次作业来说还行,但已经偏高了 |
| 最大嵌套深度 | 5 | 出现在排序的双重循环里,循环套循环再套if,看着就晕 |
| 平均方法复杂度 | 2.75 | 被Cargo那几个简单方法拉下去了,实际上main不低 |
心得:第一次作业让我明白了一个道理:别把所有代码都塞main里。虽然当时为了赶着通过测试没重构,但心里已经知道这结构撑不了多久。果然第二次就不得不重写。
第二次作业
第二次作业加了多货舱。每个货物有个目标货舱代号,先按重量降序(同重量按输入顺序升序)排好,然后挨个往对应货舱里装,如果装不下就报失败,最后打印每个货舱的装载情况和整机状态。
类图结构

类数量一下子加到7个。看起来责任分开了:Airplane管多个Bay,Dispatcher提供排序和查找,Checker做边界检查。但实际写起来还是有点别扭——Dispatcher.sortCargos是静态的,直接改传入的List;Goods里还存着targetId,Main里面一个循环就把它塞给对应的Bay。本质上还是线性思维,没有真的让对象之间互相“发消息”。
SourceMonitor 指标分析

| 指标 | 数值 | 对比第一次作业的变化 |
| 语句数 | 154 | 翻了快4倍,复杂度明显上去了 |
| 分支语句占比 | 13.6% | 占比降了,但绝对分支数反而多了 |
| 最复杂方法 | Main() | 复杂度13,比第一次的8还高5个点 |
| 最大嵌套深度 | 4 | 比第一次降了点,说明循环结构优化了一些 |
| 平均复杂度 | 2.04 | getter/setter多了,平均值被拉低 |
| 方法/类 | 3,71 | 每个类平均3-4个方法,粒度还可以 |
心得:第二次作业让我发现“静态方法用多了”其实挺害人的。Dispatcher里面全是静态,好像很方便,但这样对象自己反而啥也不干了。理想的情况应该是Airplane自己有个distributeGoods方法,内部去调用各个Bay的load,而不是main在外面一个一个塞。另外,虽然写了Checker类,但实际输入的时候压根没用上——这也给第三次作业埋了雷。
第三次作业
第三次作业加了旅客(含行李)、重心百分比计算、严格的输入校验。题目变成了:给一个航班,前后货舱容量、旅客行李重量、货物清单(分前后舱),算出起飞总重、总力矩、重心位置(%MAC),然后判断安全不安全。
类图结构

WeightBalanceCalculator把重心计算有关的常量和逻辑都包在一起,这个设计方向我是挺喜欢的,高度聚合。InputValidator也弥补了上次输入不校验的问题。
SourceMonitor 指标分析

| 指标 | 数值 | 对比第二次作业的变化 |
| 语句数 | 260 | 继续增长,新增旅客和重心计算模块 |
| 带注释的行占比 | 1.1% | 几乎没有注释,这是一个严重问题 |
| 最复杂方法 | generateLoadSheet | 复杂度14,比第二次的main(13)还高 |
| 最大嵌套深度 | 6 | 出现在货舱货物遍历和输出格式化的嵌套循环中 |
| 平均方法复杂度 | 2.3 | 略微上升 |
| 方法/类 | 3 | 每个类方法数较第二次下降,说明有些类方法较少 |
最复杂方法的问题:generateLoadSheet干了太多事——算旅客总重、算货物总重、算力矩、输出各种表格、评估安全。它不应该这么“胖”。按照单一职责原则,至少应该拆成5-6个小方法。
最大嵌套深度6:看看我当时写的代码(类似下面这样)——

功能是实现了,但读起来真费劲。而且把货舱类型(id==1或2)硬编码在里面,以后要加第三个货舱,代码就得改得乱七八糟。
心得:第三次作业让我发现,当一开始没留好扩展点的时候,后面加功能就只能“打补丁”。generateLoadSheet就是典型的补丁式产物。还有InputValidator里直接System.exit(0),虽然对PTA这种判题环境能凑合,但实际项目中这么干是不行的。
三、采坑心得
在完成这三次作业的过程中,我遇到了不少具体的问题,有些已经解决,有些至今仍有遗憾。
3.1 第一次作业的坑:
1.比较浮点数时直接使用 ==
判断超载的时候我一开始写的是if (total > maxWeight)。后来想起浮点数精度的问题,改成了if (total > maxWeight + 1e-9)。虽然这个改不改当时测试都能过,但至少让我记住了:浮点数别直接用==或者>,要给点容差。后面两次作业我都统一用了1e-9。
3.2 第二次作业的坑:
1.冒泡排序的交换条件写反了
第二次作业要求“同等重量时按输入顺序升序排列”,我在实现冒泡排序时,交换条件的逻辑写成了:

但在实际冒泡中,为了达到降序效果,应该是“如果前一个重量小于后一个重量”才交换。这个逻辑本身没有问题,但因为我在循环边界和索引处理上不够仔细,导致排序结果偶尔出现乱序。最后通过打印中间过程才发现问题:原来是在多重条件分支中少考虑了一种情况——当重量相等且输入顺序相等时,不应该交换,但我的条件分支没有显式处理,导致在某些边界条件下 保留了上一次循环的值。修复方法是每次循环前重置 。
3.3 第三次作业的坑:
1.重心百分比计算公式理解有偏差
WeightBalanceCalculator 中重心的计算方式为:

我一直以为cgPercent算出来就应该在安全范围[25,38]内,但怎么算都不对,老是超。折腾了大半天才发现:力矩累加那块出错了——我把ARM_FRONT用到了所有货舱,实际上后货舱应该用ARM_REAR。改完之后,cgPercent就正常了。这种错就属于“抄公式抄错变量”的低级失误。
2.排序的陷阱
generateLoadSheet 方法中注释掉了一段代码:

这本来是要对每个货舱内的货物再按重量排序,结果我注释掉之后忘了恢复。导致输出货舱货物清单时,顺序是输入顺序而不是重量降序,题目要求的那个测试点直接凉了。现在想想,这种错完全是自己粗心。
3.4 输入校验的“死板退出”
InputValidator里面,只要输入不合法就System.exit(0)。在PTA上能过,因为判题系统只关心输出对不对,不关心程序有没有粗暴退出。但真写项目的话,这绝对是坏习惯。更好的做法是抛异常或者返回Optional,让上层决定怎么处理。这一点我以后得改。
四、改进建议
如果让我重新做这三次作业,或者再迭代一版,我会在下面几个地方下功夫:
4.1 第一次作业——把排序抽出来
即使是小作业,也可以养成好习惯:单独写一个CargoSorter类,main只负责调用。以后想换排序算法,只改CargoSorter就行,main不受影响。
4.2 第二次作业——用策略模式代替静态排序
Dispatcher.sortCargos静态方法写死了冒泡排序。更好的设计是定义一个SortStrategy接口,然后让WeightDescSorter去实现它。以后如果需求变成按货物名排序,再加一个NameSorter就行,不用改原有代码——这就是开闭原则。
另外,货物分配的逻辑不要放在main里。可以在Airplane里加一个distributeGoods(List<Goods> goodsList)方法,让它自己去叫每个Bay的load。main只负责创建对象和调用,不负责具体的分配细节。
4.3 第三次作业——拆分generateLoadSheet
这个方法复杂度14,已经高得离谱了。可以拆成:
1.calculateTotalWeight()
2.calculateTotalMoment()
3.calculateCG()
4.printPassengerInfo()
5.printCargoInfo()
6.printBalanceAssessment()
拆完之后每个方法都很短,容易测试,也容易看懂。那些常量(WEIGHT_EMPTY、ARM_PAX等)最好挪到一个单独的AircraftConfig类里,以后换机型只需要改配置类。
4.4 通用改进——写单元测试
三次作业我全是用手工测试+提交PTA看结果。效率极低,而且很多边界条件根本测不到。以后应该用JUnit给核心方法写单元测试,比如测试Bay.load在超载时是不是返回false,测试WeightBalanceCalculator在各种合法/非法输入下是不是算得对。养成TDD的习惯,虽然开始慢,但后期省时间。
五、总结
通过这三轮迭代,我觉得自己在下面几个方面进步最明显:
1。面向对象意识:从第一次的main一条龙,到第三次知道要分好几个类,虽然分得还不完美,但至少开始考虑职责分离了。
2.排序算法:自己亲手实现了选择排序和冒泡排序,还处理了多级比较(先比重量、再比顺序),对稳定排序的理解加深了。
3.浮点数处理:被精度坑过一次之后,再也不敢直接用==了,容差比较成了习惯。
4.代码复杂度敏感度:看了SourceMonitor的报表之后,我意识到一个方法复杂度14意味着什么——不只是维护困难,改起来也容易引出新bug。
还需要继续学的东西:
-
设计模式。尤其策略模式、工厂模式,可以让代码更容易扩展。
-
异常处理和输入校验的优雅写法。别再System.exit(0)了。
-
单元测试。必须养成写测试的习惯,不能再靠手工点。
-
类图设计。写代码之前先画类图,想清楚类之间的关系,能省很多返工时间。
对课程的小建议:
1.可以在下一次迭代开始前给一个参考类图或设计思路,帮我们避免一些明显的结构坑(比如第三次作业里我注释掉的那段排序代码,如果有提示就会注意到)。
2.测试点里多加点非法输入的边界场景,比如货舱容量是0、货物重量是负数之类的,倒逼我们认真做好输入校验。
3.能不能在作业要求里明确加上“必须提供类图和复杂度分析截图”?这样大家提交之前就会主动审视自己的代码质量,而不只是盲目追求测试点全绿。
最后想说,三次作业虽然过程磕磕绊绊,第三次还没拿满分,但我真的体会到了“迭代开发”的味道——没有一次设计是完美的,都是在后续不断重构、不断踩坑、不断优化中变好的。以后写稍微复杂一点的程序,我会先问自己三个问题:这个类的责任单一吗?最复杂的方法能控制在10以内吗?加一个新功能要改多少旧代码?这三个问题,就是我这三次作业学到的最实在的东西。

浙公网安备 33010602011771号