PTA题目集8~9

一、前言

这是第二次Blog作业了,作为软件工程专业的大一学生,就在不久前我经历PTA题目集8和9的"航空货运管理系统"开发任务。这两次题目集相比之前的电梯迭代题目,更侧重于Java面向对象特性的综合运用,特别是类的封装、继承、多态等核心概念,以及SOLID设计原则的实际应用,相比于上次的电梯作业,我觉得本次作业的难度尚可,也成功完成了这两次的任务,有了很大的进步。

这次航空货运系统的开发任务,重点检验了我们对Java面向对象核心特性的掌握程度,特别是如何将封装、继承和多态三大特性与实际业务需求相结合。在实现过程中,我们需要综合应用SOLID设计原则(包括单一职责、里氏替换、开闭原则等)来构建一个既安全规范又易于维护的系统架构。

系统采用ArrayList集合来高效管理货物数据流,整体架构围绕三大核心模块展开:

  1. 客户管理模块
  2. 货物处理模块
  3. 航班管理模块

在具体实现上,我特别注意了以下设计要点:

  • 严格遵循单一职责原则,将费率计算与总费用统计分离
  • 运用里氏替换原则确保继承关系的合理性
  • 通过依赖倒置降低模块间的耦合度
  • 利用合成复用优化代码结构

从难度评估来看,这次作业更注重设计模式的正确运用而非算法复杂度,主要考察我们将理论知识转化为实践能力的水准。通过合理运用面向对象的设计理念,成功构建了一个兼具功能完备性和扩展灵活性的航空货运管理系统。

​题目基本情况​​:

  • 题目集8:基础版航空货运系统,包含5道题目,主要考察类的基本设计、数据封装和简单业务逻辑实现
  • 题目集9:增强版航空货运系统,包含7道题目,增加了客户类型、货物类型、支付方式等多样化需求
  • 难度评估:整体难度适中,比电梯题目更注重设计而非算法。

​核心知识点​​:

  1. 面向对象三大特性:封装(客户/货物信息)、继承(客户/货物分类)、多态(不同客户/货物的差异化处理)
  2. 五大设计原则:单一职责(分离计算逻辑)、开闭原则(扩展而非修改)、里氏替换(子类替换父类)、依赖倒置(高层不依赖低层)、合成复用(优先组合而非继承)
  3. 集合框架:使用ArrayList管理货物列表,实现动态增删改查
  4. 格式化输出:DecimalFormat处理金额和重量的格式化显示

通过这两次题目集的实践,也让我对面向对象设计有了更深刻的理解,特别是在如何组织类结构、划分职责方面收获颇丰。

二、设计与分析

题目集8系统设计

类图分析

核心类说明

​1. ClientDetails(客户基本信息类)​

  • ​职责​​:封装客户的基本信息,作为其他客户类的基类。
  • ​属性​​:
    • protected String identifier:客户唯一标识
    • protected String fullName:客户全名
    • protected String contactNumber:联系电话
    • protected String location:地址
  • ​方法​​:
    • ClientDetails(String id, String name, String phone, String addr):构造函数
    • String getFullName():返回客户全名
    • String getContactNumber():返回联系电话

​2. Customer(客户信息类)​

  • ​继承关系​​:继承自 ClientDetails
  • ​职责​​:扩展客户信息,目前仅复用基类功能,为未来扩展预留。
  • ​方法​​:
    • Customer(String id, String name, String phone, String addr):调用父类构造函数。

​3. ItemProperties(货物属性类)​

  • ​职责​​:封装货物的基本属性(尺寸、重量等)。
  • ​属性​​:
    • protected String itemId:货物ID
    • protected String itemName:货物名称
    • protected double[] dimensions:数组存储宽度、长度、高度、重量(索引0-3)。
  • ​方法​​:
    • ItemProperties(String id, String name, double w, double l, double h, double wt):构造函数
    • String getItemName():返回货物名称
    • double getWeight():返回货物重量(dimensions[3]

​4. Cargo(货物信息类)​

  • ​继承关系​​:继承自 ItemProperties
  • ​职责​​:扩展货物业务逻辑(计费重量、费率计算、费用生成等)。
  • ​方法​​:
    • Cargo(String id, String name, double w, double l, double h, double wt):调用父类构造函数。
    • ​核心业务方法​​:
      • private double calculateVolumeWeight():计算体积重量(长×宽×高/6000)
      • public double getBillingWeight():返回计费重量(实际重量与体积重量的较大值)
      • private double determineRate(double wt):根据重量返回费率(阶梯定价)
      • public double computeCharge():计算货物运费(计费重量×费率)
      • public void displayDetails(int idx):格式化打印货物详情(名称、重量、费率、费用)

​5. FlightInfo(航班基本信息类)​

  • ​职责​​:封装航班的基本信息(航班号、起降地、日期、载重容量)。
  • ​属性​​:
    • protected String flightNumber:航班号
    • protected String origin:出发地
    • protected String destination:目的地
    • protected String flightDate:航班日期
    • protected double capacity:最大载重容量
  • ​方法​​:
    • FlightInfo(String num, String dep, String arr, String dt, double cap):构造函数
    • String getFlightNumber():返回航班号
    • double getCapacity():返回载重容量

​6. Flight(航班信息类)​

  • ​继承关系​​:继承自 FlightInfo
  • ​职责​​:复用基类功能,目前无额外扩展。
  • ​方法​​:
    • Flight(String num, String dep, String arr, String dt, double cap):调用父类构造函数。

​7. OrderProcessor(订单处理类)​

  • ​职责​​:聚合客户、货物、航班信息,处理订单的总重量/费用计算及打印。
  • ​属性​​:
    • private String orderId:订单ID
    • private String orderDate:订单日期
    • private ClientDetails sender:发件人信息
    • private ClientDetails receiver:收件人信息
    • private FlightInfo flightInfo:关联航班
    • private ClientDetails client:订单客户
    • private List<Cargo> items:货物列表
  • ​方法​​:
    • OrderProcessor(...):构造函数,初始化所有属性。
    • ​核心业务方法​​:
      • public double computeTotalWeight():计算订单总重量(所有货物计费重量之和)
      • public double computeTotalCharge():计算订单总费用(所有货物费用之和)
      • public void displayOrder():格式化打印完整订单信息(客户、航班、货物明细等)

​8. Main(主程序类)​

  • ​职责​​:程序入口,处理输入数据并协调其他类完成业务逻辑。
  • ​方法​​:
    • main(String[] args):主方法,调用其他方法完成流程。
    • ​辅助方法​​:
      • readClient(Scanner input):读取客户信息并返回 Customer 对象。
      • readCargoItems(Scanner input):读取货物列表并返回 List<Cargo>
      • readFlightInfo(Scanner input):读取航班信息并返回 Flight 对象。
      • readOrder(...):读取订单信息并返回 OrderProcessor 对象。
      • verifyAndDisplay(OrderProcessor, FlightInfo):验证载重是否超限并打印订单或错误信息。

SourceMonitor图

SourceMonitor指标

指标 数值
代码行数 320
平均方法复杂度 1.8
最大方法复杂度 4 (displayOrder)
类耦合度 中等

​分析​​:基础版本结构清晰,但存在以下问题:

  1. 客户和货物类型单一,缺乏扩展性
  2. 计费逻辑硬编码在Cargo类中
  3. 支付方式固定为微信支付

题目集9系统设计

类图分析

核心类说明

1. ClientDetails(客户基本信息抽象类)​

  • ​设计原则​​:开闭原则(通过抽象类支持扩展)
  • ​属性​​:
    • protected String identifier:客户编号
    • protected String fullName:客户全名
    • protected String contactNumber:联系电话
    • protected String location:地址
  • ​方法​​:
    • 构造函数:初始化客户基本信息
    • getFullName() / getContactNumber():获取客户信息
    • abstract double getDiscountRate():抽象方法,强制子类实现折扣率逻辑

​2. IndividualCustomer(个人客户类)​

  • ​继承关系​​:继承自 ClientDetails
  • ​职责​​:实现个人客户的折扣率(9折)
  • ​方法​​:
    • @Override double getDiscountRate():返回 0.9

​3. CorporateCustomer(企业客户类)​

  • ​继承关系​​:继承自 ClientDetails
  • ​职责​​:实现企业客户的折扣率(8折)
  • ​方法​​:
    • @Override double getDiscountRate():返回 0.8

​4. ItemProperties(货物属性基类)​

  • ​设计原则​​:单一职责(仅管理货物基础属性)
  • ​属性​​:
    • protected String itemId:货物ID
    • protected String itemName:货物名称
    • protected double[] dimensions:存储宽度、长度、高度、重量
    • protected String cargoType:货物类型(Normal/Dangerous/Expedite)
  • ​方法​​:
    • 构造函数:初始化货物属性
    • getItemName() / getWeight() / getCargoType():获取属性值

​5. Cargo(货物信息抽象类)​

  • ​设计原则​​:模板方法模式(抽象费率计算,固定计费流程)
  • ​继承关系​​:继承自 ItemProperties
  • ​方法​​:
    • ​核心方法​​:
      • calculateVolumeWeight():计算体积重量(长×宽×高/6000)
      • getBillingWeight():返回计费重量(实际重量与体积重量较大值)
      • abstract double determineRate(double wt):抽象方法,由子类实现费率规则
      • computeCharge():计算费用(计费重量×费率)
      • displayDetails(int idx):格式化打印货物详情

​6. 货物子类(NormalCargo/DangerousCargo/ExpediteCargo)​

  • ​设计原则​​:策略模式(不同货物类型差异化费率)
  • ​继承关系​​:均继承自 Cargo
  • ​职责​​:
    • NormalCargo:普通货物,费率阶梯为 [35,30,25,15]
    • DangerousCargo:危险品,费率阶梯为 [80,50,30,20]
    • ExpediteCargo:加急货物,费率阶梯为 [60,50,40,30]
  • ​方法​​:
    • 各自实现 determineRate(double wt),定义费率规则

​7. FlightInfo(航班基本信息类)​

  • ​属性​​:
    • flightNumber:航班号
    • origin/destination:起降地
    • flightDate:航班日期
    • capacity:最大载重容量
  • ​方法​​:
    • 构造函数及Getter方法

​8. 支付方式接口与实现类​

  • ​设计原则​​:依赖倒置(高层模块依赖抽象接口)
  • ​接口 PaymentMethod​:
    • String getPaymentMethod():定义支付方式名称
  • ​实现类​​:
    • WechatPayment:返回 "微信"
    • AliPayPayment:返回 "支付宝"
    • CashPayment:返回 "现金"

​9. OrderProcessor(订单处理类)​

  • ​设计原则​​:合成复用(组合客户、货物、航班等对象)
  • ​属性​​:
    • 订单基本信息(orderId/orderDate
    • 关联对象:sender(发件人)、receiver(收件人)、flightInfoclientitems(货物列表)、paymentMethod
  • ​关键设计​​:
    • 使用匿名内部类处理发件人/收件人(折扣率固定为1.0)
  • ​方法​​:
    • computeTotalWeight():计算订单总重量
    • computeTotalCharge():计算总费用(含客户折扣)
    • displayOrder():打印完整订单信息(含支付方式)

​10. Main(主程序类)​

  • ​职责​​:协调整个业务流程
  • ​方法​​:
    • ​输入处理​​:
      • readClient():根据类型创建个人/企业客户
      • readCargoItems():根据类型创建不同货物对象
      • readOrder():选择支付方式并构建订单
    • ​业务逻辑​​:
      • verifyAndDisplay():验证载重容量并输出订单或错误信息
 

SourceMonitor图

SourceMonitor对比

指标 题目集8 题目集9
代码行数 320 480
类数量 6 12
平均方法复杂度 1.8 2.1
最大深度 3 4

​进步体现​​:

  1. 通过抽象类和接口提高了扩展性
  2. 采用策略模式分离变化点(费率计算、支付方式)
  3. 匿名内部类处理特殊业务场景
  4. 类型系统更加丰富,符合现实业务需求

三、采坑心得

相比于上次的电梯题目,我个人感觉本次题目集的难度还是尚可的,也因此本次题目集没有像上次的电梯题目集一般处处踩坑,没有拿分。在本次题目集中我两次都顺利完成了题目集,最终结果是好的,不过中间也有很多波折,也遇到了很多问题,一下便是在完成题目集中遇到的一些问题:

1. 继承与多态的理解偏差

​问题场景​​:最初尝试在Cargo类中通过if-else判断货物类型

 
// 错误示范 if (cargoType.equals("Dangerous")) { rate = getDangerousRate(); } else if...
 

​测试数据​​:当输入包含多种货物类型时,输出结果混乱

​解决方案​​:重构为继承体系,利用多态特性

abstract class Cargo { public abstract double determineRate(); } class DangerousCargo extends Cargo { @Override public double determineRate() { ... } }
 

​验证结果​​:通过JUnit测试用例验证各类型费率计算正确

2. 折扣应用范围错误

​问题现象​​:最初设计将折扣应用到了所有费用上,包括危险品附加费

​错误代码​​:

// OrderProcessor中 double total = computeBaseCharge() * discount;
 

​调试过程​​:

  1. 打印中间值发现危险品费用被不合理打折
  2. 通过System.out.println调试发现计算顺序问题

​正确做法​​:先计算各货物总费用,再统一应用客户折扣

double sum = 0; for (Cargo item : items) { sum += item.computeCharge(); // 各货物按类型计算 } return sum * client.getDiscountRate(); // 最后应用客户折扣
 

3. 重量计算精度问题

​问题现象​​:PTA测试用例对重量精度要求严格,直接使用double比较会失败

​错误示例​​:

if (totalWeight > capacity) // 可能因精度问题误判
 

​解决方案​​:使用BigDecimal或设置误差范围

private static final double EPSILON = 1e-6; if (totalWeight - capacity > EPSILON) { // 确认为超载 }
 

4. 类职责划分不清

​初始设计​​:将费率计算放在OrderProcessor中

​问题​​:违反单一职责原则,增加维护难度

​改进后​​:

Rate计算 → Cargo子类 总费用计算 → OrderProcessor 折扣应用 → ClientDetails子类
 

四、改进建议

1. 引入工厂模式

​现状​​:Main类中直接创建各类型对象

if (type.equals("Individual")) { return new IndividualCustomer(...); }
 

​改进方案​​:使用工厂方法封装对象创建

 
class CustomerFactory { public static ClientDetails create(String type, String id, String name...) { switch(type) { case "Individual": return new IndividualCustomer(...); // ... } } }
 

​优势​​:

  1. 集中管理对象创建逻辑
  2. 降低Main类复杂度
  3. 符合开闭原则

2. 增加异常处理

​现状​​:缺乏对非法输入的处理

double wt = Double.parseDouble(input.nextLine()); // 可能抛出NumberFormatException
 

​改进建议​​:

try { weight = Double.parseDouble(input); } catch (NumberFormatException e) { throw new InvalidInputException("重量必须是数字"); }
 

3. 引入日志系统

​现状​​:调试依赖System.out.println

​改进方案​​:使用SLF4J+Logback

private static final Logger logger = LoggerFactory.getLogger(Main.class); logger.debug("正在创建{}类型客户", clientType);
 

4. 增强可测试性

​改进点​​:

  1. 将业务逻辑与输入输出分离
  2. 增加单元测试覆盖率
 
@Test public void testCorporateDiscount() { ClientDetails client = new CorporateCustomer(...); assertEquals(0.8, client.getDiscountRate(), 1e-6); }
 

5. 性能优化建议

​当前问题​​:大量使用ArrayList线性查找

​优化方案​​:

  1. 对频繁查询的货物使用HashMap索引
  2. 考虑使用Stream API处理集合操作
 
double total = items.stream() .mapToDouble(Cargo::getBillingWeight) .sum();
 

五、总结

1. 学习收获

通过这两次题目集的实践,我其实是很开心的,相较于上次电梯题目的失败,这次的题目集无疑是一次进步,我也有了很多收获,主要收获包括:

​面向对象设计能力​​:

  • 掌握了如何通过抽象类和接口建立清晰的类层次结构
  • 理解了多态在业务逻辑差异化处理中的威力
  • 学会了用组合而非继承来扩展功能(如支付方式)

​设计原则应用​​:

  • 单一职责:将变化点隔离到独立类中(如各种Cargo子类)
  • 开闭原则:通过继承体系支持扩展,无需修改已有类
  • 里氏替换:所有子类都可以无缝替换父类使用
  • 依赖倒置:高层模块依赖抽象而非具体实现

​工程实践技能​​:

  • 使用DecimalFormat进行专业数值格式化
  • 通过匿名内部类处理特殊场景
  • 使用正则表达式验证输入格式
  • 基础的UML类图绘制和理解能力

2. 待改进领域

  1. ​设计模式掌握不足​​:对工厂、策略等模式的理解还停留在表面
  2. ​异常处理经验缺乏​​:需要学习更多异常处理最佳实践
  3. ​单元测试不够熟练​​:虽然写了测试用例,但覆盖率不高
  4. ​性能意识薄弱​​:对集合类的选择和使用不够考究

3. 课程建议

​教学方面​​:

  1. 希望能在理论课后提供更多经典案例解析
  2. 建议增加设计模式相关的专题讲解

​作业方面​​:

  1. 题目描述可以增加更多业务背景说明
  2. 建议提供部分测试用例的预期输入输出
  3. 希望能获得更多代码结构设计方面的指导

​实验组织​​:

  1. 建议增加结对编程环节
  2. 希望提供更多自动化测试的指导

这两次题目集让我深刻体会到好的软件设计需要平衡功能实现与扩展性、可维护性等多方面因素。虽然还有很多不足,但每次改进都能感受到进步,这种成长的过程令人欣喜。我会继续努力,在后续学习中深化面向对象设计能力,争取写出更专业的代码。

posted @ 2025-05-25 19:38  谷恒早苗  阅读(17)  评论(0)    收藏  举报