二战飞机航运系统

 

目录

一.前言

二.设计与分析

三.踩坑经验

四.改进分析

五。总结

 

一.前言

本次习题集是针对飞机航运系统管理的实际模拟系统,在第八九两次习题集中进行需求迭代使学生对于java七大原则有更深的领悟。两次习题对算法的考察不深,更注重于对七大原则的灵活使用。此处将七大原则及其要求贴出以便后文对照分析:

1. SRP(单一职责原则):一个类 / 模块 / 接口仅负责一项职责,确保引起其变化的原因只有一个。
2.OCP(开闭原则):软件实体(类、模块、函数等)应对扩展开放,对修改关闭。
3.LSP(里氏代换原则):子类必须能够替换其父类,且程序行为不变。
4.DIP(依赖倒转原则):高层模块不应依赖低层模块,两者应依赖抽象(接口 / 抽象类)。
5.CRP(合成复用原则):优先使用组合或聚合而非继承来复用代码。
6.LOD(迪米特法则):一个对象应尽可能少地了解其他对象,仅与 “直接朋友”(成员变量、方法参数、返回值中的对象)交互。
7.ISP(接口隔离原则):客户端不应依赖其不需要的接口方法。接口应细化为多个专用接口,而非单一庞大接口。

 

二.需求与分析

题目要求:某航空公司“航空货运管理系统”中的空运费的计算涉及多个因素,通常包 括货物重量/体积、运输距离、附加费用、货物类型、客户类型以及市场供需等。 本次作业主要考虑货物重量/体积,以下是具体的计算方式和关键要点:

 一、计费重量的确定 空运以实际重量(GrossWeight)和体积重量(VolumeWeight)中的较 高者作为计费重量。 计算公式: 体积重量(kg)= 货物体积(长×宽×高,单位:厘米)÷6000 示例: 若货物实际重量为80kg,体积为120cm×80cm×60cm,则: 体积重量 =(120×80×60)÷6000=96kg 计费重量取96kg(因96kg>80kg)。

二、基础运费计算 费率(Rate):航空公司或货代根据航线、货物类型、市场行情等制定(如 CNY 30/kg)。

本次作业费率采用分段计算方式:

 

公式:基础运费 = 计费重量 × 费率

三、题目说明 本次题目模拟某客户到该航空公司办理一次货运业务的过程: 航空公司提供如下信息: 航班信息(航班号,航班起飞机场所在城市,航班降落机场所在城市,航班 日期,航班最大载重量) 客户填写货运订单并进行支付,需要提供如下信息:客户信息(姓名,电话号码等)  货物信息(货物名称,货物包装长、宽、高尺寸,货物重量等)  运送信息(发件人姓名、电话、地址,收件人姓名、电话、地址,所选 航班号,订单日期)  支付方式(支付宝支付、微信支付) 注:一个货运订单可以运送多件货物,每件货物均需要根据重量及费率单独 计费。 程序需要从键盘依次输入填写订单需要提供的信息,然后分别生成订单信 息报表及货物明细报表。

 

从题目需求可以分析得到此处我们需要制作一个根据货物具体重量来计算收费并生成订单的系统,订单中涉及到顾客,货物,航运公司,费率,支付方式等。故而根据面向对象设计这些应该做成不同的类,同时根据迪米特法则,顾客类与货物类不应直接接触,而应该通过Controller类接触,而对于订单类,如果顾客有增删货物的需求应该直接对订单类进行操作,如若要对订单进行查找则另设一个订单明细类更符合实际生活。此外考虑到后续费率可能根据需求会发生变化故在此处把Rate类设置成一个接口,这样即符合开闭原则只需后续对新需求进行类的扩展而不需要更改,同时又符合依赖倒转法则使OrderList类(高层模块)依赖Rate接口(抽象)而非具体的费率实现类(如RatePrice)。

代码整体的具体结构可参照以下类图:

 关键代码分析:(七大规则的具体体现)

一、单一职责原则(SRP)

核心思想:一个类只负责一项职责。
代码体现:
import java.time.LocalDate;
import java.util.LinkedList;

public class Order {
    LinkedList<OrderList> orderlist;
    String orderId;
    private LocalDate orderDate;
    private Sender sender;
    private Recipant recipant;
    private String senderAdddress;
    private String senderName;
    private String senderPhone;
    private String racipantAddress;
    private String racipantName;
    private String racipantPhone;
    public Order(String orderId,Sender sender,Recipant recipant){
        this.orderId = orderId;
        this.sender = sender;
        this.recipant = recipant;
    }
    public Order(String orderId, LocalDate orderDate, String senderAdddress, String senderName,
                 String senderPhone, String racipantAddress, String racipantName, String racipantPhone){
        this.orderlist = new LinkedList<>();
        this.orderId = orderId;
        this.orderDate = orderDate;
        this.senderAdddress = senderAdddress;
        this.senderName = senderName;
        this.senderPhone = senderPhone;
        this.racipantAddress = racipantAddress;
        this.racipantName = racipantName;
        this.racipantPhone = racipantPhone;
    }
    public void addOrderList(OrderList orderList){
        orderlist.add(orderList);
    }
    public double calculateTotalPrice(){
        double totalPrice = 0;
        for (OrderList orderlist1:orderlist){
            totalPrice += orderlist1.getPrice();
        }
        return totalPrice;
    }

    public double calulateTotalWeight(){
        double totalWeight = 0;
        for (OrderList orderlist1:orderlist){
            totalWeight += orderlist1.getGoodsWeight();
        }
        return totalWeight;
    }

    public String getOrderId() {
        return orderId;
    }

    // 获取所有订单明细
    public LinkedList<OrderList> getOrderList() {
        return orderlist;
    }

    public Sender getSender() {
        return sender;
    }

    public Recipant getRecipant() {
        return recipant;
    }

    public LocalDate getOrderDate() {
        return orderDate;
    }

    public String getSenderName() {
        return senderName;
    }

    public String getSenderPhone() {
        return senderPhone;
    }

    public String getSenderAddress() {
        return senderAdddress;
    }

    public String getRacipantAddress() {
        return racipantAddress;
    }

    public String getRacipantName() {
        return racipantName;
    }

    public String getRacipantPhone() {
        return racipantPhone;
    }
}

这是我的Order类,其中使用LinkedList对OrderList类进行读取分析,使得代码结构更加清晰,同时订单的职责分离更加符合单一职责原则。

二、开闭原则(OCP)

核心思想:对扩展开放,对修改关闭。
代码体现:
public abstract class Payment {
    public Payment(){

    }
    public abstract void pay(double amount);
}
public interface Rate {
    double calculateRate(double weight);
}

上面是抽象类Payment和接口Rate

  • 若需新增费率规则(如会员折扣),只需创建新类(如MemberRate)实现Rate接口,无需修改原有代码。
  • 支付模块通过抽象类Payment定义支付接口,WechatAlipay子类实现具体支付逻辑。新增支付方式(如银行卡支付)时,只需扩展子类,不修改原有代码。

这里体现了代码的开闭原则

三、里氏替换原则(LSP)

核心思想:子类可替换父类,且不破坏程序正确性。
代码体现:
public abstract class Customer {
    private String Id;
    private String name;
    private String phoneNumber;
    private String address;
    public Customer(){

    }
    public Customer(String Id,String name,String phoneNumber,String address){
        this.Id = Id;
        this.name = name;
        this.phoneNumber = phoneNumber;
        this.address = address;
    }

    public String getId() {
        return Id;
    }

    public void setId(String id) {
        Id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

 

public class Sender extends Customer{
    public Sender(String Id,String name,String phoneNumber,String address){
        super( Id, name, phoneNumber, address);
    }
}

这里SenderRecipant继承了Customer

SenderRecipantCustomer的子类,重写构造方法但未改变父类行为(属性和方法签名一致),可在需要Customer的场景中替换使用(如订单中的发件人、收件人)。

体现了里氏替换原则

四、接口隔离原则(ISP)

代码体现:
public interface Rate {
    double calculateRate(double weight);
}
public abstract class Payment {
    public Payment(){

    }
    public abstract void pay(double amount);
}

 

  • Rate接口的粒度控制:Rate接口仅定义calculateRate方法,专注于费率计算,未包含无关方法(如支付、物流),确保实现类只需实现必要逻辑。
  • Payment抽象类的单一职责:Payment抽象类仅声明pay方法,避免 “胖接口”,其子类只需实现支付逻辑,无需处理其他无关操作。

五、依赖倒置原则(DIP)

核心思想:高层模块依赖抽象,而非具体实现。
代码体现:
 
public OrderList(Goods goods, Rate rate){
        this.goods = goods;
        this.rate = rate;
        this.maxWeight = goods.getMaxWeight();
        this.price = maxWeight * rate.calculateRate(maxWeight);
    }

 

  1. OrderList依赖Rate接口:
    • OrderList的构造方法接收Rate接口类型参数,而非具体类(如RatePrice),实现高层模块(订单明细)与低层模块(费率策略)的解耦。
     
  2. Order类与支付方式的解耦:
    • Order类未直接依赖WechatAlipay,而是通过抽象类Payment定义支付行为,符合 “依赖抽象” 原则。

六、迪米特法则(LOD)

核心思想:对象间交互尽可能少,降低耦合。
代码体现:
class Flight {
    private String flightId; // 私有属性
    private double maxCarryWeight; // 私有属性

    public String getFlightId() { // 通过公共方法暴露信息
        return flightId;
    }
    public double getMaxCarryWeight() {
        return maxCarryWeight;
    }
}
  • Flight类将属性(flightIdmaxCarryWeight)声明为private,外部类(如Main)只能通过公共方法(getFlightId()getMaxCarryWeight())获取信息,避免直接访问内部状态,减少了外部类对Flight内部实现的依赖。

七、合成复用原则(CRP)

核心思想:优先使用组合 / 聚合,而非继承。
代码体现:
class Order {
    private LinkedList<OrderList> orderlist = new LinkedList<>(); // 组合关系
}
  • public OrderList(Goods goods, Rate rate) { /* 组合而非继承 */ }

OrderOrderList的组合关系:Order类包含LinkedList<OrderList>类型的orderlist字段,通过组合管理订单明细,而非继承OrderList类。

 

 

再看SourceMonitor的分析

 

 

 

此处对几个主要类进行详细分析:

 

 

优点

  1. 职责相对明确
    • Goods类专注于货物相关属性(如长宽高、重量等)和计费相关计算 ,每类方法数较多(14.00 ),能较好地封装货物操作逻辑,符合单一职责原则,便于对货物相关功能进行管理和扩展。
    • Flight类主要负责航班信息的管理,如航班号、起降地、载重等属性的维护 ,方法围绕航班信息展开,职责清晰。
    • Order类围绕订单进行设计,管理订单编号、订单日期、发件收件信息以及订单明细等 ,通过方法对订单整体进行操作,如计算总重量等,职责明确。
    • Customer类负责客户信息的处理,包括客户的基本属性(如 ID、姓名、电话、地址等)的封装与操作 ,便于集中管理客户相关数据。
    • Main类作为程序入口,承担了接收用户输入、协调各模块交互等功能 ,在一定程度上起到了流程控制的作用。
  2. 方法粒度较合理
    • 从平均每方法语句数来看,Goods类为 1.36 ,Flight类为 1.25 ,Customer类为 1.20 ,说明大部分方法的代码量较少,功能相对单一,易于理解、测试和维护。
  3. 具备一定扩展性基础
    • 从类的设计上看,各个类分别处理不同的业务模块,若系统需要新增功能,例如在Goods类中添加新的货物属性或计费规则,在Flight类中扩展航班相关功能等,有相对清晰的模块划分作为基础,不至于导致代码大规模混乱。

缺点

  1. 缺乏注释
    • 多个类的注释行百分比极低,如Goods类、Flight类、Customer类均为 0.0% ,Order类仅 1.2% ,Main类虽然有 8.8% 但仍不算高。这使得代码可读性差,不利于团队协作和后期维护,新接手代码的人很难快速理解代码逻辑和功能意图。
  2. 复杂方法潜在风险
    • Goods.getMaxWeight()Flight.setMaxCarryWeight()Order.calculateTotalWeight()Main.main()等被标识为最复杂方法 ,这些方法可能包含较多逻辑判断、计算或业务规则,与其他简单方法形成反差,可能成为代码理解、测试和维护的难点,也不利于代码的复用。
  3. 代码结构问题
    • Main类分支语句百分比达 25.0% ,方法调用语句数 52 ,说明其内部逻辑判断较多,方法调用频繁,可能导致代码结构不够清晰,维护难度增加。
    • Order类分支语句百分比为 3.8% ,相比其他类逻辑稍复杂,若后续继续扩展订单相关功能,可能需要进一步梳理和优化结构,否则容易造成逻辑混乱。
  4. 可维护性隐患
    • 整体上看,虽然部分类的方法平均语句数较少,但由于缺乏注释、存在复杂方法以及部分类逻辑结构相对复杂等问题,随着项目迭代和需求变更,代码的可维护性会面临挑战,修改代码时容易引入新的问题。

 

 

第九次习题

某航空公司“航空货运管理系统”中的空运费的计算涉及多个因素,通常包 括货物重量/体积、运输距离、附加费用、货物类型、客户类型以及市场供需等。 本次作业主要考虑货物重量/体积,以下是具体的计算方式和关键要点:

一、计费重量的确定 空运以实际重量(GrossWeight)和体积重量(VolumeWeight)中的较 高者作为计费重量。 计算公式: 体积重量(kg)= 货物体积(长×宽×高,单位:厘米)÷6000 示例: 若货物实际重量为80kg,体积为120cm×80cm×60cm,则: 体积重量 =(120×80×60)÷6000=96kg 计费重量取96kg(因96kg>80kg)。

二、基础运费计算 1 费率(Rate):航空公司或货代根据航线、货物类型、市场行情等制定(如 CNY30/kg)。本次作业费率与货物类型有关,货物类型分为普通货物、危险货 物和加急货物三种,其费率分别为:

 

计算公式:基础运费 = 计费重量 × 费率 × 折扣率 其中,折扣率是指不同的用户类型针对每个订单的运费可以享受相应的折扣, 在本题中,用户分为个人用户和集团用户,其中个人用户可享受订单运费的9 折优惠,集团用户可享受订单运费的8折优惠。

三、题目说明 本次题目模拟某客户到该航空公司办理一次货运业务的过程: 航空公司提供如下信息: 航班信息(航班号,航班起飞机场,航班降落机场,航班日期,航班最大载 重量) 2 客户填写货运订单并进行支付,需要提供如下信息:  客户信息(姓名,电话号码等)  货物信息(货物名称,货物包装长、宽、高尺寸,货物重量等)  运送信息(发件人姓名、电话、地址,收件人姓名、电话、地址,所选 航班号,订单日期)  支付方式(支付宝支付、微信支付、现金支付) 注:一个货运订单可以运送多件货物,每件货物均需要根据重量及费率单独 计费。 程序需要从键盘依次输入填写订单需要提供的信息,然后分别生成订单信 息报表及货物明细报表

主要修改点:

public class Individual extends Customer{
    private double discount=0.9;
    public Individual(){

    }
    public Individual(double discount){
        this.discount = discount;
    }
    @Override
    public double getDiscount() {
        return discount;
    }

    public Individual(String Id, String name, String phoneNumber, String address){
        super(Id,name,phoneNumber,address);
    }
}
public class Corporation extends Customer{
    private double discount=0.8;
    public Corporation(){

    }
    public Corporation(double discount){
        this.discount = discount;
    }
    @Override
    public double getDiscount() {
        return discount;
    }

    public Corporation(String Id, String name, String phoneNumber, String address){
        super(Id,name,phoneNumber,address);
    }
}

此处由于会按照客户类型进行打折,故需要在顾客类里实验得到折扣的抽象方法,此时如果继续沿用之前将寄件收件人分为两个单独的类则这两个类也需要重写实现折扣的方法,但这样就与题意不符,所以在第二次习题中我删掉了这两个类,换成在主函数中对两个对象进行创建实现题意所需功能。同时新创建了个人客户类和公司客户类来继承客户类实现此处的多态。

此外第二次习题还对不同货物的费率有不同的计算方式,故而我将费率分为三个不同的费率来继承Rate接口实现费率计算的多态(具体代码如下)

public class OrdinaryRate implements Rate{
    @Override
    public double calculateRate(double weight) {
        if(weight>=100){
            return 15;
        }
        else if (weight>=50){
            return 25;
        }
        else if (weight>=20){
            return 30;
        }
        else {
            return 35;
        }
    }
}
public class ExpediteRate implements Rate{
    @Override
    public double calculateRate(double weight) {
        if(weight>=100){
            return 30;
        }
        else if (weight>=50){
            return 40;
        }
        else if (weight>=20){
            return 50;
        }
        else {
            return 60;
        }
    }
}
public class DangerRate implements Rate{
    @Override
    public double calculateRate(double weight) {
        if(weight>=100){
            return 20;
        }
        else if (weight>=50){
            return 30;
        }
        else if (weight>=20){
            return 50;
        }
        else {
            return 80;
        }
    }
}

 

 

 

 

SourceMonitor分析

 

 

 

 

  

 

优点

  1. 职责单一性较好
    • Goods类围绕货物属性及相关计算展开,如重量、体积重量等 ,有 14 个方法,将货物相关操作集中封装,符合单一职责原则,便于对货物功能进行管理和扩展。
    • Flight类专注于航班信息管理,包括航班号、载重等属性的设置与获取 ,方法围绕航班业务逻辑设计,职责明确。
    • Order类负责订单相关操作,涵盖订单编号、订单日期、货物明细管理以及总重量计算等 ,方法集中处理订单业务,功能内聚。
    • Main类作为程序入口,负责接收输入和调度各模块,承担流程控制职责,一定程度上起到系统引导作用。
  2. 方法复杂度相对可控
    • Main类外,GoodsFlightOrder类平均每方法语句数较少,如Goods类为 1.36 ,Flight类为 1.25 ,Order类为 2.67 ,说明多数方法功能单一,易于理解、测试和维护。
  3. 代码结构有一定基础
    • 各文件从行数和语句数来看,规模适中,没有出现代码过度冗长或臃肿的情况 ,为后续功能扩展和代码维护提供了相对良好的结构基础。

缺点

  1. 注释严重缺乏
    • GoodsFlight类注释行百分比为 0.0% ,Order类仅 1.2% ,Main类虽有 8.8% 但仍不足 ,这使得代码可读性差,不利于团队协作和后期维护,新开发者难以快速理解代码逻辑。
  2. 部分方法复杂度高
    • Goods.getMaxWeight()Flight.setMaxCarryWeight()Order.calculateTotalWeight()Main.main()被标识为最复杂方法 ,这些方法可能包含复杂逻辑,与整体简单方法风格不符,增加了理解、测试和维护难度。
  3. Main类逻辑复杂可扩展性与可维护性隐患
    • Main类分支语句百分比达 25.0% ,方法调用语句数为 52 ,内部逻辑判断和方法调用频繁,导致代码结构不够清晰,维护成本增加。

虽然当前代码有一定结构,但因缺乏注释、存在复杂方法以及部分类逻辑复杂等问题,随着项目需求变更,代码的可扩展性和可维护性会面临挑战,修改时易引入新问题。

 

三.  踩坑经验

在第一次题目中,我错把订单总重量当成货物真实重量之和,导致PTA一直显示答案错误(如图)

 

public double calulateTotalWeight(){
        double totalWeight = 0;
        for (OrderList orderlist1:orderlist){
            totalWeight += orderlist1.getGoodsWeight();
        }
        return totalWeight;
    }
public double getGoodsWeight(){
        return goods.getWeight(); // 返回实际重量
    }

由于习题只提供了一个测试用例,而这唯一的一个测试用例刚好货物体积重量比实际重量要轻,导致我一直没有发现到底是哪里的问题导致答案有误,直到后期我对照自己的代码的各项数值与其他同学的数值进行比较才发现此处对题目的理解有误然后才得到正确程序。

此外对于收件人和寄件人我在题集一二中分别将其当做一个类和custmor类中的一个对象来处理导致两次代码在这里的处理器并不一致,违反了开闭原则,此处值得反思。

 

四.改进建议

 

代码结构与设计模式

 

  1. 增加注释:目前代码注释较少,尤其是关键方法和业务逻辑处。添加注释能提高代码可读性,方便他人理解和维护。比如在每个类的开头说明类的职责,在复杂方法内注释关键逻辑步骤。
  2. 使用设计模式优化
    • 工厂模式:在创建CustomerRatePayment对象时,目前使用大量if - elseswitch语句,可考虑引入工厂模式。例如创建一个CustomerFactory类,根据输入的客户类型返回对应的Customer实例,使对象创建逻辑更集中、可维护性更高。
    • 策略模式:Rate接口及其实现类已具备策略模式雏形,但可进一步优化。可以将费率计算策略的选择逻辑封装得更简洁,避免在Main类中直接使用switch判断。
  3. 减少Main类的复杂性:Main类承担了过多职责,包括输入读取、对象创建、业务逻辑判断和输出等。可将部分功能抽取到单独的服务类中,比如将输入读取逻辑封装到InputService类,业务逻辑处理封装到OrderService类等,降低Main类的复杂度,提高代码的内聚性。

 

 

功能完善与健壮性

 

  1. 输入校验:目前代码对用户输入没有进行充分校验。例如读取货物数量、重量等数值时,未考虑非法输入(如负数、非数字字符)的情况。应添加输入校验逻辑,提高程序的健壮性。
  2. 异常处理:在可能出现异常的地方(如LocalDate.parse方法解析日期、数值转换等操作),添加适当的异常处理机制,避免程序因异常直接崩溃,同时给出友好的错误提示信息。

客户折扣逻辑优化:当前IndividualCorporation类中折扣值是固定的,可考虑从配置文件读取或通过更灵活的方式设置,以满足不同业务场景下折扣动态调整的需求。

 

五.总结

 

  • 细节没注意
    输出的总重量没搞清楚到底是真实重量和还是最大重量和就直接写导致后期报错却又搞不明白到底是因为哪里报错排查Bug时改的很痛苦,以后还是要完全搞清楚需求细节再敲代码

  • 继承和多态没想好就写
    在继承和多态模块,一开始没想清楚怎么设计,就直接写代码,结果功能一变,代码就得大改,很麻烦。以后得先想好类之间的关系,哪些功能要扩展,再动手写,不然很容易返工。

  • 类设计不够正确
    之前写代码,只想着因为收件人和寄件人职责差不多就把他们做成两个类来继承客户类,结果功能改变后又发现既然他们不需要实现多态会与新的客户类的子类方法冲突,导致客户类的抽象方法不好写又要改回去当成一个对象来写导致改动比较大。

这次项目让我明白,类设计和继承多态不能急,需求要搞清楚,细节要关注,设计要合理,规范很重要。以后写代码一定得注意这些,争取少踩坑。

关于改进建议

 

建议老师在以后的习题中可以多给出一些测试样例,因为题目的某些描述比较笼统,可能会误解需求,在多种测试样例的辅助下可以使我们更容易把握题目需求。

 

 

posted @ 2025-05-24 13:36  24201706-张子悦  阅读(33)  评论(0)    收藏  举报