前言
这三次程序的编写与之前的略有不同,第四次程序编写是由前三次程序迭代而来,在拥有两次编写迭代程序的经历后,第四次的程序编写并没有耗费我太多时间。其新增加的知识点,如继承,多态等较容易理解并使用。在有两个晚上的设计后,于第三天晚上完成了第四次题目集的新需求。之后的题目集是更换了新的题目类型,不过并没有从头开始,其知识点依然延续着原本的知识框架。
下面简单介绍我在这三次程序编写中所用的知识点。包括:
数据结构 ,字符串处理 , 控制流程 , 异常处理 , 分数计算与判断逻辑 , 排序算法 , 模块化编程 , 正则表达式 , 继承与多态,接口实现等。这些知识说起来简单但真正用在题目数据中学生信息的提取,标准答案与学生答案的对比,匹配错误的判断却需要脑中有一个完整的设计思路以及能真正实现想法的编程能力。各次迭代的题目具有相当的难度,包括完成数据处理的准确等是真正需要花时间来完成的,当然题量与时间也控制的很好,以至于我不得不去花大量的课下时间去完成。
设计与分析
第四次迭代设计与分析
对于题目集四其中的多选题以及填空题,其原本与普通的题目没啥不同,只是对于答案的判断对错的判断要求有所改变而已,因此在此泛化题目类,并各自构造他们的得分判断方法。
1.多选题判断方法:
点击查看代码
public void judge_Z(Question_Z q,Answer answer0){
String[] q_standers = q.getStander_answer().split(" ");
String[] studentAnswers = answer0.An_information.split(" ");
int correctCount = 0;
int incorrectCount = 0;
for(String s:studentAnswers){
if(q.getStander_answer().contains(s)){
correctCount++;
}
else{
incorrectCount++;
}
}
if (incorrectCount == 0 && correctCount == q_standers.length) {
answer0.setScore(q.getScores());
answer0.setCondition("true");
}// if all correct set full
else if(incorrectCount==0&&correctCount>0){
answer0.setScore(q.getScores()/2);
answer0.setCondition("partially correct");
}
else {
answer0.setScore(0);
answer0.setCondition("false");
}
}
点击查看代码
public void judge_K(Question q,Answer answer0){
String[] q_standers = q.getStander_answer().split("或");
String[] studentAnswers = answer0.An_information.split("或");
int correctCount = 0;
int incorrectCount = 0;
for(String s:studentAnswers){
if(q.getStander_answer().contains(s)){
correctCount++;
}
else{
incorrectCount++;
}
}
if (incorrectCount == 0 && correctCount == q_standers.length) {
answer0.setScore(q.getScores());
answer0.setCondition("true");
}// if all correct set full
else if(incorrectCount==0&&correctCount>0){
answer0.setScore(q.getScores()/2);
answer0.setCondition("partially correct");
}
else {
answer0.setScore(0);
answer0.setCondition("false");
}
}
点击查看代码
while (true) {
line_get = scan.nextLine();
if(!inter_face.check_format(line_get)){continue;}
if (Objects.equals(line_get, "end")) {
break;
} else if (line_get.contains("#N:")||line_get.contains("#Z:")||line_get.contains("#K:")) {
Controller.input_queslist(depot0, line_get,paper_list);//题库
} else if (line_get.contains("#T:")) {
Controller.input_text(paper0, line_get, depot0);
Paper paper_new = (Paper) paper0.clone();
paper_list.add(paper_new);
paper0 = new Paper();
} else if (line_get.contains("#S:")) {
Controller.input_script(script0, line_get);
Script script_new = (Script) script0.clone();
script_list.add(script_new);
script0 = new Script();
} else if (line_get.contains("#X:")) {
Controller.input_X(student0, line_get, student_list);
} else if (line_get.contains("#D:")) {
Pattern pattern = Pattern.compile("N-(-?\\d+)");
Matcher f = pattern.matcher(line_get);
if (f.find()) {
delete.setX(Integer.parseInt(f.group(1)));
delete.delete_q(depot0, paper_list);
}
}
}
试卷程序迭代4类图:
第一次电路程序
- 程序的初始设计首先是对实体对象的设计,比较各实体对象的性质很清楚的发现所有对象都有相似的属性,而往下又可分为控制类,受控类,二者又可往下细分,可以看出这是一个十分明显的继承结构,以下是设计的初稿:
2.完成实体类的编写之后就需要了解其运行逻辑,此次计算各用电器电压时是运用了电位的相关知识,但这样的计算并没有很好的表现出电路,电器的性质,在整体看来兼容性依旧不强。以至于的迭代二时,基本的迭代而我却需要去修改程序的基础处理逻辑,导致迭代二的改变较大,十分消耗时间。以此看来,一个好的程序结构,不仅要很好的实现七大原则,包括单一职责,更重要的是职责的正确表现,能够准确,正确的编写模拟对象的各个性质,能有一个兼容性较强的处理逻辑。
数据的输入格式最后完成数据的输出,这里用一个电路类模拟电路并存储各个实体对象及依赖所有电器,整个的程序结构为输入,处理,输出。我总结前面迭代的经验,对程序的各部分功能尽力执行单一职责,为此实现代码的模块化L:
以下是对整个电路程序的基本设计思路分析:
类结构设计:
Electric 类作为基类,定义了所有电子元件共有的属性和方法,如电位、状态、日期等,并实现了Comparable接口用于排序。
Control 类继承自 Electric,代表具有控制功能的元件。
具体元件类(如 Switches, Bin_governor, Con_governor, Lamp_R, Lamp_B, Fan_D)继承自 Electric 或 Control,实现各自的特有属性和行为,如开关状态、亮度计算、速度调整等。
Appliance 类代表受控装置,如灯、风扇等,继承自 Electric。
电路构建与管理:
Circuit 类代表一条串联电路,使用 LinkedHashMap 存储电路中的元件,实现了元件之间的连接逻辑,以及检查电路中开关状态对其他元件的影响。
Controller 类负责解析输入,根据输入创建并添加电子元件到电路中,同时处理控制指令,如开关切换、调节器档位调整等。
用户界面:
Interface 类提供了打印电路中各类型元件状态的功能,通过遍历电路中的元件并按类型分类后输出。
主逻辑:
Main 类中包含了程序的主入口,通过 Scanner 获取用户输入,逐步构建电路、设置控制指令,最后输出电路中所有元件的状态。
输入处理:
用户通过特定格式的字符串输入来定义电路结构(如元件连接关系)和控制指令(如调节器的增减档、设定灯泡亮度等)。
输入解析逻辑在 Controller 类的 factor 和 set_controlling 方法中实现,分别处理元件初始化和控制指令。
输出展示:
程序根据电路中元件的实际状态,通过 Interface 类的方法组织并打印输出,展示了每种类型元件的状态信息。
整体上,该程序通过面向对象的设计方法,将复杂的电路系统抽象为一系列相互关联的对象,通过输入解析、逻辑处理、状态更新和结果输出几个关键步骤,实现了对电子电路的模拟和控制。
第二次电路程序:
此前的依靠电位来计算各个用电器电压的方法较为复杂,在经过一定的思考以及提前设计假设的情况下,我改变了电压的计算思路,通过整体到局部再到整体的思路,采用嵌套的对象存储格式,当然这也是题目中提示的,电路中包含用电器,控制器 ,电路。
1.首先应对输入格式的更改,由于这几次的任务并没有明显的可以用到课堂上学到的各种模式,因此想在这里将课堂上学到的适配器模式运用进去,发现通过对输入数据存储类Contraller使用适配器模式十分贴合,能很好的简化迭代的更新操作,这里运用的是将原Contraller类作为新的数据存储适配器类的里的引用即关联关系 ,以此达到调用原数据输入存储类中方法的作用。当然总的来说是通过对需要适配方法的抽象及重写。
相关操作:
点击查看代码
interface Target {//适配目标
void factor(LinkedHashMap<String,Electric> Ele_Map,String line,LinkedHashMap<String,Circuit> circuit_all);
void set_controlling(LinkedHashMap<String,Electric> Ele_Map,String line);
void set_controls(LinkedHashMap<String,Electric> Ele_Map,LinkedHashMap<String,Electric> controls );
}
class Controller_Adapter implements Target{//适配的输入类
public Controller_Adapter(){}
private Controller controller = new Controller();
public Circuit the_last(LinkedHashMap<String,Circuit> circuit_all){
Circuit circuit = null;
for(Map.Entry<String,Circuit> entry:circuit_all.entrySet()){
circuit = entry.getValue();
}
return circuit;
}
@Override
public void set_controls(LinkedHashMap<String,Electric> Ele_Map,LinkedHashMap<String,Electric> controls ){
for(Map.Entry<String,Electric> entry:Ele_Map.entrySet()){
if(entry.getValue() instanceof Control){
controls.put(entry.getKey(),entry.getValue());
}
}
}// 存储所有的控制设备 并且形成双向绑定 controls 与 circuit_whole中的控制设备
@Override
public void factor(LinkedHashMap<String,Electric> Ele_Map,String line,LinkedHashMap<String,Circuit> circuit_all){
Pattern pattern = Pattern.compile("\\[.*?]");
Matcher m = pattern.matcher(line);
while(m.find()){
controller.factor(Ele_Map,m.group(),circuit_all);
}
}//创建对象
其中对所有控制器另外存储于一个列表以便之后的控制器信息输入,
// 第二次目前改动:
// 1 添加了Fan_A类
// 2 泛化Circuit 分为 1并联电路 2串联电路 Circuit :父类中的属性 Map 可存储包含 控制装置 受控装置 电路等 对象
// 3 转移电路类中的linking(连接) check_circuit_K(检查电路开关)方法 至 Controller 类中
// 4 添加适配器 Controller_Adapter 对应接口 Target 其中已对factor方法适配
// 5 Main 中原本的电路变量 circuit 改为 circuit_all
// 6 更改电压计算方法 ,明白对于断路的电路整个电路的电阻是无穷大,就此对电路对象的电阻进行计算
电路第二次迭代的类图
代码细节雷达图:
踩坑心得
1.对于构造父类的引用以及子类的实例
多态性和类型转换: 使用子类对象作为父类类型的引用时,如果尝试调用只有子类才有的方法,会导致运行时错误。 ——在使用子类特有的方法而在列表存储,构造对象,传参,使用增强for循环时,都会使该对象的类型被视为父类从而在调用子类特有的方法时调用错误: 如果将父类对象强制转换为子类类型,可能会触发未检查的转换异常。 对此有两种应对方法保证调用的是子类的方法; 1.在父类中编写该方法并且在子类中进行重写,但是这是针对各子类有部分相似部分不同的方法从而由父类构造而在子类中重写。 2.运用instanceof进行对象实例的判断,但是这样较繁琐。 对此,可以调整以下我们的属性的结构,因为所有的对象最终都要输出参数,并且可将参数统一为字符串,所以单独写一个获取数据的方法作为获取对象需要输出的参数,这样只需要在子类中重写getDate:并且在子类中调用子类特殊的方法,就可以实现多态性:2.实型数转换成字符串类型并将其化整有不同情况
对于return String.format("%.0f",getSpeed()); 其会将6.66转化为7
对于return Integer.toString((int) getSpeed());其会将6.66转化为6
改进建议
1.各方法的功能未实现单一职责,导致结构不够清晰,各个类之间的耦合性较强,对此,我将在之后的迭代中将相关方法进行分解,以达到实现代码的模块化。
2.其中有大量方法具有相似且重复的功能,例如其中对于数据的存储以及输出,因局限于类型的转换中而无法简化,其后会将相应的判断转化为整体的判断,并且以各个对象的属性作为输出数据,而不是像现在这样根据判断自行输出编写好的数据,这样会使程序的健壮性以及兼容性大大受损。
3.对数据的处理不局限于特定的字符判断而升为对一类的字符判断,比如对空格之间的分割,采用\s+作为替代单个空格,由于我的输入是在原有的基础上进行适配的,所以原方法的改动较少,
这里因该减少对数据的破坏,以尽量一个字符串代表一种的数据的方式去提取。
总结
理解事先设计并且清晰需求(题目要求)的重要性
编程能力一定是建立在具有优秀的判断和理解需求的基础上的,如果无法正确的理解客户(甲方)的需求,再好的设计,再好的编程思想的体现都是做无用之功,此次对于电路程序的基础逻辑的判断,我一开始仅仅局限于对各个对象的操作而不去注重于他们之间的联系。一开始接触这样的题型,自己的思路无疑是一片混乱,但经历过这么多次的迭代程序的编写,我已经了解到,这不是一个小的所谓题目,更多的应该把它当作一个项目,工程一样去看待。而工程从来都不是一蹴而就的,应该一遍一遍的去了解要求,去解析各处的细节,去考虑各种的情况,先完成最基础的部分,比如实体类的编写。完成这些之后,我所要做的也逐步清晰起来,这时在脑中形成一个较完整的框架,这样的框架不是细致复杂的,而是有体系,各处结构分明的。我的基本体系很简单:输入->处理 ->输出.有了基本的框架与设计,在写代码时才不会混乱,才知道自己写的是什么,在整个程序中承担了什么作用.
适配器模式在进行代码功能的增加,修改,有着很大的用处,在此处使用适配器模式我竟觉得十分贴合.关于适配器模式的定义是这样的:
适配器模式(Adapter Pattern)是一种结构型设计模式,用于使原本不兼容的接口能够协同工作。适配器模式通常用于以下情况:
1.当系统中的一个类需要与另一个类(接口)协作,但是它们的接口不兼容时可以使用适配器模式。适配器模式允许两者能够协同工作。
2.当想要在不修改现有类的情况下使用一个类时,可以创建一个适配器,将新类的接口转换成现有类的接口。
此前我的代码还没有运用太多的接口,对于这次的适配是为a.原控制类抽象一个接口,再将构造适配器去关联原类作为其成员变量,进而适配接口中的抽象方法.
另一种使用方法是b.继承原来的方法,可采用super来调用原(父类的方法).
使用步骤:
- 定义目标接口(Target Interface): 创建一个客户端期望的接口,这个接口声明了客户端需要使用的操作。
- 创建适配者类(Adaptee Class): 这是已存在的类,拥有特定的功能,但是接口不符合客户端的需要。
- 创建适配器类(Adapter Class):
- 类适配器方式:通过继承适配者类,并实现目标接口。
- 对象适配器方式:持有适配者类的实例,同时实现目标接口,通过委托调用适配者类的方法。
- 客户端代码调用: 客户端不再直接调用适配者类,而是通过适配器来调用,这样就实现了接口的转换,使得原本不兼容的接口可以协同工作。
** LinkedHashMap列表的使用以及性质: **
LinkedHashMap 是 Java 中的一个 Map 实现,它继承自 HashMap 并实现了 Map 接口。LinkedHashMap 有以下主要特点和性质:
- 插入顺序保持:
默认情况下,LinkedHashMap 会按照元素的插入顺序来维护其内部的顺序。当你遍历这个映射时,元素会被按照它们被插入到映射中的顺序返回。 - 访问顺序保持:
如果在构造 LinkedHashMap 时设置 accessOrder 参数为 true,那么它将按照元素的访问顺序(即最近被访问过的元素会被移动到链表尾部)来维护顺序。这可以用于实现 LRU(Least Recently Used)缓存策略。 - 遍历性能:
由于内部使用了双向链表,LinkedHashMap 在遍历时比标准的 HashMap 更具有一致性,因为顺序是确定的。