Loading

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. 业务场景

模拟航空公司货运订单处理流程,涉及客户信息、货物信息、航班信息的录入,以及空运费计算和订单合法性校验。题目要求按照面向对象设计原则进行开发,重点考核单一职责原则、里氏代换原则、开闭原则以及合成复用原则。核心功能包括:

  1. 客户信息管理 :记录客户编号、姓名、电话和地址。

  2. 货物信息管理 :记录货物编号、名称、尺寸(长、宽、高)、重量,并计算体积重量和计费重量。

  3. 航班信息管理 :记录航班号、起飞机场、降落机场、航班日期和最大载重量。

  4. 订单信息管理 :记录订单编号、日期、发件人和收件人信息、所选航班及货物清单。

  5. 运费计算 :根据计费重量和费率计算每件货物的运费,并汇总订单总运费。

  6. 支付方式管理 :支持微信支付和支付宝支付。

2. 订单处理流程
  1. 输入解析:按顺序读取各类信息,使用ArrayList存储多件货物

  2. 载重量校验:累加所有货物计费重量,与航班最大载重量比较

  3. 运费计算:遍历货物列表,调用费率计算器计算单件运费并累加

  4. 输出格式化:按指定格式打印订单信息与货物明细

3. 输入输出规则
  • 输入顺序:客户信息 → 货物信息(多件)→ 航班信息 → 订单信息

  • 关键校验:若订单总重量 > 航班最大载重量,终止程序并提示错误

  • 输出格式:包含客户信息、订单详情、支付金额及货物明细,数值保留1位小数

代码分析

以下我的代码中在PowerDesigner中的UML图:

图片

依据类图可以看出我的类设计,下面是对于类设计的总结以及面向对象设计原则的体现:

类设计分析
1. 类结构与职责划分
类名 职责描述 设计原则体现
Cargo 封装货物属性(名称、尺寸、重量),计算体积重量与计费重量 单一职责原则
Flight 管理航班信息(航班号、起降城市、日期、最大载重量) 单一职责原则
Order 维护订单核心数据(客户、收发件人、航班、货物列表、支付方式) 单一职责原则
Customer 客户信息(继承自Person,新增客户编号) 继承与单一职责
Sender/Recipient 收发件人信息(继承自Person 里氏代换原则
RateCalculator接口 定义费率计算策略,StandardRateCalculator实现分段费率逻辑 开闭原则
Payment接口 定义支付行为,WeChatPayment/AlipayPayment实现具体支付方式 合成复用原则
Agent 协调订单处理全流程(计算总重量、运费、打印报表) 组合复用(依赖RateCalculator
2. 设计原则符合性分析
  1. 单一职责原则

    • Cargo仅负责货物相关计算,Flight仅管理航班信息,符合职责分离
  2. 里氏代换原则

    • Sender/Recipient作为Person子类,可无缝替换父类对象(如订单中的收发件人字段)
  3. 开闭原则

    • 新增费率策略(如促销费率)时,只需实现RateCalculator接口,无需修改现有代码
  4. 合成复用原则

    • Agent通过构造方法注入RateCalculator实例,而非继承,支持灵活替换费率计算策略

以下是在SourceMonitor中给的生成表格:

图片
SourceMonitor 代码度量分析报告 (Main.java)
一、基本指标解读
指标 数值 分析
Lines (代码行数) 498 代码量适中,包含完整业务逻辑实现(输入解析、对象建模、订单处理)。
Statements (语句数) 207 实际执行语句少封装
注释率 (2.6%) 偏低 需补充关键逻辑注释(如计费重量算法、分段费率规则),提升可维护性,可读性。
类与接口数 (15) 丰富 覆盖业务实体(CargoFlight)、策略接口(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代码数

  • 需添加更多注释量

踩坑心得

  1. 输入处理:多货物循环读取时,nextLine()next()混用易导致数据错位,需统一处理输入格式。

  2. 载重量校验:累加的是计费重量(非实际重量),此处若用错数据,直接导致校验逻辑错误。

  3. 输出对齐:货物明细的制表符(\t)需精确控制,确保列对齐,避免格式混乱,而不是使用空格来分离,这里我就是犯了这个错误。

第二次题目

原题呈现

image

输入格式:

按如下顺序分别输入客户信息、货物信息、航班信息以及订单信息。

输入格式
客户类型[可输入项: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    ...

设计与分析

需求分析

一、业务规则扩展
  1. 货物类型与费率分层

    新增普通/危险/加急货物类型,对应不同分段费率:

    • 普通货物:费率与首次题目一致(35/30/25/15)

    • 危险货物:低重量段费率显著提高(80/50/30/20)

    • 加急货物:全段费率高于普通货物(60/50/40/30)

    关键点:需通过多态实现不同货物类型的费率计算。

  2. 用户类型与折扣机制

    引入个人用户(9折)集团用户(8折),订单总运费需乘以对应折扣率。

    实现:抽象Customer类定义getDiscount()接口,子类Individual/Corporate分别实现具体折扣逻辑。

二、输入输出变化
  1. 新增输入字段

    • 客户类型(Individual/Corporate

    • 货物类型(Normal/Expedite/Dangerous

    • 支付方式新增Cash选项。

  2. 输出逻辑调整

    • 支付方式需匹配现金支付文案

    • 总运费计算包含用户折扣(如示例中总金额含折扣后为4360.0)。

代码分析

以下我的代码中在PowerDesigner中的UML图:

图片

依据类图可以看出我的类设计,下面是对于类设计的总结以及面向对象设计原则的体现:

一、代码结构差异
  1. 新增抽象类与继承体系

    • 首次: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; } // 实现具体折扣
    }
    
  2. 费率计算扩展

    • 首次:单一StandardRateCalculator处理固定费率

    • 迭代后:新增NormalRateCalculator/DangerousRateCalculator/ExpediteRateCalculator,实现RateCalculator接口,根据货物类型动态切换策略。

    public Agent(String cargoType) { // 根据货物类型注入对应策略
        if ("Dangerous".equals(cargoType)) {
            rateCalculator = new DangerousRateCalculator(); // 危险货物高费率策略
        }
    }
    
  3. 支付方式接口扩展

    • 首次:仅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)无需修改原有逻辑
新增DangerousRateCalculatorCashPayment,原有代码无需改动
合成复用原则 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,结构未显著恶化。

虽然说第二次迭代代码因业务逻辑扩展导致复杂度指标上升,但类设计更符合面向对象原则(新增类遵循单一职责,策略模式解耦费率计算)。

踩坑心得

  1. 用户折扣作用域:折扣需作用于订单总运费(而非单件),注意计算顺序避免逻辑错误。

  2. 支付方式映射:新增CashPayment后,输出文案需与输入项严格对应(如“Cash”映射“现金支付”)。

改进

第二题代码对第一题的改进

  1. 多态与策略模式的应用

    • 引入RateCalculator接口及Normal/Dangerous/ExpediteRateCalculator实现类,通过货物类型动态切换费率策略,解耦费率逻辑,符合开闭原则

    • 抽象Customer类定义getDiscount()接口,Individual/Corporate子类实现具体折扣,通过多态计算订单总运费,增强扩展性。

  2. 依赖倒转原则的实践

    • 高层模块(如Agent)依赖RateCalculator/Payment接口而非具体类,例如Agent通过构造方法注入费率计算器,支持运行时动态替换策略。
  3. 业务逻辑的分层解耦

    • 分离用户类型、货物类型、支付方式的处理逻辑,新增Individual/Corporate用户子类和CashPayment支付类,单一职责更清晰。
  4. 输入类型的扩展性提升

    • 支持新增货物类型(如Dangerous)和支付方式(如Cash)时,只需实现对应接口,无需修改原有代码,符合开闭原则

第二题代码仍需改进的地方

  1. main方法复杂度偏高

    • main方法承担过多输入解析逻辑(客户/货物/支付类型判断),圈复杂度达9。
  2. 注释不足

    • 核心逻辑(如费率计算策略、折扣应用规则)缺少注释,影响可维护性。
  3. 代码重复与冗余

    • 用户类型和支付类型的if-else判断存在重复逻辑(如customerTypepayment的分支处理),可通过工厂模式(如PaymentFactory)进一步解耦。

总结

经过这两次的代码迭代,我感受最深的就是一定要做好需求分析,什么地方可以是扩展的,什么地方可以再加东西的,一定要在写代码之前就搞清楚,我这里就是一开始就把费率计算作为接口,然后在第二次写代码的时候没有改动我原来的代码很多东西,而是在这上面加代码,这种感觉确实不错。

这些让我明白了设计原则不是书上枯燥的东西,而是我们通往编程高手的捷径。

(PS : 开闭原则真的很好用 :开闭原则的魅力——好的设计不是预测所有变化,而是为变化留下「接口」)

posted @ 2025-05-23 20:54  anymore-stay  阅读(50)  评论(0)    收藏  举报

Loading