航空货运管理系统:两次迭代的演进与思考

前言

第8,9次题目集围绕“航空货运管理系统”这一主题进行了项目分析与代码编写。题目集要求构建一个能够处理客户信息、货物信息、航班信息以及订单管理的简易系统。从第一次题目集的基础功能实现,到第二次题目集引入了客户类型、货物类型、支付方式、折扣计算等细节。

设计与分析

第8次题目集源码分析:

第一次提交的源码结构相对简单直观。它包含了 Main、Customer、Cargo、Flight 和 Order 五个类。

Customer 类: 存储客户的基本信息(ID、姓名、电话、地址)。属性私有,通过构造方法初始化,提供公共的 getter 方法。

Cargo 类: 存储货物的基本信息(ID、名称、长、宽、高、重量)。它内部实现了计费重量 (billingWeight)、费率 (rate) 和费用 (fee) 的计算逻辑。计费重量取体积重量(长高/6000)和实际重量的最大值。费率根据计费重量分段计算。这部分计算逻辑直接耦合在 Cargo 类中。提供了 getter 方法获取这些计算结果。

Flight 类: 存储航班信息(航班号、出发/到达机场、日期、最大载重)以及当前载重 (currentLoad)。提供了检查是否能添加载重 (canAddLoad) 和添加载重 (addLoad) 的方法,但 addLoad 在容量不足时抛出异常,实际在 Main 中通过 checkFlightCapacity 提前判断避免了异常。

Order 类: 核心类之一,整合了Customer、Flight 以及通过 Cargo[] cargos 数组存储的多个 Cargo。存储订单本身的详细信息(ID、日期、发件人/收件人信息)。提供了添加货物 (addCargo)、计算总重量 (getTotalWeight,计算的是所有货物的计费重量之和)和总费用 (getTotalFee,计算的是所有货物的费用之和)的方法。同样包含检查航班载重 (checkFlightCapacity) 和确认订单 (confirmOrder,将订单总重量加到航班当前载重)的方法。最后,printOrderInfo 方法负责格式化输出订单信息。

Main 类: 负责从标准输入读取所有数据,创建各个类的对象,构建订单,进行载重检查,确认订单是否超载,最后打印订单信息。使用了 Scanner 进行输入处理,并进行了必要的类型转换。

 

设计分析总结:。业务逻辑(如计费计算、载重检查)分散在 Cargo、Flight 和 Order 类中。如果使用 PowerDesigner 建模,会看到 Customer, Cargo, Flight 与 Order 之间存在关联关系,特别是 Order 到 Customer 和 Flight 是“一对一”的关联,到 Cargo 是“一对多”的关联(体现在数组上)。SourceMonitor 报告可能会显示每个类的方法数量、代码行数等,Order 类和 Main 类的复杂度相对较高,因为它们包含了主要的业务流程和数据处理逻辑。

第9次题目集源码分析:第二次在第一次的基础上进行了显著的改进和扩展。主要的类结构依然是 Main、Customer、Cargo、Flight 和 Order,但内部实现和类之间的协作方式有所变化。

枚举类型 (enum) 的引入: 新增了 CustomerType (Individual/Corporate), CargoType (Normal/Expedite/Dangerous), PayType (Wechat/ALiPay/Cash) 三个枚举。这极大地增强了代码的可读性和可维护性,用具名的常量代替了字符串或数字来表示类型,避免了潜在的拼写错误,并使代码意图更清晰。PayType 枚举还包含了显示名称 (display),方便输出。

Customer 类: 新增了 type 属性 (CustomerType),并在构造方法中初始化。其他部分与第一次类似。

Cargo 类: 新增了 type 属性 (CargoType)。计费重量计算方式不变。费率计算 (getRate) 现在根据 CargoType 来决定,而不是简单的根据计费重量分段,这使得费率计算更符合实际业务场景。新增了 getRawFee() 方法计算单件货物的原始费用(计费重量 * 费率)。

Flight 类: 与第一次类似,但移除了 currentLoad, canAddLoad, addLoad 方法。最大载重 (maxLoad) 保留。载重检查逻辑现在完全由 Order 类负责,这更符合职责分离的原则——订单应该知道如何检查自己是否符合航班的载重要求,而不是航班自己管理载重变化。

Order 类: 变化最大。

使用 List<Cargo> cargoList 代替了 Cargo[] cargos 数组,使得处理可变数量的货物更加灵活方便,无需预先知道货物数量。

新增 payType 属性 (PayType)。

引入了折扣计算逻辑 (getDiscount):根据 CustomerType 返回不同的折扣率(个人客户 0.9,公司客户 0.8)。

新增 getTotalRawFee() 方法计算订单中所有货物的原始费用总和(不含折扣)。

新增 getTotalFeeWithDiscount() 方法计算应用折扣后的最终订单总费用。

checkFlightCapacity() 方法现在只调用 getTotalWeight() 并与 flight.getMaxLoad() 进行比较。

移除了 confirmOrder() 方法,因为航班不再负责管理载重变化(虽然实际业务中航班肯定需要更新载重信息,但在这个系统设计中,简化了航班的职责,将重点放在订单处理上)。

printOrderInfo() 方法更新,输出时使用 PayType 的 display,输出订单总费用时使用 getTotalFeeWithDiscount(),并且在货物明细中输出的是单件货物的原始费用 (getRawFee()) 而不是最终折扣后的费用(这可能是业务要求或设计上的选择)。

Main 类: 使用了 .trim() 移除空白符,避免了因输入格式问题导致的错误。读取数据时,增加了对客户类型、货物类型、支付方式的读取和枚举转换。创建 Order 对象时,传递了 cargoList 和 payType。流程与第一次类似,但移除了订单确认步骤(因为 Flight 类不再有 addLoad 方法)。

类图:

 

 

设计分析总结 (第二次):引入了枚举增强类型安全和可读性。使用 List 提升了代码的灵活性。职责分配更合理(如载重检查由 Order 负责)。引入了更贴近实际的业务逻辑(客户类型折扣、货物类型费率)。费用计算逻辑被细化为原始费用和折扣后费用,并清晰地在代码中体现。PowerDesigner 类图会更复杂,除了原有的关联外,Order 与 PayType 有关联,Cargo 与 CargoType 有关联,Customer 与 CustomerType 有关联。Order 到 Cargo 的关联 cardinality 更清晰地表现为“一对零或多”(使用 List)。SourceMonitor 报告可能会显示类的数量增加(枚举类),Order 类和 Cargo 类的复杂度略有增加(因为增加了类型判断和不同的计算逻辑),但 Main 类的复杂度可能相对稳定或略有降低(因为部分逻辑转移到了 Order 类)。两次迭代的演进:从第一次到第二次,我们看到了代码从基本功能实现向更贴近实际业务需求的演进。引入枚举是重要的改进,提高了代码的健壮性和可读性。使用 List 替代数组是面向对象设计中更常见的处理集合的方式。费用计算逻辑的细化体现了对业务规则更深入的理解。这种迭代开发模式是实际软件工程中非常常见的,通过不断地接收新的需求并改进现有设计来实现更完善的功能。采坑心得在两次题目集的编码过程中,我们可能会遇到各种各样的问题,以下是一些可能的“坑”及我的心得体会:

输入处理的鲁棒性不足: 第一次代码在 Scanner 读取后直接进行类型转换 (Integer.parseInt, Double.parseDouble)。如果用户输入了非数字字符或者多余的空格,程序会抛出 NumberFormatException。在第二次代码中,引入 .trim() 是一个很好的改进,它能去除输入字符串前后的空白,减少因用户不规范输入导致的问题。但是,对于非数字输入的完整错误处理(如使用 try-catch 块)在提供的代码中仍然缺失。

心得: 永远不要相信用户的输入是完全规范的。在进行类型转换前,总是应该对输入字符串进行检查或使用异常处理机制,提高程序的健壮性。

数组的局限性: 第一次代码使用固定大小的 Cargo[] cargos = new Cargo[cargoCount]; 数组。虽然题目已知货物数量 cargoCount,但在实际开发中,如果货物数量是动态变化的,使用数组会非常不便,需要提前确定大小,或者在运行时复制到更大的数组中。

心得: 对于数量不确定或可能变化的集合,优先考虑使用 ArrayList 等动态集合类。第二次代码改为 List<Cargo> 是一个很好的实践。

计费重量计算的精度问题: 虽然代码中使用了 double 进行计算,但在比较体积重量和实际重量时,浮点数的精度问题可能会在极端情况下导致微小的误差。例如 (width * length * height) / 6000 的结果可能不是精确的小数。

心得: 对于需要高精度计算的场景,尤其涉及货币计算,应该考虑使用 BigDecimal 类,而不是 double 或 float。尽管本题目中使用了 double 并通过格式化输出 (%.1f) 隐藏了部分精度问题,但在商业应用中这是需要注意的。

费率计算逻辑的变化: 第一次的费率完全取决于计费重量,第二次的费率取决于货物类型。这种业务规则的变化直接影响了 Cargo 类的 calculateRate 方法(在第二次中变为 getRate)。如果在第一次设计时,考虑到未来可能增加其他影响费率的因素(如货物类型、运输距离等),或许可以将费率计算逻辑抽取出来,而不是直接硬编码在 Cargo 类内部。

心得: 业务规则是不断变化的。在设计初期,对于易变的业务逻辑,考虑将其独立出来,降低耦合度,例如使用策略模式(Strategy Pattern)来处理不同的费率计算方式。

订单总费用计算的理解: 第一次直接累加所有 Cargo 的 getFee(),而 getFee() 是 billingWeight * rate。第二次引入了折扣,并将计算分为 getTotalRawFee() (总计费重量 * 对应费率) 和 getTotalFeeWithDiscount() (总原始费用 * 折扣)。如果在理解需求时混淆了“单件货物的费用”和“订单总费用(含折扣)”的概念,可能会导致计算错误。第二次的代码清晰地区分了这两者,并在打印时分别使用了 getRawFee()(单件明细)和 getTotalFeeWithDiscount()(订单总额)。

心得: 在处理复杂的计算逻辑时,仔细分析需求,将计算过程分解为更小的、职责明确的方法,并确保每个方法都正确地实现了其预期的功能。命名规范也很重要,如 getRawFee 和 getTotalFeeWithDiscount 清晰地表达了方法的含义。

类职责的分配: 第一次代码中,Flight 类负责管理 currentLoad 并提供 canAddLoad 和 addLoad 方法。第二次代码中,这些方法被移除,载重检查完全由 Order 的 checkFlightCapacity 方法负责。这体现了对类职责的新理解。究竟是航班自己管理载重,还是订单在生成时检查自己是否满足航班的载重条件?在第二次的设计中,倾向于后者。

心得: 面向对象设计的一个挑战是合理地分配职责。一个类应该只负责它自己的事情。在设计时,多思考“谁应该做什么”,避免“上帝类”(God Class,一个类承担过多职责)的出现。

总结

通过对两次题目集“航空货运管理系统”代码的实现和演进,我们深入理解了Java面向对象编程的一些核心概念,如封装、类之间的关联以及枚举的使用。从第一次的直接实现到第二次引入枚举、List 和更细化的业务逻辑,我们体验了需求变化如何驱动代码结构的调整和优化。在这个过程中,我们学到了:

面向对象设计不仅仅是简单地将数据和方法放入类中,更重要的是如何合理地分配职责和建立类之间的关系。

枚举是处理固定集合常量的好方法,能提高代码的可读性和类型安全性。

List 等动态集合类在处理数量不确定的数据时比数组更灵活。

业务规则的理解和建模是编程的关键,需要仔细分析需求并将其转化为代码逻辑。

代码的演进是一个持续的过程,需要不断地评审和改进,以适应需求的变化和提高代码质量。

 

posted @ 2025-05-25 12:59  俾斯麦阿求  阅读(28)  评论(0)    收藏  举报