南昌航空大学Java题目集OOP8~9大作业分析Blog2
一、前言
这次大作业是关于航空货运管理系统,主要是处理订单问题的,相比于第一次大作业,难度大大降低了,难度属于中等偏下的,没有什么算法问题,但这次考察的主要是面向对象设计的一些原则,如单一职责原则、里氏代换原则、开闭原则以及合成复用原则、依赖倒转原则。以下是我对这几个原则的理解:
- 单一职责原则:一个类只干一件事。就好比洗衣机不会同时具备“洗衣服”和“煮饭”功能,否则修洗衣机时其它的也需要修理,但不能过度拆分(如把 User 拆成 UserReader + UserWriter)。
- 里氏代换原则:子类必须能替换父类。就如能用塑料袋(父类)装东西,你拿个环保袋(子类)也应该能装(子类不能破坏父类承诺)。
- 开闭原则:对扩展开放,对修改关闭。就如同插座设计,你不需要改造插座就能插新电器(扩展开放),但插座内部电路不会暴露给你(修改关闭)。
- 合成复用原则:优先使用组合(has-a)/聚合(contains-a),而不是继承(is-a),主要是由于继承的耦合度高,且Java是单继承,灵活性差。
- 依赖倒转原则:依赖抽象,而非具体。就如手机充电,你依赖的是充电器(抽象),而不是具体充电器品牌(具体)。
这次大作业只有两次迭代,第一次主要考察类的设计,用单一职责原则设计类,合理地设计类与类之间的关系,第二次在原来题目的基础上添加可扩展的类:用户、支付方式、货物,并且题目要求使用继承。
主要知识点:面向对象设计的一些原则,列表List,用工厂创建对象,LocalDate的使用,Comparator的使用等。
二、设计与分析
航空货运第一次作业
题目:
一、计费重量的确定
空运以实际重量(Gross Weight)和体积重量(Volume Weight)中的较高者作为计费重量。
计算公式:体积重量(kg) = 货物体积(长×宽×高,单位:厘米)÷ 6000
示例:
若货物实际重量为 80kg,体积为 120cm×80cm×60cm,则:
体积重量 = (120×80×60) ÷ 6000 = 96kg
计费重量取 96kg(因 96kg > 80kg)。
二、基础运费计算
费率(Rate):航空公司或货代根据航线、货物类型、市场行情等制定(如CNY 30/kg)。本次作业费率采用分段计算方式:
公式:基础运费 = 计费重量 × 费率
三、题目说明
本次题目模拟某客户到该航空公司办理一次货运业务的过程:
航空公司提供如下信息:
航班信息(航班号,航班起飞机场所在城市,航班降落机场所在城市,航班日期,航班最大载重量)
客户填写货运订单并进行支付,需要提供如下信息:
- 客户信息(姓名,电话号码等)
- 货物信息(货物名称,货物包装长、宽、高尺寸,货物重量等)
- 运送信息(发件人姓名、电话、地址,收件人姓名、电话、地址,所选航班号,订单日期)
- 支付方式(支付宝支付、微信支付)
注:一个货运订单可以运送多件货物,每件货物均需要根据重量及费率单独计费。
程序需要从键盘依次输入填写订单需要提供的信息,然后分别生成订单信息报表及货物明细报表。
四、题目要求
本次题目重点考核面向对象设计原则中的单一职责原则、里氏代换原则、开闭原则以及合成复用原则。
分析:
源码类图:
源码报表分析图:
指标 | 值/描述 |
---|---|
Lines | 359 行 |
Statements | 206 条语句 |
Percent Branch Statements | 4.4% (主要来自if/else和循环) |
Method Call Statements | 30次方法调用 |
Percent Lines with Comments | 0% (代码中无注释) |
Classes and Interfaces | 9 (7个类 + 2个接口) |
Methods per Class | 平均 6-8 个方法 |
Average Statements per Method | 约 5-7 条语句 |
Most Complex Method | Rate1.rate() (第160-168行) |
Maximum Complexity | 5 (Rate1.rate()的圈复杂度) |
Deepest Block | Show.show()中的嵌套if-else (第240行) |
Maximum Block Depth | 3 层 (Show.show()中的嵌套) |
Average Block Depth | 1.5 层 |
Average Complexity | 2.2 (平均每个方法的圈复杂度) |
图表分析:
- 代码规模:
总共359行,206个语句,规模一般。 - 代码复杂度:
最大复杂度为5,来自Rate1.rate(),是由于Rate在不同的重量范围里对应不同的费率,使用了if-else,在可接受的范围内,总体来说该代码2.2的均圈复杂度相对较低,比较理想,这最主要的原因是这到题目基本上不需要使用算法,只需处理类与类之间的关系。 - 代码结构:
30 次方法调用,说明类与类之间耦合醒较强,这会增加调试的难度,很难维护,这一点需要进行改进。
9 个类与接口,具有一定的面向对象设计结构,类和接口用于封装数据和行为。 - 最大嵌套深度:
Show.show () 方法中的嵌套 if - else达到 3 层 ,平均块深度为 1.5 层 ,嵌套深度较深可能使代码可读性变差,调试难度增加,目前我还没有能力降低它的嵌套深度,还需要继续学习。
分析总结:
这次作业主要是考察类的设计是否符合面向对象设计原则,相比于第一次大作业类的设计,这次设计的更加成功,能够做到单一职责原则原则,但由于这次题目没有使用到扩展类,所以没有设计继承或接口,有点违背了开闭原则。并且我的方法大多都是具体的方法,与依赖倒转原则有点违背,在第二次迭代中我将这两个原则都使用到位了。
航空货运第二次作业
题目:
在原来的基础上添加一下内容:
本次作业费率与货物类型有关,货物类型分为普通货物、危险货物和加急货物三种,其费率分别为:
计费公式变成计算公式:基础运费 =计费重量×费率×折扣率
其中,折扣率是指不同的用户类型针对每个订单的运费可以享受相应的折扣,在本题中,用户分为个人用户和集团用户,其中个人用户可享受订单运费的 9折优惠,集团用户可享受订单运费的8折优惠。
支付方式(支付宝支付、微信支付、现金支付)
提醒:本题需求可扩展的类:用户、支付方式、货物
分析:
源码类图:
源码报表分析图:
指标 | 值/描述 |
---|---|
Lines | 459行 |
Statements | 246条语句 |
Percent Branch Statements | 22.2% (主要来自if/else和循环) |
Method Call Statements | 65次方法调用 |
Percent Lines with Comments | 0% (代码中无注释) |
Classes and Interfaces | 19 (18个类 + 1个接口) |
Methods per Class | 平均 3.4 个方法 |
Average Statements per Method | 3.4 条语句 |
Most Complex Method | Main.main() |
Maximum Complexity | 10 |
Deepest Block | Show.show()中的嵌套if-else (第240行) |
Maximum Block Depth | 2 层 |
Average Complexity | 2.8 |
图表分析:
- 代码规模:
总共459行,246个语句,规模一般。 - 代码复杂度:
最大复杂度为10,来自Main.main() ,圈复杂度较高,维护和测试难度较高,这主要是因为本题需求可扩展的类:用户、支付方式、货物,这里需要大量的if-else,使得复杂度增大,并且还有大量的数据输入,要创建很多对象,Main.main() 里的语句数也很多,这虽然不会影响圈复杂度,但一个方法超过60行非常的不好。在PTA里提交完代码后我又进行了一系列修改,创建Input类来处理输入问题,还增加3个工厂用来创建用户、支付方式、货物对应的对象,从而将if-else去除掉,降低圈复杂度。 - 代码结构:
总共19 个类与接口,比原来增加了10个类,主要是用户、支付方式、货物需要扩展,增加了大量的子类,但也有一部分类是将原来代码的类细分了一下,更加符合单一职责原则,同时也满足了开闭原则,是一个很大的进步。
分析总结:
这次最大的问题就是圈复杂度过大,以及main()方法语句数过多,以下是我的处理方法:
修改前:
public static void main(String[] args) {
Discount discount = null;
Passage passage;
Rate rate = null;
Goods goods;
GoodsList list=new GoodsList();
Scanner input=new Scanner(System.in);
String type=input.next();
Airplane airplane;
Payment payment=null;
if(type.equals("Individual"))
discount=new Individual();
else if(type.equals("Corporate"))
discount=new Corporate();
int bianhao=input.nextInt();
String passage_name=input.next();
String passage_num=input.next();
String passage_address=input.next();
String GoodType=input.next();
passage=new Passage(passage_name,passage_num,passage_address,discount);
int count= input.nextInt();
if(GoodType.equals("Normal"))
rate=new Rate1();
else if(GoodType.equals("Expedite"))
rate=new Rate3();
else if(GoodType.equals("Dangerous"))
rate=new Rate2();
for(int i=0;i<count;i++){
int good_num= input.nextInt();
String good_name=input.next();
double good_width=input.nextDouble();
double good_length=input.nextDouble();
double good_height=input.nextDouble();
double good_weight=input.nextDouble();
goods=new Goods(good_name,good_length,good_width,good_height,good_weight,good_num,rate);
list.add(goods);
}
String plane_num= input.next();
String start_city= input.next();
String end_city= input.next();
LocalDate date= LocalDate.parse(input.next());
double capacity=input.nextDouble();
airplane=new Airplane(plane_num,start_city,end_city,date,capacity);
String inform_num=input.next();
LocalDate date2= LocalDate.parse(input.next());
String mailer_address=input.next();
String mailer_name=input.next();
String mailer_num=input.next();
String consignee_address=input.next();
String consignee_name=input.next();
String consignee_num=input.next();
String pay=input.next();
if(pay.equals("Wechat"))
payment=new Wechat();
else if(pay.equals("ALiPay"))
payment=new Ali();
else if(pay.equals("Cash"))
payment=new Cash();
Passage mailer=new Passage(mailer_name,mailer_num,mailer_address);
Passage consignee=new Passage(consignee_name,consignee_num,consignee_address);
Information information=new Information(passage,airplane,list.getList(),date2,inform_num,consignee,mailer,payment);
Display display=new Show();
display.show(information);
}
修改后:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Input Input = new Input(scanner);
// 读取输入数据
Discount discount = Input.readDiscount();
Passage sender = Input.readPassage(discount);
GoodsList goodsList = Input.readGoodsList();
Airplane airplane = Input.readAirplane();
Information information = Input.readInformation(sender, goodsList, airplane);
// 显示信息
Display display = new Show();
display.show(information);
scanner.close();
}
增加Input类来处理输入问题,是得每个方法的语句数控制在合理的范围内。
class Input {
private final Scanner scanner;
public Input(Scanner scanner) {
this.scanner = scanner;
}
public Discount readDiscount() {
String type = scanner.next();
return DiscountFactory.getDiscount(type);
}
public Passage readPassage(Discount discount) {
int id = scanner.nextInt();
String name = scanner.next();
String number = scanner.next();
String address = scanner.next();
return new Passage(name, number, address, discount);
}
public Passage readPassage() {
String name = scanner.next();
String number = scanner.next();
String address = scanner.next();
return new Passage(name, number, address);
}
public GoodsList readGoodsList() {
String goodType = scanner.next();
Rate rate = getRateByType(goodType);
int count = scanner.nextInt();
GoodsList list = new GoodsList();
for (int i = 0; i < count; i++) {
int goodNum = scanner.nextInt();
String goodName = scanner.next();
double width = scanner.nextDouble();
double length = scanner.nextDouble();
double height = scanner.nextDouble();
double weight = scanner.nextDouble();
Goods goods = new Goods(goodName, length, width, height, weight, goodNum, rate);
list.add(goods);
}
return list;
}
private Rate getRateByType(String type) {
return RateFactory.getRate(type);
}
public Airplane readAirplane() {
String planeNum = scanner.next();
String startCity = scanner.next();
String endCity = scanner.next();
LocalDate date = LocalDate.parse(scanner.next());
double capacity = scanner.nextDouble();
return new Airplane(planeNum, startCity, endCity, date, capacity);
}
public Information readInformation(Passage sender, GoodsList goodsList, Airplane airplane) {
String informNum = scanner.next();
LocalDate date = LocalDate.parse(scanner.next());
Passage mailer = readPassage();
Passage consignee = readPassage();
Payment payment = readPayment();
return new Information(sender, airplane, goodsList.getList(), date, informNum, consignee, mailer, payment);
}
public Payment readPayment() {
String payType = scanner.next();
return payTypeFactory.getPayment(payType);
}
}
增加payTypeFactory,RateFactory,DiscountFactory三大工厂来去除if-else语句,从而降低圈复杂度。
class payTypeFactory{
private static final Map<String,Payment> strategies=Map.of("Wechat",new Wechat(),"ALiPay",new Ali(),"Cash",new Cash());
public static Payment getPayment(String pay){
return strategies.getOrDefault(pay,null);
}
}
class RateFactory{
private static final Map<String,Rate> strategies=Map.of("Normal",new Rate1(),"Dangerous",new Rate2(),"Expedite",new Rate3());
public static Rate getRate(String rate){
return strategies.getOrDefault(rate,null);
}
}
class DiscountFactory{
private static final Map<String, Discount> strategies=Map.of("Individual",new Individual(),"Corporate",new Corporate());
public static Discount getDiscount(String type){
return strategies.getOrDefault(type,null);
}
}
工厂模式非常地符合开闭原则,天然支持多态,无需对原来代码进行修改,只需添加一个子类接口就可以实现,提高了代码的可维护性和可扩展性,也在一定程度上降低了代码的耦合度。后期如果需要扩展,直接创建一个子类,然后在Map.of("Individual",new Individual(),"Corporate",new Corporate());添加对应的数据。
经过以上操作处理,最大全复杂度降低至5(Rate1.rate())。
我认为用户、支付方式、货物的扩展应该使用接口更加的合适,这样才更加符合合成复用原则,但由于题目要求我任使用了继承,可能是因为当前在学习继承,老是想让我们实操,从而更加掌握继承。后来我也将继承改成了接口,具体类图如下:
注:由于类之间的关系比较明显,且连起来很复杂、混乱,我将类图分成两部分,使类图变得更加清晰。
三、踩坑心得
这次题目比较简单,没有什么弯弯绕绕的地方,我对题目的解读比较清晰,没有遇到什么特别困难的地方,只能不断地进行完善。但也让我学到了一些知识点,比如LocalDate,Comparator。在之前我只是知道JVM自带LocalDate类,但我没有深入学习,正好这次要使用它,我便去网上深入学习了它的用法。我认为货物订单是按照商品的货物编号大小依次往下列出商品信息,于是我对它进行了排序,但我有不想用常规的排序方法,全部由自己编些排序代码,于是我就去学习了Java自带的排序方法,用Comparator来实现排序,大大减少排序代码。
四、改进建议
- 可以对Rate1.rate()进行修改完善,去除掉4层if-else,降低复杂度,在网上查询了修改方法,可以使用Map获得不同区间的Rate,这也从侧面反映出我应该多去学习Java里面知识点,了解更多的东西,让代码变得更加的高效。
- 这两次代码的耦合度相对比较高,我需要学习一些降低耦合度的方法。
五、总结
- 在这次大作业中,我熟练地掌握了封装,继承与多态以及接口,懂得了如何设计类,合理地处理类与类之间的关系,在第二次的代码中我设计了二十多个类与接口,我设计的不一定完全合理,但我的思维发生了变化,不在像C语言一样把所有功能放在一起,我潜意识的会更具题目的需求设计一些类,赋予它一些合理的方法与属性。我不可能通过一次大作业就能够完全合理地把类设计出来,但我会不断的学习,将类设计的更加合理。在这次大作业学到最多的便是面向对象设计原则,在前言部分就有我对其的理解,这些原则会让代码变得更加的健壮,提高代码的可用性。
- 在第二次迭代的过程中,我需要对第一次代码进行大量的修改,仅仅通过增加新类是无法实现题目要求的,这显然是违背了开闭原则,所以通过这次作业我了解了开闭原则的重要性,在之后的大作业中我就需要尽量满足开闭原则,仅仅通过增加新类就可以实现题目要求,这样才能让代码变得高效,可扩展,但也需要对原来代码进行改进,提高代码效率。