目录
一.前言
这三次大作业的难度总体而言比以往简单了些,但是对类的设计要求更高了。或许是有前三次大作业的基础在,这三次大作业虽然难度降低,但收获却大得多。话不多说,上知识点。
nchu-software-oop-2024-上-4 ~知识点
- 继承
- 多态
- 正则表达式
nchu-software-oop-2024-上-5 ~知识点
- 继承与多态
- 抽象类
- 类的设计
- 面向对象设计原则
nchu-software-oop-2024-上-6 ~知识点
- 继承与多态
- 类的设计
- 抽象类
- 面向对象设计原则
在这三次作业中,4是答卷题,5和6是电路题。答卷题不必多说,已经总结过一次了,这次只是多加了个继承和多态,总体而言比较简单。然后是电路题,这题花了我很多时间去思考类的设计,这也是我觉得的电路题的精髓,这会在之后总结中详细说明。
接下来我会按照设计与分析、采坑心得、改进建议、总结四部分进行这次PTA题目集4~6的总结。
二.设计与分析
对于题目集4~6第一题的分析
一.答题判题程序-4
这道题新增了一个多选题和填空题。在初步学习了解继承和多态后,其做题的方法就显而易见了,甚至不怎么需要改代码。
以下是我第四次作业的类图。
1.继承
先前就听闻面向对象最显著的特性有“封装、继承、多态”三个,而写这道题目前就学习并了解了这三个特性,印象里写这题是很顺利的。
首先是继承:
继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
而这种特性使得我们设计多选题和填空题的难度大大降低,这两者皆继承Topic类就行。
class Topic{
......;
}
class MCQ extends Topic{//多选题类
......;
}
class Fill extends Topic{//填空题类
......;
}
继承避免了对一般类和特殊类之间共同特征进行的重复描述。
2.多态
本次大作业的另一考点正是多态。多态是个很好用的东西,它不止被运用在第四次大作业中,在第五六次大作业中它也发挥着至关重要甚至不可或缺的作用。
关于多态:
在一些资料中,多态被定义为“为不同数据类型的实体提供统一的接口”。计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。
简单来说,所谓多态意指相同的消息给予不同的对象会引发不同的动作。
大家不妨想一想,新增的多选题和填空题与固定题型有什么区别?除开答案的不同,其实最显著的一个区别就是它们的判题方法不一样。只要利用多态写下不同题型的不同判题方法,其实这次大作业也就差不多写完了,不需要改什么代码。
class Topic{
public String judge(String a){//通过传入的答案a,跟正确答案进行比对
......;
}
}
class MCQ extends Topic{//多选题类
public String judge(String a){//通过传入的答案a,跟正确答案进行比对,但比对方法与上面不同,是判断是否符合ABCD
......;
}
}
class Fill extends Topic{//填空题类
public String judge(String a){//通过传入的答案a,跟正确答案进行比对,但比对方法不同,是判断是否为子字符串
......;
}
}
二.家居强电电路模拟程序-1
1.类的设计
这次大作业的题目很早就发布在群里了,比起迅速动手写,我是先琢磨起了类的设计:该怎么设计类才可以使各元器件顺滑地连接在一起?怎么设计类才能使各元器件有序执行自己的那一套方法?怎么设计类才能为后来的迭代留下新增功能的空间?……
我花了很多时间去想这些事情,甚至产生了些现在看来不必要的想法:一开始我被引脚牵引了思路,觉得既然设置了引脚,那么就应该有一个引脚类,然后引脚类的实例对象中可以存放元器件,这样就可以把各个元器件连接起来。
后来我又想,为什么不干脆设计个电路类?我不需要设置引脚,也不需要关注各个元器件的摆放位置,我只需要一个电路类的实例对象,这个对象里应有一个容器存放这条线路中的所有元器件,这样,各个元器件也同样可以连接起来。
我越想,就越觉得比起引脚类,电路类显然才是正道。
或许你会问,设计的电路类确实解决了串联,但并联该怎么办呢?这个问题我早在电路第一次大作业就想好了解决方法:并联显然可以看作是多个串联路线的结合体,而串联路线也可以看作是一个“元器件”,这个“元器件”存放在并联的实例对象的容器中,其实与串联电路实例对象的容器中存放的元器件并无区别。
或许有人要问了,若主路线中有并联,并联中又有并联该怎么办呢?这个问题其实跟上面这段很相似。同样地,我们可以把并联也看做是一个“元器件”,这个“元器件”存放在主路线中,等程序运行到需要对这个并联电路赋值时,我们又可以把并联电路拆成串联路线,而拆出的串联路线里又有并联电路这个“元器件”,我们再做相同的赋值操作即可。这段话听起来很绕,但其实如果你这题拿了满分,我猜,你的思路大抵跟我是一样的。
我对类的设计就是在这样一问一答中进行的,虽然我在此只列出了两个问题,但实际上当时我内心冒出的问题比这要多得多得多。我从最开始的晕头转向,到后面梳理清逻辑,早在第一次大作业就考虑好后续2、3并联电路迭代的计算问题,这一切都要归功于类的设计。
以上是我的类图。
我考虑类的设计时大概是五一假期,当时蔡老师还没有给出他对类的设计建议,所以上述一切是我独自思考后的结果。
后来过了半个月,蔡老师发布了大作业,几天后又提出了他自己对类的设计,然后我发现我们类设计的重合度至少在口头表达上是很高的,同样的元器件类与Line类,以及继承元器件类等等,关键的几个点都能对得上,这更加强化了“类的设计”在我心中的重要性。
这次大作业在我看来是出得特别特别好的,它给足了我思考的时间,也促成了我第一次自发主动地去设计自己的类,更让我理解了设计好类有多么的重要。
2.抽象类
关于抽象类:(这里引用了他人的总结与定义)
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。在Java中,如果一个类被abstract修饰则称为抽象类。抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。
本次大作业中,有一些类我也设置成了抽象类:
abstract class Device{
String name;//编号
double U;//电压
int state;//状态
double R=0;//电阻
double I=0;//电流
......
}
abstract class Lamp extends Device{
......
}
二.家居强电电路模拟程序-2
1.面向对象设计原则——单一职责原则
关于单一职责原则:(此处引用了他人的定义与总结)
单一职责原则又称单一功能原则,面向对象五个基本原则之一,它规定一个类应该只有一个发生变化的原因。所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。
本次大作业我的另一改变就是主动往“单一职责原则”上接近。无论是上个学期的C语言还是这个学期的Java,都一再强调过单一职责原则,但出于某种难以描述的心态,我几乎没注意过这个原则,几乎每段代码都是长篇大论的。
本次大作业我不敢说自己完全遵守了“单一职责原则”,甚至有部分方法仍然是“长篇大论”,但起码可以说,比起以前,我在尽量使自己更加注意这一点。
话不多说,直接上图。
这是答题判题迭代4的数据,可以看到,最高复杂度有21,平均复杂度也有3.09
这是我电器题1的数据,最高复杂度为9,平均复杂度为2.21
这是电器2的数据,最高复杂度为10,平均复杂度为1.94
显然,无论是最高复杂度还是平均复杂度,电路题较答卷题都有显著降低。即使这有题目不同的原因,也并不能完全跟“单一职责原则”挂钩,但也许或多或少能说明我有尽量往这方面接近,这使我收获很多。
遵循单一职责原的优点有:
1、类的复杂性降低。 |
关于这个复杂度方面,其实我的还是有点高了,这点我会在后文的“改进建议”里进行一个反思。
2.类的设计之继承与多态
我这个标题的表述或许并不严谨
电路题大作业的计算,知道了“串联电路电流相同,并联电路电压相同”的原理后,就可以利用继承和多态实现,计算电流电阻电压的方法,会根据串并联电路的不同而不同,这正是方法覆盖:子类继承父类并重写父类中已经定义的方法,以实现不同的行为。
关于多态:(此处引用了他人的定义与总结)
简单来说就是一个对象有多种形态,具体点说多态就是父类中的属性和方法被子类继承后,可以具有不同的数据类型或不同的行为,使父类中的同一个属性和方法在父类与各个子类具有不同的含义。
知道了这个后,我们计算前就可以进行方法覆盖,这样最后就可以从头到尾用同一个方法名进行计算。下面是我的部分代码解释:
public void working(){
queen.working();//主路线进行工作,也就是计算赋值
Output.show(device);
}
public void working(){
......;//略
for (Map.Entry<String, Device> entry : device_self.entrySet()) {
entry.getValue().setI(I);//进行计算赋值
}
}
多态可以让代码的扩展性更强,更灵活,也可以提高代码的复用性。
三.踩坑心得
这一部分会着重讲第六次大作业的电路题2
1.运行中除0
在测试自己写的测试样例进行摸索时,我发现了一个问题,那就是有时并联电路的电阻会突然显示为“Infinity”,这表示电阻为无穷。然后我就开始疑惑了,为什么会显示电阻为无穷呢?随后我发现了一些问题:
public void r(){
......;//略
for (Map.Entry<String, Device> entry : device_self.entrySet()) {
resistance+=1.0/entry.getValue().getR();
}
R=1/resistance;
......;//略
}
这段代码乍一看好像还正常,其实有大问题。对于并联电路,在计算电阻的倒数时,我只进行了加的操作,没有考虑这个倒数可能为0的情况。(虽然测试点中似乎也没有倒数为0的情况,因为这种情况在正常电路中不会出现,它会出现在短路电路中,或是全是开关的电路中)
意识到自己没考虑到除0的情况后,我进行了判定,如下:
public void r(){
......;//略
for (Map.Entry<String, Device> entry : device_self.entrySet()) {
if(entry.getValue().getR()!=0)
resistance+=1.0/entry.getValue().getR();
}
if(resistance==0)
R=0;
else
R=1.0/resistance;
......;//略
}
关于除0这个情况,其实不止出现在了我并联电路的计算中,它还出现在了其他地方,因此,敲代码时要细心一点,考虑周到一点,不然后面出现了一些未知的问题,很难找到原因。
2.精度问题
或许有人与我一样,写到94分就出现了2个过不去的测试点。
在与同学进行讨论后,我知道了自己问题所在:
好像没什么问题对吧,但其实这个B,就是灯要输出200。随后,我让这个灯输出它的电流电阻电压,分别是21.999999999999996,10.0,219.99999999999997。
这个时候就出现了问题了,因为我计算过程中运用了很多乘法和除法,精度或多或少都有损失,比如这个电流21.99999多,其实原本应该是22.0的,这使我吃了大亏,花了很多时间寻找问题。
3.考虑不周
在第五次大作业中,我遇到了四个测试点过不去的情况。因为本次大作业几乎不涉及计算,所以我考虑一会,更改了一些地方,最终发现是分档调速器的档位问题。
其实这是一个很小的问题,因为档位是在0到3之间变化,所以档位不应该超过这个范围。但我一开始怀有侥幸心理,觉得既然老师说不会有错误信息的输入,所以我想,应该也不至于升档升到超出界限才是,就没注意这一部分。最后把档位控制在0~3后就通过了测试点。
写作业时应该细心一点,在写的过程中就应该多去考虑意外情况,而不是觉得无所谓。当自己考虑周全时,其实会省去很多麻烦。
4.改进建议
1.语句与深度
在上文中,每次大作业我都用SourceMonitor进行了检查并贴出了数据,虽然比起以往,最高复杂度及平均复杂度都有所降低,但是令我不解的是,有一个东西一直很高,那就是Block Histogram显示的数据(右下角红色直方图那部分)。我曾看到别人代码检测的数据,有一些人这一部分最高都只有20几,而我却达到了120几。
经了解,Statements指的是语句数目,分支语句if,循环语句for、while,跳转语句goto都被计算在内;而Block Depth指的是函数深度,指示函数中分支嵌套的层数。
于是我观察了自己的代码,几乎全篇都是if,else的结合体,还有一两个方法是非常非常繁琐的,一层套一层,if-else里面长篇大论。我搜索了一些网友建议的减少if-else的方法,有表驱动,职责链模式,注解驱动,事件驱动,有限状态机,Optional,Assert,多态,枚举……这里面有很多我几乎完全不了解的,意味着未来还有很多的学习之路要走,还得多加学习。
2.子父类
众所周知,子类调用父类方法可以使用“super”关键字,那么父类可以调用子类的方法吗?如何实现?
这次大作业,我所设计的所有类都有个共同的父类(元器件device),所有类的实例对象都当作元器件存储,即类似于:
TreeMap<String, Device> device = new TreeMap<>();//存放所有元器件
Switch k=new Switch(name);
device.put(name,k);
后来当我运行时,发现了一点问题,那就是在我所写的子类中有一些父类没有的方法,但我是将子类当作父类存的,这些子类独有的方法就无法调用。我想了一个不是很好的方法,那就是在父类中添加同名的方法,只不过方法体体为空,其实换一句话说可能就是多态吧。这有一个很明显的坏处,那就是父类的方法会越来越繁杂,父类中的方法不是所有子类都需要的,于是子父类之间的关系变得混乱。
我于网上了解了一些父类调用子类方法的方案,但都需要在父类中多写一个方法,并没有达到我所期待的效果。所以我想,其实父类与子类之间的关系应该一开始就构思得好一点,让所有子类都需要的方法放到父类中,而不是有的子类需要这个方法有的不需要,这是个可以改进的地方。
5.总结
收获:
|
比起前三次大作业,这三次大作业给我的感受就是应该考虑代码的优劣,该怎么写更利于后续可能诞生的改变,而不是仅仅是把题目写出来就万事大吉。从读题思考,到类的设计、具体方法、如何实现等,都需要经过很多的考量,如果能独立思考完成这些部分,自己的思维和创造力都会有所提升,当然,多与同学交流也是大有益处的。
然后,在写题目时,也要多考虑意外情况。虽然电路题大作业并没有错误输入,但是却也有一些需要考虑的点,比如分档调速器的档位问题,它不可能超出自己的档位,也不可能档位为负等等。如果小的细节没有考虑到,最后找错误就会非常麻烦。
最后,本次大作业给我带来了很多提升,同时也让我意识到自己还有很多需要改进的地方。希望在日后的学习中,自己能越来越好。