Object-Oriented Programming 航空货运管理系统
OOP 第二次大作业之航空货运管理系统
题目来自 NCHU( PTA 平台)
前言
这次大作业重点考核面向对象设计原则,主要包括单一职责原则、里氏代换原则、开闭原则以及合成复用原则。
这次大作业中有两次题目集,从总体分析是:
-
第一次题目为一种费率的计算,并且要区分多种类之间的关系
-
第二次题目在第一次之上添加了不同种费率的计算,不同的客户类型
第一次题目
原题呈现
输入格式:
按如下顺序分别输入客户信息、货物信息、航班信息以及订单信息。
输入格式
客户编号
客户姓名
客户电话
客户地址
运送货物数量
[货物编号
货物名称
货物宽度
货物长度
货物高度
货物重量
]//[]内的内容输入次数取决于“运送货物数量”,输入不包含“[]”
航班号
航班起飞机场
航班降落机场
航班日期(格式为YYYY-MM-DD)
航班最大载重量
订单编号
订单日期(格式为YYYY-MM-DD)
发件人地址
发件人姓名
发件人电话
收件人地址
收件人姓名
收件人电话
输出格式
-
如果订单中货物重量超过航班剩余载重量,程序输出
The flight with flight number:航班号 has exceeded its load capacity and cannot carry the order.,程序终止运行。 -
如果航班载重量可以承接该订单,输出如下:
输出格式
客户:姓名(电话)订单信息如下:
-----------------------------------------
航班号:
订单号:
订单日期:
发件人姓名:
发件人电话:
发件人地址:
收件人姓名:
收件人电话:
收件人地址:
订单总重量(kg):
微信支付金额:
货物明细如下:
-----------------------------------------
明细编号 货物名称 计费重量 计费费率 应交运费
1 ...
2 ...
设计与分析
需求分析
1. 业务场景
模拟航空公司货运订单处理流程,涉及客户信息、货物信息、航班信息的录入,以及空运费计算和订单合法性校验。题目要求按照面向对象设计原则进行开发,重点考核单一职责原则、里氏代换原则、开闭原则以及合成复用原则。核心功能包括:
-
客户信息管理 :记录客户编号、姓名、电话和地址。
-
货物信息管理 :记录货物编号、名称、尺寸(长、宽、高)、重量,并计算体积重量和计费重量。
-
航班信息管理 :记录航班号、起飞机场、降落机场、航班日期和最大载重量。
-
订单信息管理 :记录订单编号、日期、发件人和收件人信息、所选航班及货物清单。
-
运费计算 :根据计费重量和费率计算每件货物的运费,并汇总订单总运费。
-
支付方式管理 :支持微信支付和支付宝支付。
2. 订单处理流程
-
输入解析:按顺序读取各类信息,使用
ArrayList存储多件货物 -
载重量校验:累加所有货物计费重量,与航班最大载重量比较
-
运费计算:遍历货物列表,调用费率计算器计算单件运费并累加
-
输出格式化:按指定格式打印订单信息与货物明细
3. 输入输出规则
-
输入顺序:客户信息 → 货物信息(多件)→ 航班信息 → 订单信息
-
关键校验:若订单总重量 > 航班最大载重量,终止程序并提示错误
-
输出格式:包含客户信息、订单详情、支付金额及货物明细,数值保留1位小数
代码分析
以下我的代码中在PowerDesigner中的UML图:
依据类图可以看出我的类设计,下面是对于类设计的总结以及面向对象设计原则的体现:
类设计分析
1. 类结构与职责划分
| 类名 | 职责描述 | 设计原则体现 |
|---|---|---|
Cargo |
封装货物属性(名称、尺寸、重量),计算体积重量与计费重量 | 单一职责原则 |
Flight |
管理航班信息(航班号、起降城市、日期、最大载重量) | 单一职责原则 |
Order |
维护订单核心数据(客户、收发件人、航班、货物列表、支付方式) | 单一职责原则 |
Customer |
客户信息(继承自Person,新增客户编号) |
继承与单一职责 |
Sender/Recipient |
收发件人信息(继承自Person) |
里氏代换原则 |
RateCalculator接口 |
定义费率计算策略,StandardRateCalculator实现分段费率逻辑 |
开闭原则 |
Payment接口 |
定义支付行为,WeChatPayment/AlipayPayment实现具体支付方式 |
合成复用原则 |
Agent |
协调订单处理全流程(计算总重量、运费、打印报表) | 组合复用(依赖RateCalculator) |
2. 设计原则符合性分析
-
单一职责原则:
Cargo仅负责货物相关计算,Flight仅管理航班信息,符合职责分离
-
里氏代换原则:
Sender/Recipient作为Person子类,可无缝替换父类对象(如订单中的收发件人字段)
-
开闭原则:
- 新增费率策略(如促销费率)时,只需实现
RateCalculator接口,无需修改现有代码
- 新增费率策略(如促销费率)时,只需实现
-
合成复用原则:
Agent通过构造方法注入RateCalculator实例,而非继承,支持灵活替换费率计算策略
以下是在SourceMonitor中给的生成表格:
SourceMonitor 代码度量分析报告 (Main.java)
一、基本指标解读
| 指标 | 数值 | 分析 |
|---|---|---|
| Lines (代码行数) | 498 | 代码量适中,包含完整业务逻辑实现(输入解析、对象建模、订单处理)。 |
| Statements (语句数) | 207 | 实际执行语句少封装 |
| 注释率 (2.6%) | 偏低 | 需补充关键逻辑注释(如计费重量算法、分段费率规则),提升可维护性,可读性。 |
| 类与接口数 (15) | 丰富 | 覆盖业务实体(Cargo、Flight)、策略接口(RateCalculator)、代理类(Agent),符合领域驱动设计。 |
| 平均每类方法数 (7.07) | 合理 | 避免“臃肿类”,职责分布均匀(如Cargo专注货物计算,Agent处理订单流程)。 |
| 平均每方法语句数 (0.34) | 极低 | 多数方法为 getter/setter,核心逻辑集中在Agent.Print()和Main.main(),符合单一职责。 |
二、复杂度与结构分析
| 指标 | 数值 | 分析 |
|---|---|---|
| 最大圈复杂度 (2) | 极低 | 代码逻辑分支少(仅订单载重量校验1处条件),无复杂嵌套。 |
| 最大块深度 (3) | 合理 | 代码嵌套层次浅(如main方法的输入处理为3层嵌套),符合“扁平化”设计原则。 |
| 最复杂方法 (Main.main()) | 复杂度2 | 负责输入解析与对象初始化,逻辑线性(无多重分支),可通过提取子方法(如readFlightInfo())简化。 |
三、方法调用与块分布
| 指标 | 数值 | 分析 |
|---|---|---|
| 方法调用数 (39) | 适中 | 体现类间协作(如Agent调用RateCalculator计算费率),符合策略模式的解耦设计。 |
| 块深度分布 | 0-3层 | 88%语句位于深度 ≤ 2(17 + 105 + 139 = 261,占比88%),结构清晰,调试方便(如货物列表读取的循环在深度1)。 |
四、可改进之处
-
优化
main方法,减少main代码数 -
需添加更多注释量
踩坑心得
-
输入处理:多货物循环读取时,
nextLine()与next()混用易导致数据错位,需统一处理输入格式。 -
载重量校验:累加的是计费重量(非实际重量),此处若用错数据,直接导致校验逻辑错误。
-
输出对齐:货物明细的制表符(
\t)需精确控制,确保列对齐,避免格式混乱,而不是使用空格来分离,这里我就是犯了这个错误。
第二次题目
原题呈现

输入格式:
按如下顺序分别输入客户信息、货物信息、航班信息以及订单信息。
输入格式
客户类型[可输入项:Individual/Corporate]
客户编号
客户姓名
客户电话
客户地址
货物类型[可输入项:Normal/Expedite/Dangerous]
运送货物数量
[货物编号
货物名称
货物宽度
货物长度
货物高度
货物重量
]//[]内的内容输入次数取决于“运送货物数量”,输入不包含“[]”
航班号
航班起飞机场
航班降落机场
航班日期(格式为YYYY-MM-DD)
航班最大载重量
订单编号
订单日期(格式为YYYY-MM-DD)
发件人地址
发件人姓名
发件人电话
收件人地址
收件人姓名
收件人电话
支付方式[可输入项:Wechat/ALiPay/Cash]
输出格式
-
如果订单中货物重量超过航班剩余载重量,程序输出
The flight with flight number:航班号 has exceeded its load capacity and cannot carry the order.,程序终止运行。 -
如果航班载重量可以承接该订单,输出如下:
输出格式
客户:姓名(电话)订单信息如下:
-----------------------------------------
航班号:
订单号:
订单日期:
发件人姓名:
发件人电话:
发件人地址:
收件人姓名:
收件人电话:
收件人地址:
订单总重量(kg):
[微信/支付宝/现金]支付金额:
货物明细如下:
-----------------------------------------
明细编号 货物名称 计费重量 计费费率 应交运费
1 ...
2 ...
设计与分析
需求分析
一、业务规则扩展
-
货物类型与费率分层
新增普通/危险/加急货物类型,对应不同分段费率:
-
普通货物:费率与首次题目一致(35/30/25/15)
-
危险货物:低重量段费率显著提高(80/50/30/20)
-
加急货物:全段费率高于普通货物(60/50/40/30)
关键点:需通过多态实现不同货物类型的费率计算。
-
-
用户类型与折扣机制
引入个人用户(9折)和集团用户(8折),订单总运费需乘以对应折扣率。
实现:抽象
Customer类定义getDiscount()接口,子类Individual/Corporate分别实现具体折扣逻辑。
二、输入输出变化
-
新增输入字段
-
客户类型(
Individual/Corporate) -
货物类型(
Normal/Expedite/Dangerous) -
支付方式新增
Cash选项。
-
-
输出逻辑调整
-
支付方式需匹配
现金支付文案 -
总运费计算包含用户折扣(如示例中总金额含折扣后为4360.0)。
-
代码分析
以下我的代码中在PowerDesigner中的UML图:
依据类图可以看出我的类设计,下面是对于类设计的总结以及面向对象设计原则的体现:
一、代码结构差异
-
新增抽象类与继承体系
-
首次:
Customer为普通类,无折扣逻辑 -
迭代后:定义抽象类
Customer,派生出Individual(个人用户,9折)和Corporate(集团用户,8折),通过抽象方法getDiscount()实现多态。
abstract class Customer { abstract public double getDiscount(); // 抽象方法定义折扣行为 } class Individual extends Customer { private double discount = 0.9; @Override public double getDiscount() { return discount; } // 实现具体折扣 } -
-
费率计算扩展
-
首次:单一
StandardRateCalculator处理固定费率 -
迭代后:新增
NormalRateCalculator/DangerousRateCalculator/ExpediteRateCalculator,实现RateCalculator接口,根据货物类型动态切换策略。
public Agent(String cargoType) { // 根据货物类型注入对应策略 if ("Dangerous".equals(cargoType)) { rateCalculator = new DangerousRateCalculator(); // 危险货物高费率策略 } } -
-
支付方式接口扩展
-
首次:仅
WeChatPayment/AlipayPayment -
迭代后:新增
CashPayment,通过Payment接口统一管理,符合开闭原则。
if (payment.equals("Cash")) { paymentMethod = new CashPayment(); // 新增现金支付实现类 } -
二、面向对象设计原则体现
| 原则 | 具体实现 | 代码示例 |
|---|---|---|
| 单一职责原则 | - Cargo仅负责货物属性与计费重量计算- Flight专注航班信息管理 |
Cargo类仅包含calculateVolumeWeight()等货物相关方法,无其他职责 |
| 里氏代换原则 | Individual/Corporate可无缝替换Customer,确保多态调用一致性 |
Order类中Customer字段接受任意子类对象:private Customer customer; |
| 开闭原则 | - 新增货物类型(如Dangerous)只需实现RateCalculator接口- 新增支付方式(如 Cash)无需修改原有逻辑 |
新增DangerousRateCalculator和CashPayment,原有代码无需改动 |
| 合成复用原则 | Agent通过组合RateCalculator接口实现费率计算,而非继承 |
private RateCalculator rateCalculator;(组合关系而非继承) |
| 依赖倒转原则 | 高层模块(Agent/Order)依赖抽象接口(RateCalculator/Payment) |
Agent构造方法参数为RateCalculator接口,而非具体实现类 |
以下是在SourceMonitor中给的生成表格:
SourceMonitor 数据对比分析(第二次迭代 vs 首次)
| 指标 | 首次迭代 | 第二次迭代 | 差异分析 |
|---|---|---|---|
| 代码行数 (Lines) | 498 | 641 (+28.7%) | 新增5个类(Individual/Corporate/3种费率计算器)及类型判断逻辑,代码量合理增长。 |
| 语句数 (Statements) | 207 | 276 (+33.3%) | 新增用户类型/货物类型/支付方式的条件判断(如if-else分支),业务逻辑复杂度提升。 |
| 分支语句占比 | 5.3% | 10.5% (+98%) | 新增customerType/cargoType/payment的类型校验逻辑,分支数量翻倍,符合需求扩展。 |
| 方法调用数 | 39 | 53 (+35.9%) | 多态调用增加(如rateCalculator.calculateRate()),类间协作更频繁,体现策略模式应用。 |
| 类与接口数 | 15 | 20 (+33.3%) | 新增5个类(2个用户子类、3个费率策略类),符合“开闭原则”的扩展方式。 |
| 平均每类方法数 | 7.07 | 6.40 (-9.5%) | 新增类更聚焦单一职责(如DangerousRateCalculator仅处理危险货物费率),方法数分布更均衡。 |
| 最大复杂度 (Main.main()) | 2 | 9 (+350%) | main方法新增4组类型判断(客户/货物/支付类型+非法输入提示),导致圈复杂度骤升。 |
| 块深度分布(深度≥2语句数) | 159(2+3层) | 209(2+3层) | 条件判断嵌套增加(如用户类型与支付方式的双重分支),但最大深度仍为3,结构未显著恶化。 |
虽然说第二次迭代代码因业务逻辑扩展导致复杂度指标上升,但类设计更符合面向对象原则(新增类遵循单一职责,策略模式解耦费率计算)。
踩坑心得
-
用户折扣作用域:折扣需作用于订单总运费(而非单件),注意计算顺序避免逻辑错误。
-
支付方式映射:新增
CashPayment后,输出文案需与输入项严格对应(如“Cash”映射“现金支付”)。
改进
第二题代码对第一题的改进
-
多态与策略模式的应用
-
引入
RateCalculator接口及Normal/Dangerous/ExpediteRateCalculator实现类,通过货物类型动态切换费率策略,解耦费率逻辑,符合开闭原则。 -
抽象
Customer类定义getDiscount()接口,Individual/Corporate子类实现具体折扣,通过多态计算订单总运费,增强扩展性。
-
-
依赖倒转原则的实践
- 高层模块(如
Agent)依赖RateCalculator/Payment接口而非具体类,例如Agent通过构造方法注入费率计算器,支持运行时动态替换策略。
- 高层模块(如
-
业务逻辑的分层解耦
- 分离用户类型、货物类型、支付方式的处理逻辑,新增
Individual/Corporate用户子类和CashPayment支付类,单一职责更清晰。
- 分离用户类型、货物类型、支付方式的处理逻辑,新增
-
输入类型的扩展性提升
- 支持新增货物类型(如
Dangerous)和支付方式(如Cash)时,只需实现对应接口,无需修改原有代码,符合开闭原则。
- 支持新增货物类型(如
第二题代码仍需改进的地方
-
main方法复杂度偏高main方法承担过多输入解析逻辑(客户/货物/支付类型判断),圈复杂度达9。
-
注释不足
- 核心逻辑(如费率计算策略、折扣应用规则)缺少注释,影响可维护性。
-
代码重复与冗余
- 用户类型和支付类型的
if-else判断存在重复逻辑(如customerType和payment的分支处理),可通过工厂模式(如PaymentFactory)进一步解耦。
- 用户类型和支付类型的
总结
经过这两次的代码迭代,我感受最深的就是一定要做好需求分析,什么地方可以是扩展的,什么地方可以再加东西的,一定要在写代码之前就搞清楚,我这里就是一开始就把费率计算作为接口,然后在第二次写代码的时候没有改动我原来的代码很多东西,而是在这上面加代码,这种感觉确实不错。
这些让我明白了设计原则不是书上枯燥的东西,而是我们通往编程高手的捷径。
(PS : 开闭原则真的很好用 :开闭原则的魅力——好的设计不是预测所有变化,而是为变化留下「接口」)

浙公网安备 33010602011771号