一、前言
OOP学习的第一阶段结束了,在第一阶段学习中,我们学习了OOP的类的定义与使用,了解了封装性和解耦合的重要性。
在前三次题目集中,主要是围绕着航空器的货物装载问题,来保证飞行安全。这三次题目集,一次次迭代,从易到难,很能锻炼我们对封装性的理解,对MVC模式的使用。而且以飞行安全为题还具有学校特色。
总的来说,题目难度的梯度比较合理,知识点较为简单:类与对象,封装等。
二、设计与分析
1、 OOP题目集1
本题题目背景是:在航空业中,航班起飞前需要进行严格的“配载”,即计算飞机的旅客、货物、行李的重量分布,计算出飞机的重心位置,以确保飞行安全。
题目要求设计一个基础的航班货运配载模块。
类图

其中Main是主类,Flight是飞机类,Cargo是货物类,GrandCrew是地勤人员类,CargoSorter是货物排序的工具类,LoadManifest是装载货物的类,CalculateTotalWeight是计算货物总重的工具类。
如图,在设计过程中尽量使用依赖关系来降低各个部分的耦合性,除了题目规定的关联关系之外,都在努力使用依赖进行解耦合。
以下是题目要求的部分类图:

复杂度分析

可以看到,在主方法里有23条语句,单方法承担的比较重的负担。
而且块深度分布较深,嵌套过深,逻辑易混乱。
总结
在整个编码过程中,会尽量使用封装来进行分块,但是在每个方法中嵌入了过多的语句,使代码逻辑易于混乱。在以后的编码过程中尽量使用MVC模式进行构建。
2、OOP题目集2
本题题目背景是:航空公司要求进一步细化配载管理。飞机不再只有一个货舱,而是分为多个货舱(如前舱、后舱等)。每个货舱有独立的最大载重和固定数量的装载位置(行列网格)。地勤人员需要按照货物重量从高到低的顺序,将每件货物选择放入某个货舱,系统需实时检查该货舱是否超载。同时,系统需要记录航班整体的最大起飞重量和最大业载重量,并判断整体是否超载。
题目要求在题目集1的基础上扩展一个航班货运配载模块。
在设计方面,必须遵守SRP。
类图

其中Main是主类,Position是位置类,Cargo是货物类,LoadDispatcher是一个调度类(对货物进行排序和查找),CargoCompartment是一个货舱类,Package是一个封装Cargo和目标舱的包类,Flight是一个飞机类,InputValidator是输入校验类,Input类用于管理输入部分,Output用于输出管理。
如图,为了实现题目要求的SRP,我将输入输出从Main类中分离出来,封装成单独的Input、Output类,让Main类只负责逻辑运行,不参与输入与输出的过程。
并且每个类除了他们改知道的部分外都是使用依赖进行解耦合,增强代码可复用性。
在类设计中,也遵循了以下要求
在CargoCompartment类中要聚合Cargo,Position类,CargoCompartment 创建时内部生成 Position 列表
以上操作使代码更具有条理,符合题目要求。
复杂度分析

如图,可以看到,最大圈复杂度为4,平均复杂度为1.44,代码的复杂度处于安全区间。
代码块的最大深度为5,平均深度为1.71,说明嵌套逻辑较少,处于合理范围。
整体复杂度较题目集1改善较大,有所进步。
结构方面,9个类共包含44个方法,平均每个类4.89个方法,方法平均语句占2.23行,结构设计较为合理,符合SRP。
但是在LoadDispatcher类中的 sortCargos() 方法复杂度较高,由于采用的是冒泡排序,有多重for循环嵌套,导致复杂度较高。
总结
在整个编码过程中,尽量扣紧SRP要求,降低代码的耦合性和复杂度。相较题目集1,在代码逻辑方面有所改善,并了解到SRP对代码复杂度的重要性。
3、OOP题目集3
本题题目背景是:但在真实的航空业务中,飞机的装载不仅包含货物,还包含旅客及其随身行李。更关键的是,任何装载都必须经过严格的“载重平衡”计算,得出飞机的重心位置,以确保飞行安全。在航空器配载中,所有装载项(旅客、前舱货物、后舱货物)都有其对应的力臂。重量乘以力臂等于力矩。所有力矩之和除以总重量,即为飞机的实际重心。为了统一标准,实际重心需要换算为占平均空气动力弦(MAC)的百分比。
题目要求在前两次题目集的基础上新增一个计算工具类,为了计算飞机的实际重心,保证“载重平衡”,来保障飞行安全。
在设计方面,要遵循题目给定的关系,在类设计方面,使用SRP,并且严禁使用继承和多态。
类图

其中Main是主类,Position是位置类,Cargo是货物类,LoadDispatcher是一个调度类(对货物进行排序和查找),CargoCompartment是一个货舱类,Package是一个封装Cargo和目标舱的包类,Flight是一个飞机类,InputValidator是输入校验类,Input类用于管理输入部分,Output用于输出管理,Passenger是乘客类,Luggage是行李类,WeightBalanceCalculator是一个计算工具类。
在类设计中,已遵循以下要求:
| 新增类 | 职责划分 | 类间关系设计建议 |
|---|---|---|
| Passenger | 仅负责管理旅客姓名及计算旅客总重量(含标准体重) | 与 Luggage 是组合关系。旅客登机必带行李(可为0kg),行李对象必须在 Passenger 构造器内部 new 出来,不对外暴露修改 |
| Luggage | 仅负责记录行李重量 | 作为 Passenger 的组成部分,体现类的细粒度拆分 |
| WeightBalanceCalculator | 纯计算工具类。仅负责根据航空力学公式计算总重量、总力矩、重心及百分比。 | 与 Flight 是依赖关系。该类不应有 Flight 的成员变量,必须将 Flight 对象作为 generateLoadSheet 方法的参数传入 |
| InputValidator (优化) | 剥离所有控制台的异常处理逻辑,提供统一的获取整数、浮点数的静态方法 | 与主流程是依赖关系 |
原有的 Flight 类需增加
List<Passenger>属性,体现关联关系。原有的LoadDispatcher(排序)在本次生成报告时需被内部调用。
复杂度分析

如图,可以看到,平均圈复杂度为1.65,表明代码逻辑整体清晰,分支结构简单,符合可维护性要求,最大圈复杂度为6,在InputValidator类的 readIntRange() 方法中,复杂度较高。
最大块深度为6,与最大复杂度一致,该方法由于含多重嵌套 if 判断,复杂度和块深度较大,平均块深度为1.95,整体结构较为合理。
结构方面,代码整体含7个类,平均每个类4个方法,符合SRP,平均语句数为2.93,较为细化。
总结
在整个编码过程中,尽量扣紧SRP要求,降低代码的耦合性和复杂度。相较题目集1, 2,在代码逻辑方面有所改善,并了解到SRP对代码复杂度的重要性,在进行新类的添加时,感觉较为方便,说明各个模块的耦合性较低。
三、踩坑心得
在写代码的过程中哪有不踩坑的捏,在完成三次题目集的过程中,犯了一些错误,踩了一些坑,现在复盘的时候回头看看。
1. 第一次题目集
在第一次提交时,没有关注到超载部分,只注重于输出结果与样例一致。这种过分执着于 “面向结果编程” 的错误,导致忽视了在测试中的边界测试。
而在改代码时,只注意了测试点中占比较大的超载部分测试,又忽视了排序的实现,结果在提交时才注意到这个部分。
增加排序时,由于不是很熟练,再加上赶着完成作业的急切心理,结果连在上学期学C语言时十分熟练的排序都写错的好多遍,包括冒泡、选择等排序方法。
在提交成功后,显示答案正确的红字出现,松了一口气的同时,我又在想我写的这个有没有能改进的地方,回忆起上课时说的 SRP我感觉还能再改改,但是改完又开始重复这个问题。也许是完成作业了,那种急切感没了,脑子就冷静下来了。一直到在床上睡觉都在想这个问题。
第二天,老师说,只以第一次提交正确为准,我意识到这种急切感不对。
因此在后两次题目集中我会尽量优化好后才会提交,将每一次提交当成最后一次,或许这样能提高我的代码质量吧。
2. 第二次题目集
由于有第一次题目集的教训,我在提交时会尽量考虑全面,因此在初次提交时的正确性要显著高于第一次。嘻嘻 由于第二次题目集没有了测试点的提示,所以只能自己在考虑时尽量更全面。但是在一次次更改中,发现自己的代码不是太符合题目要求的 SRP 。因此在不知道更改哪里时,决定重构一下代码,使其更符合SRP。
在一次次更改过程中,试着改过判断超载的判断,改过输出。而且还在思考 Position 类的作用,毕竟使用 Position 进行列表限制时,会导致和题目示例的输出不一致,所以到最后我都没有明白Position类的作用。
其实在最后发现是输出中的中文输出错误,真是 诶,没招了,好在提交正确了。
因此在后续过程中试图注意这部分,但是根据第三次的结果来看,还是没有长记性。
3. 第三次题目集
在完成第三次题目集的过程中,我有一个比较疑惑的地方,题目明明要求排序来着,但是我如果调用 LoadDispatcher 中的排序就会错。
好了说回第三次题目集,初次提交时也是吃了第一次的教训 (在后面看来并没有嘛) ,检查了感觉是比较符合自己检测就提交了。由于整体框架是继承自题目集2的,因为在写第二次的时候重构了来着,所以在添加新类时十分方便,输入和输出也只需要更改Input和Output类 (感谢第二次中决定重构的自己) 。
说来也是,由于输出部分太多,导致自己没有看清楚,题目的样例也只有正确的一个,因此输出错误答案时也没有太过于小心,导致查看了很多遍。
在实现以下需求时,由于没有注意到带范围的值只有在超出范围才输出第二种,小于0还是第一种输出,在这个方面也纠结了好久。
所有检测到输入的数值如果为负数,则输出
数值不能为负数!,程序停止运行
如果检测到输入的数值超过应有范围,则提示输入必须在 最小值 到 最大值 之间!,程序停止运行
而实现以下需求时,由于不了解当前载重要不要停止,载重值是装前还是装后的,也折磨了一段时间。
前舱装载量超过最大装载量,提示:!!! 警告:[1]剩余容量不足(当前载重_载重量值/最大载重量_kg),请重新分配或减轻重量!
后舱装载量超过最大装载量,提示:!!! 警告:[2]剩余容量不足(当前载重_载重量值/最大载重量_kg),请重新分配或减轻重量!
四、总结和建议
经过这三次题目集,对面向对象编程的封装相关部分有了些许了解。
我的代码经三次迭代,较第一次相比,代码的 SRP 部分更合理了,并且由于输入,输出和对象的组装部分从 main 函数分离出来,使其在扩展方面更好,可以持续改进这段代码。
在OOP第一阶段的三次题目集中,感觉对类的封装部分了解较好,但是在类的职责分配方面不是很擅长。由于体会到了 SRP 也就是解耦合在迭代过程中的好处,感觉会在学习中持续尝试 SRP 等解耦合的方法,进一步了解面向对象设计。
在下一阶段应该就是 继承 和 多态 的使用,希望体会继承和多态在面向对象设计中的强大之处。
以及 SOLID 等原则在设计中的妙处。
