· 前言
本次的三个作业,由答题判题程序- 4、家居强电电路模拟程序- 1、家居强电电路模拟程序 -2组成。
答题判题程序-4是对前三次判题程序的最后升级,设计多个子类继承于基础题类来实现对每种题型的判断和计算分值;而家居强电电路模拟程序-1则是对输入的各个设备在串联关系中的状态更新,其中涉及到设备电压的计算和开关状态的判断,以及受控设备的状态输出;最后家居强电电路模拟程序-2则在前一题的基础上增加了并联的内容,在逻辑处理上更加的复杂了。本文将系统性总结这三次题目集的知识点、题量及难度,并分析其中的核心内容和实现方法。
· 题目集概述
· 答题判题程序 - 4:
题目分析:
-
数据结构
题目信息、试卷信息、学生信息、答卷信息分别存储为字典或列表,方便检索与处理。
单选题、多选题、填空题设计为子类单独处理判题逻辑。
-
输入处理
根据输入前缀(如#N:、#T:)判断当前行信息类型,逐一解析存储。
本次作业新增输出顺序变化:
只要是正确格式的信息,可以以任意的先后顺序输入各类不同的信息。比如试卷可以出现在题目之前,删除题目的信息可以出现在题目之前等。
要成功处理以上要求就应该将题目、试卷等信息先
本次作业新增输入格式内容:
多选题:
格式:"#Z:"+题目编号+" "+"#Q:"+题目内容+" "#A:"+标准答案
填空题:
格式:"#K:"+题目编号+" "+"#Q:"+题目内容+" "#A:"+标准答案
格式基本的约束与一般的题目输入信息一致。 例如:#K:2 #Q:古琴在古代被称为: #A:瑶琴或七弦琴
删除题目信息需在存储中直接删除,同时记录。
-
判题计算分数规则
单选题:答案完全匹配为正确,否则为错误。
多选题:所有正确选项被选择且无错误选项给满分;部分正确无错误选项得半分;错误或无答案得0分。
填空题:答案完全匹配为正确;部分匹配得半分;错误或无答案得0分。
-
输出处理
按学号与试卷号排序输出。
输出每道题的详细判定结果和学生总分。
若题目回答正确则输出true,多选题或者填空题部分正确输出~partially correct,单选题没有部分正确的情况;
若答案错误或者包含错误选项或者内容则输出~false。
-
警告信息
如果试卷总分不为100,输出
"alert: full score of test paper1 is not 100 points"
。如果试卷不存在,输出
"the test paper number does not exist"
如果试卷错误地引用了一道不存在题号的试题,在输出学生答案时,提示
"non-existent question~"
加答案。如果该题号被删除并且在答卷中没有答案则只输出
answer is null
。
设计与分析
正则表达式
- 使用正则表达式匹配输入格式:匹配题目格式(#N, #Z, #K 等)。匹配试卷信息格式(#T)。匹配答卷信息格式(#S)。
Pattern
:定义正则表达式。Matcher
:执行匹配操作。
数据结构与集合框架
集合类型
- 使用 Map(HashMap 和 TreeMap)存储数据:
- Map<Integer, List > testPapers 用于存储试卷题目及分数。
- Map<Integer, List > answerSheets 用于存储学生答卷。
- 使用 List(ArrayList)存储问题、答卷等顺序数据。
- 使用 Set(HashSet)处理多选题答案的集合操作。
排序
- 使用 Comparator 对答卷按试卷号排序。
- 使用 TreeMap 对 answerSheets 按学生 ID 进行排序。
流操作
- 使用 stream 和 mapToInt 方法计算试卷总分。
面向对象设计
封装
- 将不同的功能模块封装为独立的类(如 InputHandler, OutputHandler 等)。使用私有字段和公共方法访问数据。
继承与代码复用
- 通过继承 Question 类实现不同题型的共同属性与行为(如
getAnswerCorrectnessLevel()
方法)。
多态
- 使用多态统一调用
calculateScore()
等方法。
组合
- 类中包含其他类的对象,例如
AnswerSheet
包含QuestionScore
和学生答案。
单一职责
InputHandler
专注于输入数据解析。OutputHandler
专注于处理输出。
源码结构分析
- 设计模式:
继承与多态:题目类型(单选、多选、填空)继承自 Question,实现不同的行为。
组合模式:Exam 包含题目集合,AnswerSheet 包含题目分数列表。
分层设计:输入、逻辑处理、输出各自分离,增强模块化。
类间关系: - Main 负责调用。
Exam
作为核心类管理题目。
AnswerSheet
结合 Exam 和 Question 进行答案校验与得分计算。
InputHandler
和OutputHandler
负责与外界的交互。
main 类中调用:
inputHandler.readExamData(exam, testPapers, answerSheets, testIds, students);
来处理用户输入
点击查看代码
// 读取题目、试卷和答卷数据
public void readExamData(Exam exam, Map<Integer, List<QuestionScore>> testPapers, Map<Integer, List<AnswerSheet>> answerSheets, List<Integer> testIds, Map<Integer, Student> students) {
while (true) {
String inputLine = scanner.nextLine();
if (inputLine.equals("end")) {
break; // 输入结束
}
// 解析题目信息
try {
if (inputLine.startsWith("#N:")) {
Pattern questionPattern = Pattern.compile("#N:(\\d+)\\s+(?:(#Q:(.+?))\\s+#A:(.+?))");
Matcher matcher = questionPattern.matcher(inputLine);
if (matcher.matches()) {
int num = Integer.parseInt(matcher.group(1));
String questionContent, standardAnswer;
// 判断匹配的顺序并获取对应的 group
if (matcher.group(3) != null) {
// #Q 在前
questionContent = matcher.group(3);
standardAnswer = matcher.group(4);
} else {
// #A 在前
questionContent = matcher.group(6);
standardAnswer = matcher.group(5);
}
exam.addQuestion(num, new BasicQuestion(num, questionContent, standardAnswer));
} else {
System.out.println("wrong format:" + inputLine);
}
} // 解析多选题
else if (inputLine.startsWith("#Z:")) {
Pattern mcPattern = Pattern.compile("#Z:(\\d+)\\s+#Q:(.+?)\\s+#A:(.+)");
Matcher matcher = mcPattern.matcher(inputLine);
if (matcher.matches()) {
int num = Integer.parseInt(matcher.group(1));
String questionContent = matcher.group(2);
String standardAnswer = matcher.group(3);
exam.addQuestion(num, new ChoiceQuestion(num, questionContent, standardAnswer));
} else {
System.out.println("wrong format:" + inputLine);
}
}
// 解析填空题
else if (inputLine.startsWith("#K:")) {
Pattern fbPattern = Pattern.compile("#K:(\\d+)\\s+#Q:(.+?)\\s+#A:(.+)");
Matcher matcher = fbPattern.matcher(inputLine);
if (matcher.matches()) {
int num = Integer.parseInt(matcher.group(1));
String questionContent = matcher.group(2);
String standardAnswer = matcher.group(3);
exam.addQuestion(num, new FillInTheBlankQuestion(num,questionContent, standardAnswer));
} else {
System.out.println("wrong format:" + inputLine);
}
}
// 解析试卷信息
else if (inputLine.startsWith("#T:")) {
Pattern testPattern = Pattern.compile("#T:(\\d+) ((\\d+-\\d+ ?)+)");
Matcher matcher = testPattern.matcher(inputLine);
if (matcher.matches()) {
int testPaperId = Integer.parseInt(matcher.group(1));
List<QuestionScore> paperQuestions = new ArrayList<>();
String[] parts = matcher.group(2).split(" ");
for (String part : parts) {
String[] tValues = part.split("-");
int questionNum = Integer.parseInt(tValues[0]);
int score = Integer.parseInt(tValues[1]);
paperQuestions.add(new QuestionScore(questionNum, score));
}
testPapers.put(testPaperId, paperQuestions);
int totalScore = paperQuestions.stream().mapToInt(QuestionScore::getScore).sum();
if (totalScore != 100) {
System.out.println("alert: full score of test paper " + testPaperId + " is not 100 points");
}
} else {
System.out.println("wrong format:" + inputLine);
}
}
// 解析学生信息
else if (inputLine.startsWith("#X:")) {
Pattern studentPattern = Pattern.compile("#X:(.+)");
Matcher matcher = studentPattern.matcher(inputLine);
if (matcher.matches()) {
String[] parts = inputLine.substring(3).split("-");
for (String part : parts) {
String[] studentData = part.split(" ");
int studentId = Integer.parseInt(studentData[0].trim());
String studentName = studentData[1].trim();
students.put(studentId, new Student(studentId, studentName));
}
} else {
System.out.println("wrong format:" + inputLine);
}
}
// 解析答卷信息
else if (inputLine.startsWith("#S:")) {
Pattern answerPattern = Pattern.compile("#S:(\\d+) (\\d+)(\\s+(#A:\\d+-(.+)*)*)*");
Matcher matcher = answerPattern.matcher(inputLine);
if (matcher.matches()) {
String[] parts = inputLine.split("#");
int testPaperId = Integer.parseInt(parts[1].split(" ")[0].split(":")[1].trim());
int studentId = Integer.parseInt(parts[1].split(" ")[1].trim()); // 提取学号
AnswerSheet answerSheet = new AnswerSheet(testPaperId);
if (parts.length > 2) {
for (int i = 2; i < parts.length; i++) {
if (parts[i].startsWith("A:")) {
String[] answerParts = parts[i].split("-");
if (answerParts.length == 2) {
int questionNum = Integer.parseInt(answerParts[0].split(":")[1].trim());
String str = answerParts[1]; // 去除可能的空格
String answer = removeLastSpace(str);
answerSheet.addAnswer(questionNum, answer);
}
}
}
}
answerSheets.computeIfAbsent(studentId, k -> new ArrayList<>()).add(answerSheet); // 将答卷关联到学生
} else {
System.out.println("wrong format:" + inputLine);
}
}
// 解析删除题目信息
else if (inputLine.startsWith("#D:N-")) {
Pattern deletePattern = Pattern.compile("#D:N-(\\d+)");
Matcher matcher = deletePattern.matcher(inputLine);
if (matcher.matches()) {
int questionNum = Integer.parseInt(inputLine.split("-")[1].trim());
exam.removeQuestion(questionNum); // 移除题目
}
else {
System.out.println("wrong format:" + inputLine);
}
}
} catch (Exception e) {
System.out.println("wrong format");
}
}
}
该函数 readExamData
用于从输入中解析并加载试卷系统的相关数据,包括题目、试卷、答卷和学生信息。它通过扫描输入的每一行,根据不同的前缀(如 #N:
、#T:
等)区分处理不同类型的数据。
解析的逻辑主要包括以下部分:
-
题目数据解析 :
针对不同类型的题目(普通题、多选题、填空题等),使用正则表达式提取题目编号、题干、标准答案等信息。根据题目类型创建相应的题目对象(如
BasicQuestion
、ChoiceQuestion
等),并将其添加到考试对象中。输入格式错误会输出提示。 -
试卷信息解析 :
试卷数据以
#T:
开头,提取试卷编号及其对应的题目编号和分值。分值被封装为QuestionScore
对象,并存入testPapers
映射中。同时校验总分是否为 100 分,否则会警告。 -
学生信息解析 :
以
#X:
开头的输入解析学生编号和姓名,将其存入students
映射中,关联学生 ID 和学生对象。 -
答卷信息解析 :
以
#S:
开头的输入解析学生提交的答卷,包括试卷编号、学生编号及其题目作答信息,将答卷与学生对应关系存储到answerSheets
中。 -
删除题目 :
以
#D:N-
开头的输入解析需要删除的题目编号,从考试中移除对应题目。
函数通过逐行处理输入,使用正则表达式确保数据格式的正确性,并对异常或格式错误的输入提供警告提示,同时确保将各类数据有序地存储到相应的结构中(如 Map
、List
等),为试卷系统的后续操作提供数据支持。
AnswerSheet 类用于记录考生对某张试卷的答题情况,以及根据试卷内容输出答案详情和得分情况。该类包含了试卷编号、题目信息、考生的答案,以及输出答案和计算得分的逻辑。
outputAnswers
方法输出考生的答案以及其对应的正确性。
实现细节:
- 遍历所有试卷题目(paperQuestions)。
- 从 Exam 中获取每道题的 Question 实例。
- 获取考生答案并进行以下判断:
- 如果试题不存在于试卷中,输出 non-existent question~0;
- 如果答案为空,输出 answer is null;
- 如果题目内容无效(如包含 "invalid"),输出内容加 ~0。
- 调用 Question 的 getAnswerCorrectnessLevel 方法,获取答案的正确性等级,并输出具体结果。
outputScores
类输出考生在每道题上的得分以及总分。
实现细节:
- 初始化 totalScore 和 earnedScore。
- 遍历所有题目:
- 如果题目不存在,得分为 0;
- 如果题目存在,调用 Question 的 calculateScore 方法计算该题得分,并累计到 totalScore。
- 按题目顺序输出每题得分,用空格分隔,最后输出总分。
时序图
踩坑心得
1. 乱序输入问题
- 问题描述:
比如 #N, #Z, #K, #T, #X, #S 等只要是正确格式的信息,可以以任意的先后顺序输入各类不同的信息。比如试卷可以出现在题目之前,删除题目的信息可以出现在题目之前等。
else if (inputLine.startsWith("#S:")) {
Pattern answerPattern = Pattern.compile("#S:(\\d+) (\\d+)( (#A:\\d+-(.+)*)*)*");
Matcher matcher = answerPattern.matcher(inputLine);
if (matcher.matches()) {
String[] parts = inputLine.split("#");
int testPaperId = Integer.parseInt(parts[1].split(" ")[0].split(":")[1].trim());
int studentId = Integer.parseInt(parts[1].split(" ")[1].trim()); // 提取学号
List<QuestionScore> paperQuestions = testPapers.get(testPaperId);
AnswerSheet answerSheet = new AnswerSheet(testPaperId, paperQuestions);
if (parts.length > 2) {
for (int i = 2; i < parts.length; i++) {
if (parts[i].startsWith("A:")) {
int questionnum = Integer.parseInt(parts[i].split("-")[0].split(":")[1].trim());
String answer = parts[i].split("-")[1].trim();
answerSheet.addAnswer(questionnum, answer);
}
}
}
answerSheets.computeIfAbsent(studentId, k -> new ArrayList<>()).add(answerSheet); // 将答卷关联到学生
}
在上面的代码中进行#S答卷内容的解析,如果按代码的逻辑先获得testPapers.get(testPaperId);就会导致乱序输入试卷在答卷前报错。
解决思路:
AnswerSheet answerSheet = new AnswerSheet(testPaperId);
修改AnswerSheet类构造方法,
List<QuestionScore> questionScores = testPapers.get(testPaperId);
answerSheet.setPaperQuestions(questionScores);
在输出类中获取questionScores 再将该值赋给该对象。
2. 多选题、填空题判分逻辑问题
- 问题描述:
①如果多选题答卷答案与标准答案部分相同,且没有包含不包括在标准答案中的答案,就判定为部分正确,分数计算为该题分数的一半,多余小数直接舍去。
②如果为填空题同理多选题,但标准答案以或连接多个答案,所以可以根据字符拆分正确答案来计算分值。
解决思路
-
多选题:
@Override public boolean isPartiallyCorrect(String answer) { String[] correctAnswers = this.standardAnswer.split(" "); String[] userAnswers = answer.split(" "); Set<String> correctSet = new HashSet<>(Arrays.asList(correctAnswers)); Set<String> userSet = new HashSet<>(Arrays.asList(userAnswers)); // 判断用户的答案是否为正确答案的子集,且没有多余选项 return correctSet.containsAll(userSet) && !userSet.equals(correctSet); }
-
填空题:
@Override public CorrectnessLevel getAnswerCorrectnessLevel(String answer) { if (isCorrect(answer)) { return CorrectnessLevel.CORRECT; } else if (isPartiallyCorrectForFillIn(answer)) { return CorrectnessLevel.PARTIALLY_CORRECT; } else { return CorrectnessLevel.INCORRECT; } } // 填空题的部分正确判断 private boolean isPartiallyCorrectForFillIn(String answer) { String[] part = standardAnswer.split("或"); for (String ne : part) { if (answer.equals(ne.trim())) { return true; } } return false; }
这些判断逻辑根据以下结构体来简化操作逻辑:
public enum CorrectnessLevel {
CORRECT,
PARTIALLY_CORRECT,
INCORRECT
}
改进建议
-
分清职责,结构更清晰
现在的代码把逻辑都放在一个类里,显得臃肿。可以拆分成负责输入输出、流程控制、题目解析等不同模块,各自只做自己的事。
-
减少重复,提升复用性
解析题目、验证答案时有很多重复代码。把这些重复逻辑抽取成公共方法或工具类,既省事又方便维护。
-
代码更易读,少写嵌套
多用早返回和拆分小方法的方式,避免复杂的 if-else 嵌套,让代码看起来更直观。
· 家居强电电路模拟程序 - 1
题目分析
1. 电路设备分类
设备分为控制设备 和受控设备两类,每类包含多种具体设备。
-
控制设备
- 开关(K) :
- 状态:
0(打开/turned on)
或1(关闭/closed)
。 - 功能:控制电压传递,状态为
1
时,输入电压传递到输出端;为0
时输出端电压固定为0
。
- 状态:
- 分档调速器(F) :
- 档位:
0
至3
。 - 功能:输入固定电压,通过档位调节输出电压比例(
0.3
、0.6
、0.9
)。
- 档位:
- 连续调速器(L) :
- 档位范围:
[0.00, 1.00]
,精确到两位小数。 - 功能:输出电压为档位值与输入电压的乘积。
- 档位范围:
- 开关(K) :
-
受控设备
- 白炽灯(B) :
- 工作状态:亮(0~200lux)或灭。
- 功能:根据电压差计算亮度(线性比例)。
- 日光灯(R) :
- 工作状态:亮度为
180lux
或0lux
。 - 功能:仅取决于是否有电压差。
- 工作状态:亮度为
- 吊扇(D) :
- 工作状态:停止或转动(转速范围 0~360 转/分钟)。
- 功能:根据电压差线性调整转速,低于 80V 停止。
- 白炽灯(B) :
2. 电路规则
- 电压传递规则 :
- 开关决定电压传递状态。
- 调速器通过档位或比例控制输出电压。
- 连接规则 :
- 串联方式,电压从电源依次传递。
- 所有设备需严格按照物理规律接入(如无反馈、并联等复杂情况)。
- 输入输出规则 :
- 所有设备连接以
VCC
为起点,GND
为终点。 - 输入无连接的引脚默认为接地(
0V
)。
- 所有设备连接以
- 设备编号与状态输出 :
- 同种设备按编号顺序依次输出状态。
3. 输入与输出
- 输入内容 :
- 设备连接信息。
- 控制设备调节信息。
- 电路结束标志(
end
)。
- 输出内容 :
- 所有设备的状态或参数值,按设备类型和编号顺序输出。
4. 功能实现核心:
- 设备状态更新 :
- 根据连接关系和调节操作动态更新每个设备的状态。
- 电压传递与计算 :
- 模拟串联电路中电压的分布和传递。
- 处理调速器对电压的调节和受控设备的状态计算。
运行逻辑分析
1. 初始化设备
- 程序通过继承和多态设计了多种设备类型,包括电源(
VCC
)、接地(GND
)、开关(Switch
)、分档调速器(StepSpeedController
)、连续调速器(ContinuousSpeedController
)、灯(白炽灯和日光灯)、风扇等。 - 这些设备通过继承基类
Device
,实现了多态行为,比如输入电压的设置和状态更新。 - 特殊设备(如
VCC
和GND
)在程序启动时被初始化。
2. 解析输入
- 程序从标准输入中读取指令,分为两类:
- 连接关系 (
[设备1-引脚 设备2-引脚]
):通过parseConnection
方法将设备连接信息解析成Device
对象,并将连接关系存储到Circuit
中。 - 控制命令 (
#设备编号:参数
):如开关状态切换、调速器调整等,这些命令被存储到commands
列表中。
- 连接关系 (
3. 建立电路连接
- 所有设备通过
connect
方法连接,形成设备链。Device
的nextDevice
属性记录下一个设备。 - 特殊处理:
- 如果某设备连接到
GND
,会调用Ground.setPreviousDevice
,设置电压为零。
- 如果某设备连接到
4. 执行命令
- 调用
executeCommands
方法,解析存储的命令并对设备进行操作:- 切换开关状态(如开关打开或关闭)。
- 调整分档调速器的档位。
- 设置连续调速器的电压比例。
- 在操作结束后,电路从
VCC
开始,通过setVoltage
触发电压向后传递,最终更新所有设备的状态。
5. 状态输出
- 调用
printStatus
方法,根据设备类型输出状态,包括:- 开关的开关状态。
- 调速器的档位。
- 灯的亮度。
- 风扇的转速。
- 每种设备的状态通过其自身的
updateState
方法根据输入电压更新。
知识点总结
1. 面向对象编程 (OOP)
- 抽象类 :
Device
是所有设备的基类,定义通用属性和方法(如inputVoltage
、connectTo
等),子类通过继承实现具体行为。 - 多态 :通过
Device
的引用调用子类重写的方法(如updateState
),实现设备的状态更新。 - 封装:各设备的具体实现细节被封装在子类中,对外提供统一的接口。
2. 继承与类层次设计
- 程序利用继承,设计了清晰的类层次:
Device
是基类。ControlDevice
和ControlledDevice
是子类,分别表示控制型设备和受控型设备。- 各种具体设备(如
Switch
、Fan
等)再进一步继承上述子类。
3. 电路仿真逻辑
- 电压传递 :设备通过
connectTo
方法形成链式连接,电压从电源VCC
开始逐级传递给后续设备。 - 状态更新:每个设备的状态由输入电压和自身的逻辑决定。
4. 数据结构
Map
存储设备:通过设备名称作为键,存储所有设备实例。List
存储连接关系:记录设备间的连接信息。List
存储命令:保存用户输入的操作指令。
5. 输入处理
- 使用字符串解析连接信息和控制命令。
- 运用了
String.split
方法对输入进行拆分,并对输入格式进行了简单验证。
6. 流式操作与排序
- 利用 Java 8 的流式操作对设备按名称排序后输出状态,代码简洁高效。
7. 异常处理与约束
- 设置电压、电流、亮度等参数时,对输入值进行了范围限制,确保模拟行为的合理性。
源码结构分析
1. 核心模块
Device
类:所有设备的抽象基类,定义了设备的通用属性和行为(如输入电压、输出电压、连接关系等)。- 子类划分 :
- 电源与接地 :
VCC
:电路电源,起始设备,负责提供固定电压。Ground
:电路接地,终止设备,确保电压差为0。
- 控制设备 :
Switch
:控制电路通断。StepSpeedController
:分档调速器。ContinuousSpeedController
:连续调速器。
- 受控设备 :
IncandescentLamp
:白炽灯,亮度根据电压线性变化。FluorescentLamp
:日光灯,亮度仅两种状态(180lux或0)。Fan
:风扇,转速根据电压非线性变化。
- 电源与接地 :
2. 电路管理模块
Circuit
类 :- 负责管理设备集合和连接关系。
- 提供设备连接、命令执行、状态输出的功能。
- 方法设计 :
addDevice
:添加设备到集合。connect
:记录设备连接关系。setConnections
:根据连接信息建立设备之间的串联关系。executeCommands
:执行控制命令,调整设备状态。printStatus
:打印设备状态。
3. 主程序模块
Main
类 :- 处理用户输入。
- 调用
Circuit
类的方法完成电路搭建与操作。
4. 类图设计如下:
以下是程序中几个主要方法的讲解,包括它们的功能、实现原理及作用:
1. Circuit.setConnections()
功能:
将所有已添加的设备按照连接关系(connect
方法记录)构建实际的电路。
实现逻辑:
- 遍历
connections
列表,逐个取出两个引脚之间的连接关系(pin1
和pin2
)。 - 确定
pin1
对应的设备与pin2
对应的设备之间的连接:- 如果
pin2
是接地设备(Ground
),通过Ground.setPreviousDevice()
设置pin1
为其上一个设备,并设置电压。 - 如果是普通设备,则通过设备的
connectTo
方法建立连接。
- 如果
作用:
- 将用户输入的电路描述翻译为程序内部的设备连接关系,为后续电压传递和逻辑控制提供支持。
关键代码:
java
for (String[] connection : connections) {
String pin1 = connection[0].split("-")[0];
String pin2 = connection[1].split("-")[0];
if (devices.get(pin2) instanceof Ground) {
Ground GND = (Ground) devices.get(pin2);
GND.setPreviousDevice(devices.get(pin1));
}
devices.get(pin1).connectTo(devices.get(pin2));
}
2. VCC.setVoltage()
功能:
为电路提供电压源,并向下传递电压到连接的下一个设备。
实现逻辑:
- 直接将电压值
voltage
传递给nextDevice
。 - 递归更新后续设备的输入电压(通过控制设备或受控设备的逻辑处理)。
作用:
- 模拟电路中电压从电源流向各设备的过程,开始整个电路的工作。
关键代码:
java
public void setVoltage() {
this.nextDevice.setInputVoltage(voltage);
}
3. Switch.toggleStatus()
功能:
切换开关的状态(开/关)。
实现逻辑:
- 切换
status
的布尔值。 - 根据当前状态(
true
/false
)更新开关的输出电压。 - 输出电压传递给下一个连接的设备(递归传播)。
作用:
- 提供对开关的基本操作,用户可以通过命令控制电路工作或中断。
关键代码:
java
public void toggleStatus() {
this.status = !this.status; // 切换状态
updateState(); // 更新设备状态
}
4. StepSpeedController.updateState()
功能:
根据档位(level
)调整设备的输出电压,并传递给下一个设备。
实现逻辑:
- 计算当前档位的电压比例(通过
getVoltageRatio
方法)。 - 根据比例计算输出电压,传递给连接的下一个设备。
- 如果连接的是开关,需检查开关状态;否则直接传递电压。
作用:
- 实现分档调速器对设备输入电压的调节。档位和比例对应实际工程中分段电路设计。
关键代码:
java
@Override
void updateState() {
this.outputVoltage = getVoltageRatio(level) * inputVoltage; // 根据档位比例计算输出电压
if (nextDevice != null) {
if (nextDevice instanceof ControlledDevice) {
((ControlledDevice) nextDevice).setInputVoltage(outputVoltage);
} else if (nextDevice instanceof Switch) {
((ControlDevice) nextDevice).setInputVoltage(outputVoltage);
}
}
}
5. Light.updateState()
功能:
更新灯的亮度,根据电位差(dianshicha
)计算亮度值并设置。
实现逻辑:
- 白炽灯亮度根据电位差线性计算:
- 0~10V:亮度为 0;
- 10V~220V:线性计算亮度。
- 日光灯亮度只有两种状态:
- 电位差为 0:亮度为 0;
- 电位差不为 0:亮度为 180lux。
作用:
- 模拟灯光设备的行为(白炽灯和日光灯特性不同),并体现亮度变化与输入电压的关系。
关键代码(以白炽灯为例):
java
@Override
protected void updateState() {
setDianshicha(); // 计算电位差
int brightness = 0;
if (dianshicha < 10) {
brightness = 0;
} else if (dianshicha > 10 && dianshicha <= 220) {
double ratio = (dianshicha - 10) / (220 - 10);
brightness = (int) (50 + ratio * 150);
}
this.setBrightness(brightness);
}
6. Circuit.printStatus()
功能:
以指定格式打印各设备的状态,供用户查看电路运行情况。
实现逻辑:
- 遍历
devices
,分别处理不同类型的设备:- 开关:打印是否打开;
- 分档调速器:打印档位;
- 连续调速器:打印电压比例;
- 灯:打印亮度;
- 风扇:打印转速。
作用:
- 汇总所有设备的运行状态,便于用户调试和分析电路行为。
关键代码(打印开关状态为例):
java
devices.values().stream()
.filter(device -> device instanceof Switch)
.sorted(Comparator.comparing(device -> device.name))
.forEach(device -> {
Switch s = (Switch) device;
System.out.println("@" + s.name + ":" + (!s.status ? "turned on" : "closed"));
});
5. 顺序图设计如下:
踩坑心得
问题 1:建立串联设备之间关系的难题
在电路设计中,设备是串联的(比如 VCC -> 开关 -> 灯
),更新设备状态时需要沿着串联关系逐一传播电压,但一开始不清楚如何在程序中表达这种连接关系并递归更新状态。
解决方案:
- 使用属性连接电路上的设备:
- 每个设备包含一个
nextDevice
属性,用于指向下一个设备。 - 更新电压时,递归调用
nextDevice
的更新方法。
- 每个设备包含一个
- 设计设备基类的接口:
- 提供统一的
updateState()
和connectTo(Device nextDevice)
方法。 - 子类只需在
updateState()
中实现自己的状态逻辑,无需关心全局串联关系。
- 提供统一的
实现代码示例:
java
abstract class Device {
protected Device nextDevice; // 下一个连接设备
protected double inputVoltage; // 当前设备输入电压
public void connectTo(Device nextDevice) {
this.nextDevice = nextDevice; // 建立连接
}
// 更新设备状态,子类需要重写
abstract void updateState();
public void setInputVoltage(double voltage) {
this.inputVoltage = voltage;
updateState(); // 根据输入电压更新自身状态
if (nextDevice != null) {
nextDevice.setInputVoltage(this.inputVoltage); // 递归传播电压
}
}
}
- 设备的串联关系通过
connectTo
方法逐步构建。 - 电压的更新从电源(
VCC
)向后传播,通过递归调用完成。
问题 2:未正确处理开关的状态对电压传播的影响
假设电路为:VCC -> 白炽灯 -> 开关
。
- 如果开关关闭,白炽灯的状态不应受电压影响,但在设计中可能直接将
VCC
的电压传递给白炽灯,忽略了开关状态。 - 开关的状态应决定电压是否能继续传递到后续设备。
问题分析:
- 忽视开关的断路行为:
- 开关断开时,应阻断电压传播,但可能在代码中直接递归调用后续设备的
setInputVoltage
方法。
- 开关断开时,应阻断电压传播,但可能在代码中直接递归调用后续设备的
- 开关行为未被单独抽象:
- 开关在电路中是特殊的设备,既要管理自己的状态(开/关),又要影响电压的传播。
改进方案:
- 引入开关逻辑:
- 开关应判断状态(开/关),仅在开启状态下向下传递电压。
- 修改电压传播逻辑:
- 在
Switch.updateState()
方法中,控制是否调用后续设备的setInputVoltage
方法。
- 在
实现代码示例:
java
class Switch extends Device {
private boolean status; // 开关状态,true为关闭,false为打开
public void toggleStatus() {
this.status = !this.status; // 切换开关状态
updateState();
}
@Override
void updateState() {
if (!status) { // 如果开关是打开状态
if (nextDevice != null) {
nextDevice.setInputVoltage(this.inputVoltage); // 传递电压
}
} else {
if (nextDevice != null) {
nextDevice.setInputVoltage(0); // 阻断电压
}
}
}
}
改进建议
1. 改进串联设备关系的灵活性
- 问题: 现有的单向链式结构(
nextDevice
)难以支持复杂电路拓扑(如并联、环路等)。 - 建议:
- 使用图或树结构代替单链表,以便更好地描述复杂的电路连接关系。
- 提供统一的连接接口,支持动态添加或移除设备。
2. 设备状态传播逻辑优化
- 问题: 电压传播逻辑和设备的工作状态强耦合,重复判断开关等设备的状态,逻辑分散。
- 建议:
- 引入统一的电源状态校验机制(如
isPowered()
方法),由设备自主决定是否传播电压或更新状态。 - 将电源开关的逻辑抽象为独立模块,减少其他设备对其内部逻辑的依赖。
- 引入统一的电源状态校验机制(如
3. 增强扩展性
- 问题: 不同设备的状态更新逻辑分散在各个类中,扩展新设备需要频繁修改核心代码。
- 建议:
- 使用策略模式或配置化设计,将设备的状态更新逻辑解耦为独立模块,便于扩展和维护。
- 提供统一的设备基类接口,子类仅需实现自己的功能逻辑。
4. 设计上的职责分离
- 问题: 部分设备(如开关)承担了过多的职责,如既要管理自身状态,还要控制电压传播。
- 建议:
- 将职责拆分为更小的模块(如一个专门管理电压传播的控制器)。
- 开关仅管理自己的开关状态,具体的传播逻辑交由上层处理。